Obtener la longitud de cadena correcta en Python para cadenas con códigos de color ANSI

StackOverflow https://stackoverflow.com/questions/2186919

  •  25-09-2019
  •  | 
  •  

Pregunta

Tengo un código Python que imprimirá automáticamente un conjunto de datos en un formato de columna agradable, incluida la colocación de las secuencias de escape ASCII apropiadas para colorear varias partes de los datos para facilitar la lectura.

Eventualmente termino con cada línea representada como una lista, con cada elemento como una columna con espacio para que las mismas columnas en cada línea tengan siempre la misma longitud.Desafortunadamente, cuando voy a imprimir esto, no todas las columnas están alineadas.Sospecho que esto tiene que ver con las secuencias de escape ASCII, porque el len La función no parece reconocer estos:

>>> a = '\x1b[1m0.0\x1b[0m'
>>> len(a)
11
>>> print a
0.0

Y así, si bien cada columna tiene la misma longitud según len, en realidad no tienen la misma longitud cuando se imprimen en la pantalla.

¿Hay alguna manera (salvo hacer algo de piratería con expresiones regulares que prefiero no hacer) de tomar la cadena de escape y averiguar cuál es la longitud impresa para poder espaciar adecuadamente?¿Quizás alguna forma de simplemente "imprimirlo" nuevamente en una cuerda y examinar su longitud?

¿Fue útil?

Solución

El pyparsing wiki incluye esta expresión útil para la coincidencia en secuencias de escape ANSI:

ESC = Literal('\x1b')
integer = Word(nums)
escapeSeq = Combine(ESC + '[' + Optional(delimitedList(integer,';')) + 
                oneOf(list(alphas)))

Aquí está cómo hacer esto en un escape de secuencia stripper:

from pyparsing import *

ESC = Literal('\x1b')
integer = Word(nums)
escapeSeq = Combine(ESC + '[' + Optional(delimitedList(integer,';')) + 
                oneOf(list(alphas)))

nonAnsiString = lambda s : Suppress(escapeSeq).transformString(s)

unColorString = nonAnsiString('\x1b[1m0.0\x1b[0m')
print unColorString, len(unColorString)

impresiones:

0.0 3

Otros consejos

No entiendo dos cosas.

(1) Es el código, bajo su control. Si desea añadir secuencias de escape a sus datos y luego despojarlos de nuevo para que pueda calcular la longitud de los datos ?? Parece mucho más simple para calcular el relleno antes de añadir las secuencias de escape. ¿Qué me falta?

Vamos a presumir que ninguna de las secuencias de escape cambie la posición del cursor. Si lo hacen, la respuesta aceptada actualmente no funciona de todos modos.

a suponer que usted tiene los datos de cadena para cada columna Sea (antes de añadir secuencias de escape) en una lista llamada string_data y los anchos de columna predeterminado se encuentran en una lista llamada width. Intentar algo como esto:

temp = []
for colx, text in enumerate(string_data):
    npad = width[colx] - len(text) # calculate padding size
    assert npad >= 0
    enhanced = fancy_text(text, colx, etc, whatever) # add escape sequences
    temp.append(enhanced + " " * npad)
sys.stdout.write("".join(temp))

Actualizar-1

Tras el comentario de OP:

  

La razón quiero despojar a cabo y calcular la longitud después de la   cadena contiene los códigos de color se debe a que todos los datos se construyen   mediante programación. Tengo un montón de métodos colorize y estoy construyendo   seguridad de los datos algo como esto: str = "%s/%s/%s" % (GREEN(data1), BLUE(data2), RED(data3)) Sería muy difícil para colorear el   texto después del hecho.

Si los datos se construye con piezas cada una con su propio formato, todavía puede calcular la longitud mostrada y la almohadilla en su caso. He aquí una función que lo hace por el contenido de una celda:

BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(40, 48)
BOLD = 1

def render_and_pad(reqd_width, components, sep="/"):
    temp = []
    actual_width = 0
    for fmt_code, text in components:
        actual_width += len(text)
        strg = "\x1b[%dm%s\x1b[m" % (fmt_code, text)
        temp.append(strg)
    if temp:
        actual_width += len(temp) - 1
    npad = reqd_width - actual_width
    assert npad >= 0
    return sep.join(temp) + " " * npad

print repr(
    render_and_pad(20, zip([BOLD, GREEN, YELLOW], ["foo", "bar", "zot"]))
    )

Si usted piensa que la llamada está sobrecargado por puntuacion, usted podría hacer algo como:

BOLD = lambda s: (1, s)
BLACK = lambda s: (40, s)
# etc
def render_and_pad(reqd_width, sep, *components):
    # etc

x = render_and_pad(20, '/', BOLD(data1), GREEN(data2), YELLOW(data3))

(2) No entiendo por qué no se desea utilizar el kit de expresiones regulares de Python-provisto con-? No "hackery" (para cualquier posible significado de "hackery" que yo sepa) está involucrado:

>>> import re
>>> test = "1\x1b[a2\x1b[42b3\x1b[98;99c4\x1b[77;66;55d5"
>>> expected = "12345"
>>> # regex = re.compile(r"\x1b\[[;\d]*[A-Za-z]")
... regex = re.compile(r"""
...     \x1b     # literal ESC
...     \[       # literal [
...     [;\d]*   # zero or more digits or semicolons
...     [A-Za-z] # a letter
...     """, re.VERBOSE)
>>> print regex.findall(test)
['\x1b[a', '\x1b[42b', '\x1b[98;99c', '\x1b[77;66;55d']
>>> actual = regex.sub("", test)
>>> print repr(actual)
'12345'
>>> assert actual == expected
>>>

Actualizar-2

Tras el comentario de OP:

  

Yo prefiero la respuesta de Pablo, ya que es más concisa

Más concisa de lo que? No es suficiente la siguiente expresión regular solución concisa para usted?

# === setup ===
import re
strip_ANSI_escape_sequences_sub = re.compile(r"""
    \x1b     # literal ESC
    \[       # literal [
    [;\d]*   # zero or more digits or semicolons
    [A-Za-z] # a letter
    """, re.VERBOSE).sub
def strip_ANSI_escape_sequences(s):
    return strip_ANSI_escape_sequences_sub("", s)

# === usage ===
raw_data = strip_ANSI_escape_sequences(formatted_data)

[Por encima de código corrige después de @ Nick Perkins señaló que no funcionó]

Mirando en ANSI_escape_code, la secuencia en su ejemplo esSeleccionar representación gráfica (probablemente atrevido).

Intente controlar la posición de la columna con el Posición del cursor ( CSI n ; m H) secuencia.De esta manera, el ancho del texto anterior no afecta la posición actual de la columna y no hay necesidad de preocuparse por el ancho de las cadenas.

Una mejor opción, si apunta a Unix, es usar el objetos-ventana del módulo de curses.Por ejemplo, se puede colocar una cadena en la pantalla con:

window.addnstr([y, x], str, n[, attr])

Pinte como máximo n caracteres de la cadena str en (y, x) con atributos attr, sobrescribiendo cualquier cosa que estuviera previamente en la pantalla.

Si acaba de añadir color a algunas células, puede agregar 9 a la anchura de la celda esperado (5 caracteres ocultos para encender el color, 4 para apagarlo), por ejemplo,

import colorama # handle ANSI codes on Windows
colorama.init()

RED   = '\033[91m' # 5 chars
GREEN = '\033[92m' # 5 chars
RESET = '\033[0m'  # 4 chars

def red(s):
    "color a string red"
    return RED + s + RESET
def green(s):
    "color a string green"
    return GREEN + s + RESET
def redgreen(v, fmt, sign=1):
    "color a value v red or green, depending on sign of value"
    s = fmt.format(v)
    return red(s) if (v*sign)<0 else green(s)

header_format = "{:9} {:5}  {:>8}  {:10}  {:10}  {:9}  {:>8}"
row_format =    "{:9} {:5}  {:8.2f}  {:>19}  {:>19}  {:>18}  {:>17}"
print(header_format.format("Type","Trial","Epsilon","Avg Reward","Violations", "Accidents","Status"))

# some dummy data
testing = True
ntrials = 3
nsteps = 1
reward = 0.95
actions = [0,1,0,0,1]
d = {'success': True}
epsilon = 0.1

for trial in range(ntrials):
    trial_type = "Testing " if testing else "Training"
    avg_reward = redgreen(float(reward)/nsteps, "{:.2f}")
    violations = redgreen(actions[1] + actions[2], "{:d}", -1)
    accidents = redgreen(actions[3] + actions[4], "{:d}", -1)
    status = green("On time") if d['success'] else red("Late")
    print(row_format.format(trial_type, trial, epsilon, avg_reward, violations, accidents, status))

Dar

pantalla

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top