[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Pythonできれいに揃ったテキストのレポートを作りたかった

要は、SQLの結果とかでよくあるこういう整ったレポートを作るには。という話です。

 EMPNO ENAME    JOB               MGR HIREDATE        SAL       COMM     DEPTNO
------ -------- ---------- ---------- -------- ---------- ---------- ----------
  7369 SMITH    CLERK            7902 80-12-17        800                    20
  7499 ALLEN    SALESMAN         7698 81-02-20       1600        300         30
  7521 WARD     SALESMAN         7698 81-02-22       1250        500         30
  7566 JONES    MANAGER          7839 81-04-02       2975                    20
:

必要な情報

整ったレポートを作るには、各列の文字列最大長が必要になります。最大長さえ手に入れば、Pythonにはljustrjustがあるので容易に整えることができます。

配列の最大長

配列内の最大長を得るには各要素のlenを取ってmaxを使えば良いです。ただ、int等の場合も考慮するとstr()で文字列にしてからlenを取るといいでしょう。このようなケースではリスト内包式を使うと楽です。

data = ["SMITH", "ALLEN", "WORD"]
max([len(str(x)) for x in data])  # --> SMITH,ALLENの5が戻る

最大長が取得できました。

対象のデータ型

SQLの結果のような形式の場合、大抵は以下のようなlist[dict]の形式になっていると思います。

[
    {
        "empno": "7369",
        "ename": "SMITH",
        "job": "CLERK",
        "mgr": "7902",
        "hiredate": "1980-12-17T00:00:00.000Z",
        "sal": "800.00",
        "comm": null,
        "deptno": "20"
    },
    {
        "empno": "7499",
        "ename": "ALLEN",
        "job": "SALESMAN",
        "mgr": "7698",
        "hiredate": "1981-02-20T00:00:00.000Z",
        "sal": "1600.00",
        "comm": "300.00",
        "deptno": "30"
    }
]

そのため、任意の列の最大長を取れるように以下のような関数があると便利だと思います。

def get_max_str_length(raw_list_dict_data, column_name):
    """
    指定したlist[dict]について指定した各列を取得し、その列の最大長を戻す
 
    :param list[dict] raw_list_dict_data:
    :param str column_name:
    :return:
    """
    max_length = max([len(str(dict_[column_name])) for dict_ in raw_list_dict_data])
    # カラム名も結局ヘッダに使うのでそちらのほうが長いかもチェック
    if len(column_name) > max_length:
        return len(column_name)
    return max_length

あとは、この結果を元にrjustljustをする関数を書けば良いです。

def get_pretty_report(base_data, columns):
    """
    インデント等がきれいに揃った形で結果を戻す
 
    :param list[dict] base_data: 元データ
    :param list[str] columns: 元データのうち表示する列のリスト
    :return:
    """
 
    ret_str = ""
    # 各列の最大長のDictを作る
    max_length_dict = {x: get_max_str_length(base_data, x) for x in columns}
 
    # まずはヘッダーを作る
    header_str = "  ".join([x.rjust(max_length_dict[x]) for x in columns])
    # ヘッダーの下の---- --- みたいなところを作る
    header_line = "  ".join(["-".rjust(max_length_dict[x], "-") for x in columns])
    ret_str += "{}\n".format(header_str)
    ret_str += "{}\n".format(header_line)
 
    # データ部分を追加していく
    for row_data in base_data:
        row_data_list = []
        for col in columns:
            col_data = row_data[col]
            # 文字列でない場合はstr()でキャストする
            if hasattr(col_data, "rjust"):
                if col_data:
                    row_data_list.append(col_data.rjust(max_length_dict[col]))
            else:
                # Noneは0にしてしまう
                if col_data:
                    row_data_list.append(str(col_data).rjust(max_length_dict[col]))
                else:
                    row_data_list.append(str(0).rjust(max_length_dict[col]))
        # 1行分の組み立て
        ret_str += "{}\n".format("  ".join(row_data_list))
 
    return ret_str

これはwsgi_lineprof_reporterで使ったコードを元にしています。これくらい汎化しておけば、元データと列を書き換えるだけで整ったレポートが作れるので、SQLの結果を受けている場合などに便利だと思います。