在Python中获取带有ANSI颜色代码的字符串的正确长度

31

我有一些Python代码,可以自动以漂亮的列格式打印一组数据,包括插入适当的ASCII转义序列来为易读性着色。

最终,每行都被表示为一个列表,每个项目都是空格填充的列,以使每行上的相同列始终具有相同的长度。不幸的是,当我实际打印它时,并非所有列都对齐。我怀疑这与ASCII转义序列有关——因为len函数似乎无法识别它们:

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

尽管每一列都根据len具有相同的长度,但当它们在屏幕上打印时它们实际上长度是不相等的。

除了使用正则表达式进行一些hackery(我宁愿不这样做)之外,是否有任何方法可以获取转义后的字符串并找出其打印长度,以便我可以适当地进行空格填充?也许有一种方法可以将其“打印”回字符串,并检查那个长度?


4
实际上这些是“ANSI”颜色代码,而不是“ASCII”,它们会在支持ANSI颜色的终端或使用ANSI.SYS驱动程序的PC上显示。 - PaulMcG
4个回答

12

pyparsing维基包括这个有用的表达式,用于匹配ANSI转义序列:

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

这是如何将其制作成转义序列剥离器的方法:

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

从技术上讲,分隔列表也可以包含字符串,尽管您很少会遇到这样的序列。另请参阅http://stackoverflow.com/questions/1833873/python-regex-escape-characters/1834669#1834669。 - bobince
1
哦,我可真知道啊!在我年轻的时候,我们让那些VT100终端跳舞,闪烁着它们的LED灯,改变滚动区域,输出双高双宽字体,以粗体反色显示——啊,那些日子多么令人陶醉啊... - PaulMcG
谢谢,这个完美地解决了我的问题!我本来希望能找到一个blahlibrary.unescape()方法之类的东西,但这也是一个不错的替代方案! - Paul D.

6

我不理解两件事情。

(1) 这是你的代码,在你的控制下。你想要给你的数据添加转义序列,然后再将它们剥离出来,以便计算数据的长度??在添加转义序列之前计算填充似乎更简单。我错过了什么吗?

假设没有任何转义序列会改变光标位置。如果有,当前被接受的答案也无法正常工作。

假设你有每列字符串数据(在添加转义序列之前)的列表,名为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

根据OP的评论:

我想要去除它们并计算包含颜色代码的字符串长度的原因是因为所有数据都是通过编程构建的。我有一堆着色方法,正在构建数据,就像这样: 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自带的正则表达式工具包?没有涉及到任何“hackery”(我知道的任何可能的“hackery”含义):

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

根据评论:

我仍然更喜欢Paul的答案,因为它更加简洁。

比什么更加简洁呢?以下的正则表达式解决方案难道不够简洁了吗?

# === 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
关于正则表达式的评论,我没有使用Python内置支持时遇到任何问题。我通常会避免使用正则表达式解析,因为很容易出错并忘记一些边缘情况。可以看看这里SO上无休止的问题列表,这些人试图使用HTML来证明这一点。或者,只需查看Paul帖子上的评论,指出他提供的内容实际上并没有考虑非颜色控制代码。话虽如此,当只担心颜色时,就像你展示的那样,它非常简单。 - Paul D.
1
“遗忘”边缘情况的实现与实现工具(pyParsing、regex、汇编语言)无关。正则表达式无法正确解析HTML;那些不知道这一点的人提出的无尽问题证明不了什么。实际上,Paul帖子中的评论是指带有字符串常量参数而不是整数常量的序列,还提到它们很少见。与颜色相关的序列是图形呈现命令集合的一个小子集,而这只是众多命令之一。你没有回答“更简洁”的问题。 - John Machin
代码去除序列的功能无法正常工作:'_sre.SRE_Pattern'对象不可调用--难道你不需要调用一些“replace”函数或其他什么吗? - Nick Perkins
这个代码是有效的:return re.sub(strip_ANSI_escape_sequences_regex,"",s) - Nick Perkins
@Nick Perkins:感谢您指出这一点。我已经更正了,在设置中进行编译和属性查找。 - John Machin
显示剩余2条评论

1
ANSI转义代码中查找,你的示例中的序列是选择图形渲染(可能是粗体)。
尝试使用 光标位置CSI n ; m H)序列来控制列定位。这样就不会受到先前文本宽度的影响,也不需要担心字符串宽度。
如果你的目标是Unix,更好的选择是使用curses模块窗口对象。例如,可以使用以下方式在屏幕上定位字符串:

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

在(y, x)处最多绘制字符串str的前n个字符,带有属性attr,并覆盖以前显示的任何内容。


1

如果你只是给一些单元格添加颜色,你可以在预期的单元格宽度上加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


你能再描述一下你在这里做什么吗?我的意思是,你正在混合格式和颜色,所以看起来有点混乱。输出结果又是什么样子呢?(没有看到输出结果很难知道它是如何完成的。) - not2qubit
1
@not2qubit 确定,我已经添加了一些注释和截图 - 希望这能更清楚地解释它。 - Brian Burns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接