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

32 投票
4 回答
8187 浏览
提问于 2025-04-15 18:47

我有一段Python代码,可以自动把一组数据以整齐的列格式打印出来,还会加上合适的ASCII转义序列,让不同的数据部分有颜色,方便阅读。

最后,我得到的每一行都被表示成一个列表,每个项目就是一列,并且这些列之间用空格填充,这样每行的同一列总是保持相同的长度。可惜的是,当我实际打印这些内容时,并不是所有的列都对齐。我怀疑这和ASCII转义序列有关,因为len函数似乎不识别这些序列:

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

所以,虽然每列在len看来是相同长度的,但在屏幕上打印出来时,它们实际上并不是同样的长度。

有没有什么办法(除了用正则表达式搞一些黑科技,我不太想这样做)来处理这个转义字符串,找出它在打印时的实际长度,这样我就可以适当地填充空格?也许有办法把它“打印”回字符串,然后检查那个的长度?

4 个回答

1

ANSI转义码中,你的例子里的序列是选择图形表现(可能是加粗)。

你可以试着用光标位置控制CSI n ; m H)来调整列的位置。这样,之前的文本宽度就不会影响当前的列位置,也不用担心字符串的宽度了。

如果你是针对Unix系统,使用curses模块的窗口对象会是个更好的选择。例如,你可以用下面的方式在屏幕上定位一个字符串:

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

在(y, x)的位置最多绘制n个字符的字符串str,并且可以设置属性attr,这样会覆盖之前显示的内容。

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

在提问者的评论后:

我想去掉转义序列并在字符串包含颜色代码后计算长度的原因是,所有数据都是程序生成的。我有一堆上色的方法,我是这样构建数据的: 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指出它不工作后已修正]

13

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

撰写回答