Получение правильной длины строки в Python для строк с цветовыми кодами ANSI - PullRequest
18 голосов
/ 02 февраля 2010

У меня есть некоторый код Python, который автоматически печатает набор данных в хорошем формате столбцов, включая вставку соответствующих escape-последовательностей ASCII, чтобы раскрасить различные части данных для удобства чтения.

Я в конце концовв итоге каждая строка представляется в виде списка, причем каждый элемент представляет собой столбец с пробелами, так что одинаковые столбцы в каждой строке всегда имеют одинаковую длину.К сожалению, когда я на самом деле иду печатать это, не все столбцы выстраиваются в линию.Я подозреваю, что это связано с escape-последовательностями ASCII - потому что, похоже, функция len не распознает их:

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

И поэтому, хотя каждый столбец имеет одинаковую длину в соответствии с len,они на самом деле не одинаковой длины при печати на экране.

Есть ли какой-нибудь способ (кроме хакерства с помощью регулярных выражений, который я бы предпочел не делать), чтобы взять экранированную строку и выяснить, какова длина печати, чтобы я мог соответствующим образом расставить пробел?Может быть, какой-нибудь способ просто «напечатать» его обратно в строку и проверить его длину?

Ответы [ 4 ]

11 голосов
/ 02 февраля 2010

Вики Pyparsing включает это полезное выражение для сопоставления на escape-последовательностях ANSI:

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

Вот как это сделать в обработчике 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)

печать:

0.0 3
3 голосов
/ 03 февраля 2010

Я не понимаю ДВУХ вещей.

(1) Это ваш код под вашим контролем. Вы хотите добавить escape-последовательности к вашим данным, а затем удалить их снова, чтобы вы могли вычислить длину ваших данных? Кажется, гораздо проще вычислить заполнение перед добавлением escape-последовательностей. Чего мне не хватает?

Предположим, что ни одна из escape-последовательностей не меняет положение курсора. Если они это сделают, то принятый ответ все равно не будет работать.

Предположим, что у вас есть строковые данные для каждого столбца (перед добавлением escape-последовательностей) в списке с именем string_data, а предварительно заданные значения ширины столбца находятся в списке с именем width. Попробуйте что-то вроде этого:

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

Обновление-1

После комментария ОП:

Причина, по которой я хочу вырезать их и вычислить длину после строка содержит коды цветов, потому что все данные созданы программно. У меня есть куча методов раскраски, и я строю скопировать данные примерно так: str = "%s/%s/%s" % (GREEN(data1), BLUE(data2), RED(data3)) Было бы довольно сложно раскрасить текст по факту.

Если данные составлены из частей, каждая из которых имеет свое собственное форматирование, вы все равно можете вычислить отображаемую длину и пэд в зависимости от ситуации. Вот функция, которая делает это для содержимого одной ячейки:

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

Если вы считаете, что вызов перегружен пунктуацией, вы можете сделать что-то вроде:

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) Я не понимаю, почему вы не хотите использовать комплект регулярных выражений, поставляемый с Python? Никакое «хакерство» (для любого возможного значения «хакерства», о котором я знаю) не задействовано:

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

Обновление-2

После комментария ОП:

Я все еще предпочитаю ответ Павла, поскольку он более лаконичен

Более кратко, чем что? Разве следующее решение для регулярных выражений недостаточно для вас?

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

[Вышеупомянутый код исправлен после того, как @Nick Perkins указал, что он не работает]

1 голос
/ 02 декабря 2016

Если вы просто добавляете цвет к некоторым ячейкам, вы можете добавить 9 к ожидаемой ширине ячейки (5 скрытых символов, чтобы включить цвет, 4 для его выключения), например,

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

Даёшь

screenshot

1 голос
/ 02 февраля 2010

Глядя на ANSI_escape_code , последовательность в вашем примере: Выберите графическое представление (вероятно, жирный ).

Попробуйте управлять позиционированием столбца с помощью последовательности CUrsor Position (CSI n ; m H). Таким образом, ширина предыдущего текста не влияет на текущую позицию столбца, и нет необходимости беспокоиться о ширине строки.

Лучшим вариантом, если вы ориентируетесь на Unix, является использование оконных объектов модуля curses . Например, строка может быть размещена на экране с помощью:

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

Нарисуйте не более n символов строки str at (y, x) с атрибутами attr, перезаписывая все, что ранее отображалось на дисплее.

...