akiyoko blog

akiyoko の IT技術系ブログです

Python でシンプルなテーブルを(PrettyTableを使わずに)出力する方法

Python で、

+--------------+----------+----------+
| Header 1     | Header 2 | Header 3 |
+--------------+----------+----------+
| aaa          | bbb      | ccc      |
| aaaaaaaaaaaa | bb       | ccccc    |
| a            | b        |          |
+--------------+----------+----------+

こんな感じのテーブル表記をするなら、「PrettyTable」を使うのが一般的でしょうか。

PrettyTable

GitHubには本家のコードはないようです。誰かが作ったクローンなら これ とか)



有名なプロダクトでも普通に使われているようです。
例えば、OpenStack の コマンドクライアントでは、コマンドの結果をテーブル表記で出力するのに使っていますね。
https://github.com/openstack/python-novaclient/blob/master/novaclient/utils.py



しかしながら、標準で入っていないことが多いので、

$ sudo pip install prettytable
$ pip list | grep prettytable
prettytable (0.7.2)

$ python
Python 2.7.5 (default, Mar  9 2014, 22:15:05) 
>>> import prettytable

と、インストールが必要なのが若干難点です。



本番環境とかでちょっとしたコマンドを作りたいときなど、余計なソフトをインストールしたくないケースでは、PrettyTable が候補にならない場合がありますよね(少なくとも私の場合は)。


ということで、家で(!)せっせか自作してみました。


simple_table.py

# -*- coding: utf-8 -*-
from itertools import izip_longest


class SimpleTable(object):
    """
    SimpleTable
    """

    def __init__(self, header=None, rows=None):
        self.header = header or ()
        self.rows = rows or []

    def set_header(self, header):
        self.header = header

    def add_row(self, row):
        self.rows.append(row)

    def _calc_maxes(self):
        array = [self.header] + self.rows
        return [max(len(str(s)) for s in ss) for ss in izip_longest(*array, fillvalue='')]

    def _get_printable_row(self, row):
        maxes = self._calc_maxes()
        return '| ' + ' | '.join([('{0: <%d}' % m).format(r) for r, m in izip_longest(row, maxes, fillvalue='')]) + ' |'

    def _get_printable_header(self):
        return self._get_printable_row(self.header)

    def _get_printable_border(self):
        maxes = self._calc_maxes()
        return '+-' + '-+-'.join(['-' * m for m in maxes]) + '-+'

    def get_table(self):
        lines = []
        if self.header:
            lines.append(self._get_printable_border())
            lines.append(self._get_printable_header())
        lines.append(self._get_printable_border())
        for row in self.rows:
            lines.append(self._get_printable_row(row))
        lines.append(self._get_printable_border())
        return lines

    def print_table(self):
        lines = self.get_table()
        for line in lines:
            print(line)

GitHub にもアップしておきました。
https://github.com/akiyoko/python-simple-table/blob/master/simple_table.py



SimpleTable の使い方

使い方はこんな感じです。

>>> from simple_table import SimpleTable
>>>
>>> table = SimpleTable()
>>> table.set_header(('Header 1', 'Header 2', 'Header 3'))
>>> table.add_row(('aaa', 'bbb', 'ccc'))
>>> table.add_row(('aaaaaaaaaaaa', 'bb', 'ccccc'))
>>> table.add_row(('a', 'b'))
>>> table.print_table()
+--------------+----------+----------+
| Header 1     | Header 2 | Header 3 |
+--------------+----------+----------+
| aaa          | bbb      | ccc      |
| aaaaaaaaaaaa | bb       | ccccc    |
| a            | b        |          |
+--------------+----------+----------+



あまりないかもしれませんが、こういう使い方もできます。

>>> table = SimpleTable(('Header 1', 'Header 2', 'Header 3'), [('aaa', 'bbb', 'ccc'), ('aaaaaaaaaaaa', 'bb', 'ccccc'), ('a', 'b')])
>>> table.print_table()
+--------------+----------+----------+
| Header 1     | Header 2 | Header 3 |
+--------------+----------+----------+
| aaa          | bbb      | ccc      |
| aaaaaaaaaaaa | bb       | ccccc    |
| a            | b        |          |
+--------------+----------+----------+

>>> table = SimpleTable(None, [('aaa', 'bbb', 'ccc'), ('aaaaaaaaaaaa', 'bb', 'ccccc'), ('a', 'b')])
>>> table.print_table()
+--------------+-----+-------+
| aaa          | bbb | ccc   |
| aaaaaaaaaaaa | bb  | ccccc |
| a            | b   |       |
+--------------+-----+-------+



 

事前準備

インタプリタで実行する場合は、事前に simple_table.py にパスを通しておく必要があります。
いろいろやり方がありますが、いくつか方法を挙げておきます。

(方法その1)

Mac における Pythonの標準ライブラリ「/Library/Python/2.7/site-packages/」に pyファイルを直接置くのが一番簡単かもしれません。

$ sudo cp /Users/akiyoko/github/python-simple-table/simple_table.py /Library/Python/2.7/site-packages/

(方法その2)

あるいは、Pythonインタープリタを実行するカレントディレクトリに置くか、

$ cp /Users/akiyoko/github/python-simple-table/simple_table.py .
$ python
Python 2.7.5 (default, Mar  9 2014, 22:15:05) 
>>> import simple_table

(方法その3)

環境変数「PYTHONPATH」を使う方法もあります。もちろん、.bash_profile に書くのもアリです。

$ PYTHONPATH="/Users/akiyoko/github/python-simple-table:$PYTHONPATH"
$ export PYTHONPATH
$ python
Python 2.7.5 (default, Mar  9 2014, 22:15:05) 
>>> import simple_table

参考

ちょっと解説

1) 各カラムの最大サイズの取り方

これを参考にしつつ、ちょっと工夫してみました。
http://stackoverflow.com/questions/6018916/find-max-length-of-each-column-in-a-list-of-lists

>>> from itertools import izip_longest
>>> array = [(1, 'This is a test', 12039), (12, 'test', 1235)]
>>> [max(len(str(s)) for s in ss) for ss in izip_longest(*array, fillvalue='')]
[2, 14, 5]

zip を使わなかったのは、カラムの数が違う場合にカラム数が短い方に合わせて縮小させられてしまうからです。

>>> array = [(1, 'This is a test', 12039), (12, 'test')]
>>> [max(len(str(s)) for s in ss) for ss in zip(*array)]
[2, 14]  # これを [2, 14, 5] となるようにしたい

 

2) zip と map、そして izip_longest

複数のリスト(やタプル)から同じインデックスにある要素をセットにする場合、zip や map を使いますが、zip は短い方、map は長い方に合わせてくれます。

参考

map の第一引数には関数を指定できるのですが、zip と同じ使い方をしたいなら「None」でよいでしょう。
それでも若干違いがあるのですが、今回 map を使わなかったのはその違いが影響したためでした。結局、izip_longest というのを使っています。


 

3) 高度な文字列フォーマット

format() を使って、柔軟な文字列フォーマットができます。「<」「>」「^」のメタ記号で、左寄せ・右寄せ・センタリングが指定できます。
便利ですね。

>>> '{0: <10}'.format('12345')
'12345     '
>>> '{0: >10}'.format('12345')
'     12345'
>>> '{0: ^10}'.format('12345')
'  12345   '

Python 組み込み関数の ljust(), rjust(), center() を使っても同じ結果が得られます。

>>> '12345'.ljust(10)
'12345     '
>>> '12345'.rjust(10)
'     12345'
>>> '12345'.center(10)
'  12345   '

参考



 

日本語にも対応させました (※2014/6/25 追記)

日本語(2バイト文字)に対応していなかったので、対応させました。
len() だけで横幅を計算しようとしていたのがダメだったようでした。

len() は str型 と unicode型 で微妙に利用目的?が違うらしく


str: バイト数
unicode: 文字数


なので、それぞれちゃんと使い分けてください、私。

Python で文字数を数えるときの注意点 - みひゃろぐ」より


そこで、

あたりを参考にさせていただき、日本語(2バイト文字)を2文字としてカウントするように修正しました。

具体的にはこんな感じ。

def _unicode_width(self, s, width={'F': 2, 'H': 1, 'W': 2, 'Na': 1, 'A': 2, 'N': 1}):
    s = unicode(s)
    return sum(width[east_asian_width(c)] for c in s)

詳しくは、https://github.com/akiyoko/python-simple-table/blob/master/simple_table.py



おかげで、こんな勝敗表を作ることができましたー。

>>> from simple_table import SimpleTable
>>> table = SimpleTable()
>>> table.set_header(('', u'', u'', u'', u'勝点', u'得失差'))
>>> table.add_row((u'コロンビア', 3, 0, 0, 9, u'+7'))
>>> table.add_row((u'ギリシャ', 1, 1, 1, 4, u'-2'))
>>> table.add_row((u'コートジボワール', 1, 0, 2, 3, u'-1'))
>>> table.add_row((u'日本', 0, 1, 2, 1, u'-4'))
>>> table.print_table()
+------------------+----+----+----+------+--------+
|                  | 勝 | 分 | 敗 | 勝点 | 得失差 |
+------------------+----+----+----+------+--------+
| コロンビア       | 3  | 0  | 0  | 9    | +7     |
| ギリシャ         | 1  | 1  | 1  | 4    | -2     |
| コートジボワール | 1  | 0  | 2  | 3    | -1     |
| 日本             | 0  | 1  | 2  | 1    | -4     |
+------------------+----+----+----+------+--------+

はてなブログで書くとテーブルがずれてしまいますね。。まあ、しゃーない。)




 

PrettyTable の使い方

最後に、PrettyTable の使い方を紹介。
チュートリアルそのままですが。
https://code.google.com/p/prettytable/wiki/Tutorial

>>> from prettytable import PrettyTable
>>> x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"])
>>> x.add_row(["Adelaide",1295, 1158259, 600.5])
>>> x.add_row(["Brisbane",5905, 1857594, 1146.4])
>>> x.add_row(["Darwin", 112, 120900, 1714.7])
>>> x.add_row(["Hobart", 1357, 205556, 619.5])
>>> x.add_row(["Sydney", 2058, 4336374, 1214.8])
>>> x.add_row(["Melbourne", 1566, 3806092, 646.9])
>>> x.add_row(["Perth", 5386, 1554769, 869.4])
>>> print x.get_string()
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
|  Adelaide | 1295 |  1158259   |      600.5      |
|  Brisbane | 5905 |  1857594   |      1146.4     |
|   Darwin  | 112  |   120900   |      1714.7     |
|   Hobart  | 1357 |   205556   |      619.5      |
|   Sydney  | 2058 |  4336374   |      1214.8     |
| Melbourne | 1566 |  3806092   |      646.9      |
|   Perth   | 5386 |  1554769   |      869.4      |
+-----------+------+------------+-----------------+

>>> print x.get_html_string()
<table>
    <tr>
        <th>City name</th>
        <th>Area</th>
        <th>Population</th>
        <th>Annual Rainfall</th>
    </tr>
    <tr>
        <td>Adelaide</td>
        <td>1295</td>
        <td>1158259</td>
        <td>600.5</td>
    </tr>
    ・
    ・

参考: