关于string:在python中创建漂亮的列输出

Create nice column output in python

我试图在python中创建一个很好的列列表,用于我创建的命令行管理工具。

基本上,我想要一个列表,比如:

1
[['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]

变成:

1
2
3
a            b            c
aaaaaaaaaa   b            c
a            bbbbbbbbbb   c

使用简单的制表符在这里不起作用,因为我不知道每行中最长的数据。

这与Linux中的"column-t"行为相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ echo -e"a b c
aaaaaaaaaa b c
a bbbbbbbbbb c"

a b c
aaaaaaaaaa b c
a bbbbbbbbbb c

$ echo -e"a b c
aaaaaaaaaa b c
a bbbbbbbbbb c"
| column -t
a           b           c
aaaaaaaaaa  b           c
a           bbbbbbbbbb  c

我到处寻找各种各样的python库来完成这项工作,但是找不到任何有用的。


由于python 2.6+,您可以使用以下格式字符串将列设置为至少20个字符,并将文本右对齐。

1
2
3
4
5
6
7
table_data = [
    ['a', 'b', 'c'],
    ['aaaaaaaaaa', 'b', 'c'],
    ['a', 'bbbbbbbbbb', 'c']
]
for row in table_data:
    print("{: >20} {: >20} {: >20}".format(*row))

输出:

1
2
3
               a                    b                    c
      aaaaaaaaaa                    b                    c
               a           bbbbbbbbbb                    c


1
2
3
4
5
6
7
8
9
data = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]

col_width = max(len(word) for row in data for word in row) + 2  # padding
for row in data:
    print"".join(word.ljust(col_width) for word in row)

a            b            c            
aaaaaaaaaa   b            c            
a            bbbbbbbbbb   c

这样做的目的是计算最长的数据条目以确定列宽,然后在打印出每一列时使用.ljust()添加必要的填充。


我来这里的时候也有同样的要求,但是@lvc和@preet的答案似乎更符合column -t在这些列中产生的宽度不同:

1
2
3
4
5
>>> rows =  [   ['a',           'b',            'c',    'd']
...         ,   ['aaaaaaaaaa',  'b',            'c',    'd']
...         ,   ['a',           'bbbbbbbbbb',   'c',    'd']
...         ]
...
1
2
3
4
5
6
7
>>> widths = [max(map(len, col)) for col in zip(*rows)]
>>> for row in rows:
...     print" ".join((val.ljust(width) for val, width in zip(row, widths)))
...
a           b           c  d
aaaaaaaaaa  b           c  d
a           bbbbbbbbbb  c  d


你必须通过两次传球:

  • 获取每列的最大宽度。
  • 使用str.ljust()str.rjust()从第一遍开始的最大宽度知识格式化列。

  • 得到更豪华的桌子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ---------------------------------------------------
    | First Name | Last Name        | Age | Position  |
    ---------------------------------------------------
    | John       | Smith            | 24  | Software  |
    |            |                  |     | Engineer  |
    ---------------------------------------------------
    | Mary       | Brohowski        | 23  | Sales     |
    |            |                  |     | Manager   |
    ---------------------------------------------------
    | Aristidis  | Papageorgopoulos | 28  | Senior    |
    |            |                  |     | Reseacher |
    ---------------------------------------------------

    您可以使用这个python配方:

    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    '''
    From http://code.activestate.com/recipes/267662-table-indentation/
    PSF License
    '''

    import cStringIO,operator

    def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left',
               separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x):
       """Indents a table by column.
           - rows: A sequence of sequences of items, one sequence per row.
           - hasHeader: True if the first row consists of the columns' names.
           - headerChar: Character to be used for the row separator line
             (if hasHeader==True or separateRows==True).
           - delim: The column delimiter.
           - justify: Determines how are data justified in their column.
             Valid values are 'left','right' and 'center'.
           - separateRows: True if rows are to be separated by a line
             of 'headerChar's.
           - prefix: A string prepended to each printed row.
           - postfix: A string appended to each printed row.
           - wrapfunc: A function f(text) for wrapping text; each element in
             the table is first wrapped by this function."""

        # closure for breaking logical rows to physical, using wrapfunc
        def rowWrapper(row):
            newRows = [wrapfunc(item).split('
    '
    ) for item in row]
            return [[substr or '' for substr in item] for item in map(None,*newRows)]
        # break each logical row into one or more physical ones
        logicalRows = [rowWrapper(row) for row in rows]
        # columns of physical rows
        columns = map(None,*reduce(operator.add,logicalRows))
        # get the maximum of each column by the string length of its items
        maxWidths = [max([len(str(item)) for item in column]) for column in columns]
        rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \
                                     len(delim)*(len(maxWidths)-1))
        # select the appropriate justify method
        justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()]
        output=cStringIO.StringIO()
        if separateRows: print >> output, rowSeparator
        for physicalRows in logicalRows:
            for row in physicalRows:
                print >> output, \
                    prefix \
                    + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \
                    + postfix
            if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False
        return output.getvalue()

    # written by Mike Brown
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
    def wrap_onspace(text, width):
       """
        A word-wrap function that preserves existing line breaks
        and most spaces in the text. Expects that existing line
        breaks are posix newlines (
    ).
       """

        return reduce(lambda line, word, width=width: '%s%s%s' %
                      (line,
                       '
    '
    [(len(line[line.rfind('
    '
    )+1:])
                             + len(word.split('
    '
    ,1)[0]
                                  ) >= width)],
                       word),
                      text.split(' ')
                     )

    import re
    def wrap_onspace_strict(text, width):
       """Similar to wrap_onspace, but enforces the width constraint:
           words longer than width are split."""

        wordRegex = re.compile(r'\S{'+str(width)+r',}')
        return wrap_onspace(wordRegex.sub(lambda m: wrap_always(m.group(),width),text),width)

    import math
    def wrap_always(text, width):
       """A simple word-wrap function that wraps text on exactly width characters.
           It doesn't split the text in words."""

        return '
    '
    .join([ text[width*i:width*(i+1)] \
                           for i in xrange(int(math.ceil(1.*len(text)/width))) ])

    if __name__ == '__main__':
        labels = ('First Name', 'Last Name', 'Age', 'Position')
        data = \
        '''John,Smith,24,Software Engineer
           Mary,Brohowski,23,Sales Manager
           Aristidis,Papageorgopoulos,28,Senior Reseacher'''

        rows = [row.strip().split(',')  for row in data.splitlines()]

        print 'Without wrapping function
    '

        print indent([labels]+rows, hasHeader=True)
        # test indent with different wrapping functions
        width = 10
        for wrapper in (wrap_always,wrap_onspace,wrap_onspace_strict):
            print 'Wrapping function: %s(x,width=%d)
    '
    % (wrapper.__name__,width)
            print indent([labels]+rows, hasHeader=True, separateRows=True,
                         prefix='| ', postfix=' |',
                         wrapfunc=lambda x: wrapper(x,width))

        # output:
        #
        #Without wrapping function
        #
        #First Name | Last Name        | Age | Position        
        #-------------------------------------------------------
        #John       | Smith            | 24  | Software Engineer
        #Mary       | Brohowski        | 23  | Sales Manager    
        #Aristidis  | Papageorgopoulos | 28  | Senior Reseacher
        #
        #Wrapping function: wrap_always(x,width=10)
        #
        #----------------------------------------------
        #| First Name | Last Name  | Age | Position   |
        #----------------------------------------------
        #| John       | Smith      | 24  | Software E |
        #|            |            |     | ngineer    |
        #----------------------------------------------
        #| Mary       | Brohowski  | 23  | Sales Mana |
        #|            |            |     | ger        |
        #----------------------------------------------
        #| Aristidis  | Papageorgo | 28  | Senior Res |
        #|            | poulos     |     | eacher     |
        #----------------------------------------------
        #
        #Wrapping function: wrap_onspace(x,width=10)
        #
        #---------------------------------------------------
        #| First Name | Last Name        | Age | Position  |
        #---------------------------------------------------
        #| John       | Smith            | 24  | Software  |
        #|            |                  |     | Engineer  |
        #---------------------------------------------------
        #| Mary       | Brohowski        | 23  | Sales     |
        #|            |                  |     | Manager   |
        #---------------------------------------------------
        #| Aristidis  | Papageorgopoulos | 28  | Senior    |
        #|            |                  |     | Reseacher |
        #---------------------------------------------------
        #
        #Wrapping function: wrap_onspace_strict(x,width=10)
        #
        #---------------------------------------------
        #| First Name | Last Name  | Age | Position  |
        #---------------------------------------------
        #| John       | Smith      | 24  | Software  |
        #|            |            |     | Engineer  |
        #---------------------------------------------
        #| Mary       | Brohowski  | 23  | Sales     |
        #|            |            |     | Manager   |
        #---------------------------------------------
        #| Aristidis  | Papageorgo | 28  | Senior    |
        #|            | poulos     |     | Reseacher |
        #---------------------------------------------

    python配方页面包含一些改进。


    像这样转换列是zip的一项工作:

    1
    2
    3
    >>> a = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
    >>> list(zip(*a))
    [('a', 'aaaaaaaaaa', 'a'), ('b', 'b', 'bbbbbbbbbb'), ('c', 'c', 'c')]

    要找到每列所需的长度,可以使用max

    1
    2
    3
    >>> trans_a = zip(*a)
    >>> [max(len(c) for c in b) for b in trans_a]
    [10, 10, 1]

    它可以使用适当的填充来构造要传递给print的字符串:

    1
    2
    3
    4
    >>> col_lenghts = [max(len(c) for c in b) for b in trans_a]
    >>> padding = ' ' # You might want more
    >>> padding.join(s.ljust(l) for s,l in zip(a[0], col_lenghts))
    'a          b          c'


    基于pandas的解决方案,创建数据帧:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pandas as pd
    l = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
    df = pd.DataFrame(l)

    print(df)
                0           1  2
    0           a           b  c
    1  aaaaaaaaaa           b  c
    2           a  bbbbbbbbbb  c

    要删除索引和标题值以创建所需的输出,可以使用to_string方法:

    1
    2
    3
    4
    5
    6
    result = df.to_string(index=False, header=False)

    print(result)
              a           b  c
     aaaaaaaaaa           b  c
              a  bbbbbbbbbb  c

    这对派对来说有点晚了,对于我写的一个包来说是一个无耻的插件,但是你也可以查看这个专栏包。

    它获取输入列表和头列表,并输出一个表格式的字符串。这段代码创建了一个Docker风格的表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from columnar import columnar

    headers = ['name', 'id', 'host', 'notes']

    data = [
        ['busybox', 'c3c37d5d-38d2-409f-8d02-600fd9d51239', 'linuxnode-1-292735', 'Test server.'],
        ['alpine-python', '6bb77855-0fda-45a9-b553-e19e1a795f1e', 'linuxnode-2-249253', 'The one that runs python.'],
        ['redis', 'afb648ba-ac97-4fb2-8953-9a5b5f39663e', 'linuxnode-3-3416918', 'For queues and stuff.'],
        ['app-server', 'b866cd0f-bf80-40c7-84e3-c40891ec68f9', 'linuxnode-4-295918', 'A popular destination.'],
        ['nginx', '76fea0f0-aa53-4911-b7e4-fae28c2e469b', 'linuxnode-5-292735', 'Traffic Cop'],
    ]

    table = columnar(data, headers, no_borders=True)
    print(table)

    Table Displaying No-border Style

    或者你可以对颜色和边框有点着迷。Table Displaying Spring Classics

    要了解有关列大小调整算法的更多信息并查看API的其余部分,您可以查看上面的链接或查看列Github repo


    这是肖恩·钦回答的一个变化。每列的宽度是固定的,不是所有列的宽度。第一行下面和列之间也有边框。(IContract库用于执行合同。)

    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
    @icontract.pre(
        lambda table: not table or all(len(row) == len(table[0]) for row in table))
    @icontract.post(lambda table, result: result =="" if not table else True)
    @icontract.post(lambda result: not result.endswith("
    "
    ))
    def format_table(table: List[List[str]]) -> str:
       """
        Format the table as equal-spaced columns.

        :param table: rows of cells
        :return: table as string
       """

        cols = len(table[0])

        col_widths = [max(len(row[i]) for row in table) for i in range(cols)]

        lines = []  # type: List[str]
        for i, row in enumerate(table):
            parts = []  # type: List[str]

            for cell, width in zip(row, col_widths):
                parts.append(cell.ljust(width))

            line =" |".join(parts)
            lines.append(line)

            if i == 0:
                border = []  # type: List[str]

                for width in col_widths:
                    border.append("-" * width)

                lines.append("-+-".join(border))

        result ="
    "
    .join(lines)

        return result

    下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    >>> table = [['column 0', 'another column 1'], ['00', '01'], ['10', '11']]
    >>> result = packagery._format_table(table=table)
    >>> print(result)
    column 0 | another column 1
    ---------+-----------------
    00       | 01              
    10       | 11

    我发现这个答案非常有用而且优雅,最初是从这里开始的:

    1
    2
    3
    4
    matrix = [["A","B"], ["C","D"]]

    print('
    '
    .join(['\t'.join([str(cell) for cell in row]) for row in matrix]))

    产量

    1
    2
    A   B
    C   D

    我知道这个问题很古老,但我不理解安塔克的答案,也不想使用图书馆,所以我推出了自己的解决方案。

    解决方案假定记录是二维数组,记录的长度都相同,字段都是字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def stringifyRecords(records):
        column_widths = [0] * len(records[0])
        for record in records:
            for i, field in enumerate(record):
                width = len(field)
                if width > column_widths[i]: column_widths[i] = width

        s =""
        for record in records:
            for column_width, field in zip(column_widths, record):
                s += field.ljust(column_width+1)
            s +="
    "


        return s