¿Cómo puedo imprimir tablas ASCII con Python? [cerrado]

81

Estoy buscando una forma de imprimir tablas bonitas como esta:

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

Encontré la biblioteca asciitable pero no hace los bordes, etc. No necesito ningún formato complejo de elementos de datos, son solo cadenas. Lo necesito para ajustar automáticamente el tamaño de las columnas.

¿Existen otras bibliotecas o métodos, o necesito dedicar unos minutos a escribir los míos?

kdt
fuente
¿Por qué no utilizar docutils para hacer esto por usted?
S.Lott
¿Cómo llamas a una mesa? ¿Cómo se organizan los datos en una tabla? ¿Son value1, value2, value3, value4 ... los valores sucesivos en una lista? Creo que fomat () es suficiente para obtener una pantalla tan simple, no es necesario aprender durante mucho tiempo un tutorial que explica cómo ganar tiempo usando una biblioteca
eyquem
2
@korona: No, no estaba haciendo una sugerencia. Estaba haciendo una pregunta. No tengo ni idea de lo que @kdt sabe o no sabe. En lugar de asumir, me siento obligado a preguntar.
S.Lott
5
Me sonó como si de hecho estuvieras asumiendo que él sabe sobre docutils. ¿Quizás no?
korona
2
@ S.Lott He visto docutils, y aunque, por supuesto, es excelente para convertir texto a html, latex, etc., no veo una forma de generar tablas de texto agradables, con columnas que se alinean y se ven bonitas fuentes de ancho fijo. ¿Entendiste mal el objetivo de kdt o me estoy perdiendo algo?
nealmcb

Respuestas:

71

He leído esta pregunta hace mucho tiempo, y de terminar de escribir mi propia bastante-impresora para tablas: tabulate.

Mi caso de uso es:

  • Quiero una sola línea la mayor parte del tiempo
  • que es lo suficientemente inteligente como para determinar el mejor formato para mí
  • y puede generar diferentes formatos de texto sin formato

Dado su ejemplo, grides probablemente el formato de salida más similar:

from tabulate import tabulate
print tabulate([["value1", "value2"], ["value3", "value4"]], ["column 1", "column 2"], tablefmt="grid")
+------------+------------+
| column 1   | column 2   |
+============+============+
| value1     | value2     |
+------------+------------+
| value3     | value4     |
+------------+------------+

Otros formatos soportados son plain(sin líneas), simple(tablas simples de Pandoc), pipe(como tablas en PHP Markdown Extra), orgtbl(como tablas en el modo org de Emacs), rst(como tablas simples en reStructuredText). gridy orgtblson fácilmente editables en Emacs.

En cuanto al rendimiento, tabulatees un poco más lento asciitable, pero mucho más rápido que PrettyTabley texttable.

PD: También soy un gran fanático de alinear números con una columna decimal . Entonces, esta es la alineación predeterminada para los números si hay alguno (reemplazable).

Sastanin
fuente
4
¡Simplemente necesitaba una solución de tabulación y tuve la suerte de encontrar su biblioteca! Funciona como un encanto: D En caso de que estés escuchando, solo quería
darte las
2
Sí! Estoy escuchando. Gracias por las amables palabras. Es muy agradable recibir comentarios positivos.
Sastanin
1
Hola, @sastanin Antes que nada, muchas gracias por una biblioteca tan agradable. ¿Puedo saber si hay alguna opción para imprimir la tabla para abarcar todo el ancho de la terminal?
Validus Oculus
1
Hola Sastanin, solo quería dejar unas palabras aquí para agradecerle por este paquete tan útil. Funciona a las mil maravillas y me ahorró la molestia de escribir el mío. Muchas gracias por compartir !
Valentin B.
1
Su lista de funciones es insuficiente. Intenté ansi escapó cosas, funciona perfecto. ¡Gracias por esto!
Red Pill
37

Aquí hay una pequeña función rápida y sucia que escribí para mostrar los resultados de las consultas SQL que solo puedo hacer sobre una API SOAP. Espera una entrada de una secuencia de una o más namedtuplesfilas de tabla. Si solo hay un registro, lo imprime de manera diferente.

Es útil para mí y podría ser un punto de partida para ti:

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

Salida de muestra:

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

Ejemplo

>>> 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
fuente
@MattH, ¿puedes mostrar el uso de esta función con un ejemplo?
theAlse
1
@MattH gracias, pero un gran número parece fallar de inmediato. TypeError: el objeto de tipo 'int' no tiene len ().
theAlse
@Alborz: publiqué esto como un punto de partida para otros, personalícelo para tratar con sus tipos de datos si lo desea. Aunque dependiendo de la línea de la que provenga el error, es posible que no esté llamando a la función como se
esperaba
1
@theAlse Corregí el error que identificaste, len(str(max(...)))colocándolo en la línea lens.append. Entonces, si un número en una columna es más ancho que el encabezado de la columna, todavía estamos bien. Por cierto, MattH - ¡lindo uso del argumento "clave" para max ()!
nealmcb
19

Por alguna razón, cuando incluí 'docutils' en mis búsquedas de Google, me encontré con texttable , que parece ser lo que estoy buscando.

kdt
fuente
2
Buena esa. Carece de detección automática de ancho de columna; uso: pastebin.com/SAsPJUxM
Kos
12

Yo también escribí mi propia solución a esto. Traté de hacerlo simple.

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 |
+--------------+--------------+
Robpol86
fuente
9

Acabo de publicar tablas de términos para este propósito. Por ejemplo, este

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

te atrapa

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

De forma predeterminada, la tabla se representa con caracteres de dibujo de cuadro Unicode ,

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

termtables son muy configurables; consulte las pruebas para obtener más ejemplos.

Nico Schlömer
fuente
Ojalá pudieras establecer la columna máxima para mostrar y dejar que la biblioteca maneje la lógica de envoltura.
Kang Min Yoo
7

Puedes probar BeautifulTable . Hace lo que quieres hacer. Aquí hay un ejemplo de su documentación.

>>> 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   |
+----------+------+--------+
Priyam Singh
fuente
/usr/local/lib/python3.8/site-packages/beautifultable/utils.py:113: FutureWarning: 'BeautifulTable.column_headers' has been deprecated in 'v1.0.0' and will be removed in 'v1.2.0'. Use 'BTColumnCollection.header' instead. warnings.warn(message, FutureWarning)
evandrix
/usr/local/lib/python3.8/site-packages/beautifultable/utils.py:113: FutureWarning: 'BeautifulTable.append_row' has been deprecated in 'v1.0.0' and will be removed in 'v1.2.0'. Use 'BTRowCollection.append' instead. warnings.warn(message, FutureWarning)
evandrix
6

Versión que usa w3m diseñada para manejar los tipos que acepta la versión de 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])

resulta en:

┌───────┬─┐
│ first │1│
├───────┼─┤
│second │2│
├───────┼─┤
│ third │3│
└───────┴─┘
┌─────┬───────┬─────┐
│first│second │third│
├─────┼───────┼─────┤
│123    │
├─────┼───────┼─────┤
│456    │
└─────┴───────┴─────┘
Janus Troelsen
fuente
5

Si desea una tabla con intervalos de columnas y filas, pruebe mi tablero de biblioteca

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

Qué salidas:

+----------+------------+----------+----------+
| 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            |
+----------+------------+---------------------+
dmodo
fuente
ERROR: Spans must be a list of lists
cz
2

Sé que la pregunta es un poco vieja, pero aquí está mi intento de esto:

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

Es un poco más legible en mi humilde opinión (aunque no diferencia entre filas únicas / múltiples como lo hacen las soluciones de @ MattH, ni usa NamedTuples).

solitario
fuente
2

Utilizo esta pequeña función de utilidad.

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) + '\n'
    output += '|' + ''.join([h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '\n'
    output += '-' * (sum(max_len) + 1) + '\n'
    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)]) + '\n'
    output += '-' * (sum(max_len) + 1) + '\n'
    return output

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

salida

-----------------
|header 1|header 2|
-----------------
|1       |2       |
|3       |4       |
-----------------
Thavan
fuente
1
Está agregando un espacio entre cada columna output += '|' + ''.join([h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '\n' en las líneas de separación, pero no en ellas. Puede extender esa fila de -s con algo tan simple como output = '-' * (sum(max_len) + 1 + len(header)) + '\n'
ochawkeye
1

Esta es mi solución:

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 "\n".join([header, sep] + rows)
Luke Taylor
fuente
1
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()

Ejemplo:

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

create_table(table)

Salida:

uid   | name       | 
------+------------+-
0     | Jon        | 
1     | Doe        | 
2     | Lemma      | 
3     | Hemma      | 
------+------------+-
Lepstr
fuente
2
Podría mejorar su respuesta de solo código ampliándola con alguna explicación.
Yunnosch
0

Esto se puede hacer solo con módulos incorporados de manera bastante compacta usando listas y cadenas de comprensión. Acepta una lista de diccionarios todos del mismo formato ...

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 += "\n"

    outmsg = lenstr.format(*dictlist[0].keys())
    outmsg += "-" * (sum(lengths) + 3*len(lengths))
    outmsg += "\n"
    outmsg += "".join(
        lenstr.format(*v) for v in [ item.values() for item in dictlist ]
    )
    return outmsg
MattK
fuente