Obtendo o comprimento correto da corda em Python para cordas com códigos de cores ANSI

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

  •  25-09-2019
  •  | 
  •  

Pergunta

Eu tenho algum código Python que imprimirá automaticamente um conjunto de dados em um bom formato de coluna, incluindo a colocação das sequências de escape ASCII apropriadas para colorir várias peças dos dados para a legibilidade.

Acabei acabando com cada linha sendo representada como uma lista, com cada item sendo uma coluna que está com espaço para que as mesmas colunas em cada linha sejam sempre o mesmo comprimento. Infelizmente, quando eu realmente vou imprimir isso, nem todas as colunas se alinham. Eu suspeito que isso tem a ver com as sequências de escape ASCII - porque o len A função não parece reconhecer isso:

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

E assim, enquanto cada coluna é o mesmo comprimento de acordo com len, eles não são realmente o mesmo comprimento quando impressos na tela.

Existe alguma maneira (exceto para fazer algumas hackery com expressões regulares que eu prefiro não fazer) para pegar a corda escapada e descobrir qual é o comprimento impresso para que eu possa fazer o Space Pad adequadamente? Talvez uma maneira de apenas "imprimir" de volta para amarrar e examinar a duração disso?

Foi útil?

Solução

O wiki de Pyparsing inclui isso expressão útil Para corresponder às sequências de escape da ANSI:

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

Veja como transformar isso em um titripper de sequência de escape:

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)

impressões:

0.0 3

Outras dicas

Eu não entendo duas coisas.

(1) É o seu código, sob seu controle. Você deseja adicionar sequências de fuga aos seus dados e depois retirá -los novamente para que você possa calcular o comprimento de seus dados? Parece muito mais simples calcular o preenchimento antes da Adicionando as sequências de fuga. o que estou perdendo?

Vamos presumir que nenhuma das seqüências de fuga muda a posição do cursor. Se o fizerem, a resposta atualmente aceita não funcionará de qualquer maneira.

Vamos supor que você tenha os dados da string para cada coluna (antes de adicionar sequências de fuga) em uma lista nomeada string_data e as larguras pré-determinadas da coluna estão em uma lista nomeada width. Tente algo assim:

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

Atualização 1

Após o comentário do OP:

A razão pela qual eu quero retirá -los e calcular o comprimento após a string contém os códigos de cores é porque todos os dados são criados programaticamente. Eu tenho vários métodos colorizados e estou construindo os dados algo assim: str = "%s/%s/%s" % (GREEN(data1), BLUE(data2), RED(data3)) Seria muito difícil colorir o texto após o fato.

Se os dados forem construídos de peças, cada uma com sua própria formatação, você ainda poderá calcular o comprimento e o bloco exibidos, conforme apropriado. Aqui está uma função que faz isso para o conteúdo de uma célula:

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

Se você acha que a ligação está sobrecarregada pela pontuação, você pode fazer 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) Não entendo por que você não quer usar o kit de expressão regular fornecido com python? Não há "hackery" (para qualquer significado possível de "hackery" que eu conheço) está envolvido:

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

Atualização 2

Após o comentário do OP:

Eu ainda prefiro a resposta de Paulo, pois é mais concisa

Mais conciso do que o quê? A solução regex a seguir não é suficiente para você?

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

Código acima corrigido depois que @nick Perkins apontou que não funcionou

Olhando para dentro ANSI_ESCAPE_CODE, a sequência em seu exemplo éSelecione a interpretação gráfica (provavelmente negrito).

Tente controlar o posicionamento da coluna com o Posição do cursor ( CSI n ; m H) seqüência. Dessa forma, a largura do texto anterior não afeta a posição da coluna atual e não há necessidade de se preocupar com as larguras das cordas.

Uma opção melhor, se você segmentar Unix, está usando o Objetos de janela do módulo de maldição. Por exemplo, uma string pode ser posicionada na tela com:

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

Pinte na maioria dos caracteres N da string str em (y, x) com atributos attr, substituindo qualquer coisa anteriormente na tela.

Se você está apenas adicionando cores a algumas células, pode adicionar 9 à largura da célula esperada (5 caracteres ocultos para ligar a cor, 4 para desligá -la), por exemplo,

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

Dando

screenshot

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top