如何用Python漂亮地打印ASCII表?

How can I pretty-print ASCII tables with Python?

我正在寻找一种方法来漂亮地打印这样的表格:

1
2
3
4
5
6
=======================
| column 1 | column 2 |
=======================
| value1   | value2   |
| value3   | value4   |
=======================

我找到了asciitable库,但它不做边框等。我不需要任何复杂的数据项格式,它们只是字符串。我需要它来自动调整列的大小。

是否存在其他库或方法,或者我是否需要花费几分钟来编写自己的库或方法?


很久以前我就读过这个问题,并为表格写了自己漂亮的打印机:tabulate

我的用例是:

  • 我大部分时间都想要一个单程航班
  • 哪一个足够聪明,能为我找到最好的格式
  • 可以输出不同的纯文本格式

例如,grid可能是最相似的输出格式:

1
2
3
4
5
6
7
8
9
from tabulate import tabulate
print tabulate([["value1","value2"], ["value3","value4"]], ["column 1","column 2"], tablefmt="grid")
+------------+------------+
| column 1   | column 2   |
+============+============+
| value1     | value2     |
+------------+------------+
| value3     | value4     |
+------------+------------+

其他支持的格式有:plain(无行)、simple(pandoc简单表)、pipe(类似php markdown extra中的表)、orgtbl(类似emacs的org模式中的表)、rst(类似restructuredtext中的简单表)。gridorgtbl在emacs中很容易编辑。

从性能上看,tabulateasciitable稍慢,但比PrettyTabletexttable快得多。

另外,我也很喜欢用十进制列来排列数字。所以这是数字的默认对齐方式(如果有)(可重写)。


这里有一个快速而肮脏的小函数,我编写它来显示我只能通过SOAP API进行的SQL查询的结果。它期望一个或多个namedtuples序列的输入作为表行。如果只有一个记录,它会以不同的方式打印出来。

它对我来说很方便,可以作为你的起点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def pprinttable(rows):
  if len(rows) > 1:
    headers = rows[0]._fields
    lens = []
    for i in range(len(rows[0])):
      lens.append(len(max([x[i] for x in rows] + [headers[i]],key=lambda x:len(str(x)))))
    formats = []
    hformats = []
    for i in range(len(rows[0])):
      if isinstance(rows[0][i], int):
        formats.append("%%%dd" % lens[i])
      else:
        formats.append("%%-%ds" % lens[i])
      hformats.append("%%-%ds" % lens[i])
    pattern =" |".join(formats)
    hpattern =" |".join(hformats)
    separator ="-+-".join(['-' * n for n in lens])
    print hpattern % tuple(headers)
    print separator
    _u = lambda t: t.decode('UTF-8', 'replace') if isinstance(t, str) else t
    for line in rows:
        print pattern % tuple(_u(t) for t in line)
  elif len(rows) == 1:
    row = rows[0]
    hwidth = len(max(row._fields,key=lambda x: len(x)))
    for i in range(len(row)):
      print"%*s = %s" % (hwidth,row._fields[i],row[i])

样品输出:

1
2
3
4
5
6
7
8
9
10
11
12
pkid                                 | fkn                                  | npi
-------------------------------------+--------------------------------------+----
405fd665-0a2f-4f69-7320-be01201752ec | 8c9949b9-552e-e448-64e2-74292834c73e | 0
5b517507-2a42-ad2e-98dc-8c9ac6152afa | f972bee7-f5a4-8532-c4e5-2e82897b10f6 | 0
2f960dfc-b67a-26be-d1b3-9b105535e0a8 | ec3e1058-8840-c9f2-3b25-2488f8b3a8af | 1
c71b28a3-5299-7f4d-f27a-7ad8aeadafe0 | 72d25703-4735-310b-2e06-ff76af1e45ed | 0
3b0a5021-a52b-9ba0-1439-d5aafcf348e7 | d81bb78a-d984-e957-034d-87434acb4e97 | 1
96c36bb7-c4f4-2787-ada8-4aadc17d1123 | c171fe85-33e2-6481-0791-2922267e8777 | 1
95d0f85f-71da-bb9a-2d80-fe27f7c02fe2 | 226f964c-028d-d6de-bf6c-688d2908c5ae | 1
132aa774-42e5-3d3f-498b-50b44a89d401 | 44e31f89-d089-8afc-f4b1-ada051c01474 | 1
ff91641a-5802-be02-bece-79bca993fdbc | 33d8294a-053d-6ab4-94d4-890b47fcf70d | 1
f3196e15-5b61-e92d-e717-f00ed93fe8ae | 62fa4566-5ca2-4a36-f872-4d00f7abadcf | 1

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from collections import namedtuple
>>> Row = namedtuple('Row',['first','second','third'])
>>> data = Row(1,2,3)
>>> data
Row(first=1, second=2, third=3)
>>> pprinttable([data])
 first = 1
second = 2
 third = 3
>>> pprinttable([data,data])
first | second | third
------+--------+------
    1 |      2 |     3
    1 |      2 |     3


出于某种原因,当我在谷歌搜索中包含"docutils"时,我偶然发现了texttable,这似乎是我要找的。


我也写了自己的解决方案。我尽量保持简单。

https://github.com/robpol86/terminaltables网站

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from terminaltables import AsciiTable
table_data = [
    ['Heading1', 'Heading2'],
    ['row1 column1', 'row1 column2'],
    ['row2 column1', 'row2 column2']
]
table = AsciiTable(table_data)
print table.table
+--------------+--------------+
| Heading1     | Heading2     |
+--------------+--------------+
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
+--------------+--------------+

table.inner_heading_row_border = False
print table.table
+--------------+--------------+
| Heading1     | Heading2     |
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
+--------------+--------------+

table.inner_row_border = True
table.justify_columns[1] = 'right'
table.table_data[1][1] += '
newline'

print table.table
+--------------+--------------+
| Heading1     |     Heading2 |
+--------------+--------------+
| row1 column1 | row1 column2 |
|              |      newline |
+--------------+--------------+
| row2 column1 | row2 column2 |
+--------------+--------------+

使用W3M设计用于处理类型Matth的版本接受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import subprocess
import tempfile
import html
def pprinttable(rows):
    esc = lambda x: html.escape(str(x))
    sour ="<table border=1>"
    if len(rows) == 1:
        for i in range(len(rows[0]._fields)):
            sour +="<tr><th>%s<td>%s" % (esc(rows[0]._fields[i]), esc(rows[0][i]))
    else:
        sour +="<tr>" +"".join(["<th>%s" % esc(x) for x in rows[0]._fields])
        sour +="".join(["<tr>%s" %"".join(["<td>%s" % esc(y) for y in x]) for x in rows])
    with tempfile.NamedTemporaryFile(suffix=".html") as f:
        f.write(sour.encode("utf-8"))
        f.flush()
        print(
            subprocess
            .Popen(["w3m","-dump",f.name], stdout=subprocess.PIPE)
            .communicate()[0].decode("utf-8").strip()
        )

from collections import namedtuple
Row = namedtuple('Row',['first','second','third'])
data1 = Row(1,2,3)
data2 = Row(4,5,6)
pprinttable([data1])
pprinttable([data1,data2])

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌───────┬─┐
│ first │1
├───────┼─┤
│second │2
├───────┼─┤
│ third │3
└───────┴─┘
┌─────┬───────┬─────┐
│first│second │third│
├─────┼───────┼─────┤
1    │2      │3    │
├─────┼───────┼─────┤
4    │5      │6    │
└─────┴───────┴─────┘

如果您想要一个具有列和行跨距的表,请尝试我的库dashtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from dashtable import data2rst

table = [
        ["Header 1","Header 2","Header3","Header 4"],
        ["row 1","column 2","column 3","column 4"],
        ["row 2","Cells span columns.","",""],
        ["row 3","Cells
span rows."
,"- Cells
- contain
- blocks"
,""],
        ["row 4","","",""]
    ]

# [Row, Column] pairs of merged cells
span0 = ([2, 1], [2, 2], [2, 3])
span1 = ([3, 1], [4, 1])
span2 = ([3, 3], [3, 2], [4, 2], [4, 3])

my_spans = [span0, span1, span2]

print(data2rst(table, spans=my_spans, use_headers=True))

输出:

1
2
3
4
5
6
7
8
9
10
11
+----------+------------+----------+----------+
| Header 1 | Header 2   | Header3  | Header 4 |
+==========+============+==========+==========+
| row 1    | column 2   | column 3 | column 4 |
+----------+------------+----------+----------+
| row 2    | Cells span columns.              |
+----------+----------------------------------+
| row 3    | Cells      | - Cells             |
+----------+ span rows. | - contain           |
| row 4    |            | - blocks            |
+----------+------------+---------------------+


你可以试试漂亮的。它做你想做的。以下是它文档中的一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> from beautifultable import BeautifulTable
>>> table = BeautifulTable()
>>> table.column_headers = ["name","rank","gender"]
>>> table.append_row(["Jacob", 1,"boy"])
>>> table.append_row(["Isabella", 1,"girl"])
>>> table.append_row(["Ethan", 2,"boy"])
>>> table.append_row(["Sophia", 2,"girl"])
>>> table.append_row(["Michael", 3,"boy"])
>>> print(table)
+----------+------+--------+
|   name   | rank | gender |
+----------+------+--------+
|  Jacob   |  1   |  boy   |
+----------+------+--------+
| Isabella |  1   |  girl  |
+----------+------+--------+
|  Ethan   |  2   |  boy   |
+----------+------+--------+
|  Sophia  |  2   |  girl  |
+----------+------+--------+
| Michael  |  3   |  boy   |
+----------+------+--------+

我使用这个小的效用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_pretty_table(iterable, header):
    max_len = [len(x) for x in header]
    for row in iterable:
        row = [row] if type(row) not in (list, tuple) else row
        for index, col in enumerate(row):
            if max_len[index] < len(str(col)):
                max_len[index] = len(str(col))
    output = '-' * (sum(max_len) + 1) + '
'

    output += '|' + ''.join([h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '
'

    output += '-' * (sum(max_len) + 1) + '
'

    for row in iterable:
        row = [row] if type(row) not in (list, tuple) else row
        output += '|' + ''.join([str(c) + ' ' * (l - len(str(c))) + '|' for c, l in zip(row, max_len)]) + '
'

    output += '-' * (sum(max_len) + 1) + '
'

    return output

print get_pretty_table([[1, 2], [3, 4]], ['header 1', 'header 2'])

输出

1
2
3
4
5
6
-----------------
|header 1|header 2|
-----------------
|1       |2       |
|3       |4       |
-----------------


我知道这个问题有点老,但我的尝试是:

https://gist.github.com/lonetwin/4721748

它的imho可读性更高(尽管它不像@matth的解决方案那样区分单行/多行,也不使用namedtuples)。


我的解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def make_table(columns, data):
   """Create an ASCII table and return it as a string.

    Pass a list of strings to use as columns in the table and a list of
    dicts. The strings in 'columns' will be used as the keys to the dicts in
    'data.'

    Not all column values have to be present in each data dict.

    >>> print(make_table(["a","b"], [{"a":"1","b":"test"}]))
    | a | b    |
    |----------|
    | 1 | test |
   """

    # Calculate how wide each cell needs to be
    cell_widths = {}
    for c in columns:
        values = [str(d.get(c,"")) for d in data]
        cell_widths[c] = len(max(values + [c]))

    # Used for formatting rows of data
    row_template ="|" +" {} |" * len(columns)

    # CONSTRUCT THE TABLE

    # The top row with the column titles
    justified_column_heads = [c.ljust(cell_widths[c]) for c in columns]
    header = row_template.format(*justified_column_heads)
    # The second row contains separators
    sep ="|" +"-" * (len(header) - 2) +"|"
    # Rows of data
    rows = []
    for d in data:
        fields = [str(d.get(c,"")).ljust(cell_widths[c]) for c in columns]
        row = row_template.format(*fields)
        rows.append(row)

    return"
"
.join([header, sep] + rows)

我刚刚发布了asciiplotlib,它也有漂亮的桌子。例如,这个

1
2
3
4
5
6
7
8
9
10
import asciiplotlib as apl

data = [
    [["a","bb","ccc"]],
    [[1, 2, 3], [613.23236243236, 613.23236243236, 613.23236243236]],
]

fig = apl.figure()
fig.table(data, border_style="thin", ascii_mode=True, padding=(0, 1), alignment="lcr")
fig.show()

得到你

1
2
3
4
5
6
7
+-----------------+-----------------+-----------------+
| a               |       bb        |             ccc |
+=================+=================+=================+
| 1               |        2        |               3 |
+-----------------+-----------------+-----------------+
| 613.23236243236 | 613.23236243236 | 613.23236243236 |
+-----------------+-----------------+-----------------+

默认情况下,该表以Unicode制表符呈现,

1
2
3
4
5
6
7
┌─────────────────┬─────────────────┬─────────────────┐
│ a               │ bb              │ ccc             │
╞═════════════════╪═════════════════╪═════════════════╡
1               │ 2               │ 3               │
├─────────────────┼─────────────────┼─────────────────┤
613.23236243236613.23236243236613.23236243236
└─────────────────┴─────────────────┴─────────────────┘

APL的表是非常可配置的;请查看测试以获取更多示例。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from sys import stderr, stdout    
def create_table(table: dict, full_row: bool = False) -> None:

        min_len = len(min((v for v in table.values()), key=lambda q: len(q)))
        max_len = len(max((v for v in table.values()), key=lambda q: len(q)))

        if min_len < max_len:
            stderr.write("Table is out of shape, please make sure all columns have the same length.")
            stderr.flush()
            return

        additional_spacing = 1

        heading_separator = '| '
        horizontal_split = '| '

        rc_separator = ''
        key_list = list(table.keys())
        rc_len_values = []
        for key in key_list:
            rc_len = len(max((v for v in table[key]), key=lambda q: len(str(q))))
            rc_len_values += ([rc_len, [key]] for n in range(len(table[key])))

            heading_line = (key + ("" * (rc_len + (additional_spacing + 1)))) + heading_separator
            stdout.write(heading_line)

            rc_separator += ("-" * (len(key) + (rc_len + (additional_spacing + 1)))) + '+-'

            if key is key_list[-1]:
                stdout.flush()
                stdout.write('
'
+ rc_separator + '
'
)

        value_list = [v for vl in table.values() for v in vl]

        aligned_data_offset = max_len

        row_count = len(key_list)

        next_idx = 0
        newline_indicator = 0
        iterations = 0

        for n in range(len(value_list)):
            key = rc_len_values[next_idx][1][0]
            rc_len = rc_len_values[next_idx][0]

            line = ('{:{}} ' +"" * len(key)).format(value_list[next_idx], str(rc_len + additional_spacing)) + horizontal_split

            if next_idx >= (len(value_list) - aligned_data_offset):
                next_idx = iterations + 1
                iterations += 1
            else:
                next_idx += aligned_data_offset

            if newline_indicator >= row_count:
                if full_row:
                    stdout.flush()
                    stdout.write('
'
+ rc_separator + '
'
)
                else:
                    stdout.flush()
                    stdout.write('
'
)

                newline_indicator = 0

            stdout.write(line)
            newline_indicator += 1

        stdout.write('
'
+ rc_separator + '
'
)
        stdout.flush()

例子:

1
2
3
4
5
6
table = {
       "uid": ["0","1","2","3"],
       "name": ["Jon","Doe","Lemma","Hemma"]
    }

create_table(table)

输出:

1
2
3
4
5
6
7
uid   | name       |
------+------------+-
0     | Jon        |
1     | Doe        |
2     | Lemma      |
3     | Hemma      |
------+------------+-


只有使用列表和字符串理解的非常紧凑的内置模块才能做到这一点。接受所有相同格式的词典列表…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def tableit(dictlist):
    lengths = [ max(map(lambda x:len(x.get(k)), dictlist) + [len(k)]) for k in dictlist[0].keys() ]
    lenstr =" |".join("{:<%s}" % m for m in lengths)
    lenstr +="
"


    outmsg = lenstr.format(*dictlist[0].keys())
    outmsg +="-" * (sum(lengths) + 3*len(lengths))
    outmsg +="
"

    outmsg +="".join(
        lenstr.format(*v) for v in [ item.values() for item in dictlist ]
    )
    return outmsg