如何使用Python美化打印ASCII表格?

97

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

=======================
| column 1 | column 2 |
=======================
| value1   | value2   |
| value3   | value4   |
=======================

我发现了asciitable库,但它不包括边框等内容。我不需要数据项的复杂格式化,它们只是字符串。但我需要自动调整列的大小。

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


为什么不使用docutils来完成这项任务呢? - S.Lott
你如何称呼一个表?表中的数据是如何组织的?value1,value2,value3,value4……是否是列表中连续的值?我认为fomat()足以获得这样一个简单的显示,不需要在长时间的教程中学习使用库来节省时间。 - eyquem
2
@korona:不,我不是在提建议。我在问一个问题。我不知道@kdt知道什么或不知道什么。与其假设,我觉得有必要问一下。 - S.Lott
6
听起来你是假设他知道docutils,而事实上可能他不知道? - korona
2
@S.Lott 我看过docutils,虽然它当然非常适合将文本转换为html、latex等格式,但我没有看到一种方法来生成漂亮的文本表格,其中列对齐并且使用固定宽度字体看起来很漂亮。你是否误解了kdt的目标,还是我漏掉了什么? - nealmcb
可能是在Python中打印表格数据的重复问题。 - Martin Thoma
13个回答

98

我很久之前就读过这个问题,并编写了自己的表格漂亮打印程序:tabulate

我的用例是:

  • 我大多数时间都想要一个一行代码解决的方法
  • 它足够智能,可以为我找到最佳格式
  • 并且可以输出不同的纯文本格式

根据你的示例,grid 可能是最相似的输出格式:

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-mode中的表格),rst(类似于reStructuredText中的简单表格)。gridorgtbl在Emacs中易于编辑。

就性能而言,tabulateasciitable稍慢,但比PrettyTabletexttable要快得多。

附注:我也很喜欢使用十进制列对齐数字。因此,如果有数字,则默认对齐方式为按小数点对齐(可重写)。


4
我很巧地需要一个制表解决方案,幸运地找到了你的库!非常好用 :D 如果你在听,请允许我说一声 谢谢 :) - deepak
2
是的,我在听。谢谢你的好话。得到积极的反馈真的很不错。 - sastanin
1
嗨,@sastanin 首先,非常感谢您提供这样一个好的库。我想知道是否有任何选项可以将表格打印到终端的整个宽度? - Validus Oculus
1
你好sastanin,我想在这里留下一句话来感谢你提供的非常方便的软件包。它运行得非常好,让我省去了编写自己代码的麻烦。非常感谢你的分享! - Valentin B.
1
您的功能列表过于保守。尝试了 ANSI 转义字符,完美地运行了。感谢您! - Red Pill
通过 tabulatepandas 1.0.0 及更高版本可以直接以 Markdown 格式打印数据框,这对于在 pandas 数据框中管理数据且需要直接打印输出的情况非常方便。 - Edward

40

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

对我很方便,也可以成为您的起点:

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])

示例输出:

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

例子

>>> 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

@MattH,你能举个例子展示一下这个函数的用法吗? - theAlse
1
@MattH 谢谢,但是大数似乎会立即崩溃。TypeError: 'int'类型的对象没有len()。 - theAlse
@Alborz:我发布了这个作为其他人的起点,如果你想要的话,可以自定义它来处理你的数据类型。不过,根据出现错误的那一行,你可能没有按照预期调用函数。 - MattH
1
@theAlse,我通过在lens.append行中使用len(str(max(...)))修复了您发现的错误。 现在,如果列中的数字比列标题宽,我们仍然没问题。 另外,MattH - 很聪明地使用了max()的“key”参数! - nealmcb

20

由于某种原因,当我在谷歌搜索中包含“docutils”时,我偶然发现了texttable,它似乎是我要找的东西。


2
不错。缺少自动列宽检测;请使用:http://pastebin.com/SAsPJUxM - Kos

14

我也写了自己的解决方案,并尝试保持简单。

https://github.com/Robpol86/terminaltables

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] += '\nnewline'
print table.table
+--------------+--------------+
| Heading1     |     Heading2 |
+--------------+--------------+
| row1 column1 | row1 column2 |
|              |      newline |
+--------------+--------------+
| row2 column1 | row2 column2 |
+--------------+--------------+

这个项目似乎已经死了。 - a0s

13

我刚刚发布了termtables用于此目的。例如,这个。

import termtables as tt

tt.print(
    [[1, 2, 3], [613.23236243236, 613.23236243236, 613.23236243236]],
    header=["a", "bb", "ccc"],
    style=tt.styles.ascii_thin_double,
    padding=(0, 1),
    alignment="lcr"
)

帮助你

+-----------------+-----------------+-----------------+
| a               |       bb        |             ccc |
+=================+=================+=================+
| 1               |        2        |               3 |
+-----------------+-----------------+-----------------+
| 613.23236243236 | 613.23236243236 | 613.23236243236 |
+-----------------+-----------------+-----------------+

默认情况下,该表格使用Unicode绘图符号进行呈现。

┌─────────────────┬─────────────────┬─────────────────┐
│ a               │       bb        │             ccc │
╞═════════════════╪═════════════════╪═════════════════╡
│ 123 │
├─────────────────┼─────────────────┼─────────────────┤
│ 613.23236243236613.23236243236613.23236243236 │
└─────────────────┴─────────────────┴─────────────────┘

termtables非常可配置;请查看测试以获得更多示例。


我希望你可以设置最大列以显示,并让库处理换行逻辑。 - Kang Min Yoo

8

如果你想要一个拥有列和行跨度的表格,那么可以尝试使用我的库dashtable

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\nspan rows.", "- Cells\n- contain\n- 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))

这将输出:

+----------+------------+----------+----------+
| 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            |
+----------+------------+---------------------+

ERROR: Spans must be a list of lists - c z

7

使用w3m制作的版本,旨在处理MattH版本接受的类型:

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])

结果为:

┌───────┬─┐
│ first │1│
├───────┼─┤
│second │2│
├───────┼─┤
│ third │3│
└───────┴─┘
┌─────┬───────┬─────┐
│first│second │third│
├─────┼───────┼─────┤
│1    │2      │3    │
├─────┼───────┼─────┤
│4    │5      │6    │
└─────┴───────┴─────┘

7
你可以尝试使用BeautifulTable。它能够实现你想要的功能。以下是来自它的文档的示例。
>>> from beautifultable import BeautifulTable
>>> table = BeautifulTable()
>>> table.columns.header = ["name", "rank", "gender"]
>>> table.rows.append(["Jacob", 1, "boy"])
>>> table.rows.append(["Isabella", 1, "girl"])
>>> table.rows.append(["Ethan", 2, "boy"])
>>> table.rows.append(["Sophia", 2, "girl"])
>>> table.rows.append(["Michael", 3, "boy"])
>>> print(table)
+----------+------+--------+
|   name   | rank | gender |
+----------+------+--------+
|  Jacob   |  1   |  boy   |
+----------+------+--------+
| Isabella |  1   |  girl  |
+----------+------+--------+
|  Ethan   |  2   |  boy   |
+----------+------+--------+
|  Sophia  |  2   |  girl  |
+----------+------+--------+
| Michael  |  3   |  boy   |
+----------+------+--------+

warnings.warn(message, FutureWarning)``` - evandrix
warnings.warn(message, FutureWarning)``` - evandrix

3
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('\n' + rc_separator + '\n')

        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('\n' + rc_separator + '\n')
                else:
                    stdout.flush()
                    stdout.write('\n')

                newline_indicator = 0

            stdout.write(line)
            newline_indicator += 1

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

例子:

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

create_table(table)

输出:

uid   | name       | 
------+------------+-
0     | Jon        | 
1     | Doe        | 
2     | Lemma      | 
3     | Hemma      | 
------+------------+-

3
你可以通过添加一些解释来改善你的仅包含代码的答案,以使其更易于理解,但不要改变原始意思。 - Yunnosch

2
我知道这个问题有点老了,但以下是我的尝试: https://gist.github.com/lonetwin/4721748 在我看来,它更易读(虽然它不像@MattH的解决方案那样区分单行/多行,也不使用NamedTuples)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接