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

Posted

技术标签:

【中文标题】在 Python 中为具有 ANSI 颜色代码的字符串获取正确的字符串长度【英文标题】:Getting correct string length in Python for strings with ANSI color codes 【发布时间】:2011-01-12 07:34:55 【问题描述】:

我有一些 Python 代码可以自动以漂亮的列格式打印一组数据,包括放入适当的 ASCII 转义序列以对各种数据段进行着色以提高可读性。

我最终将每一行表示为一个列表,每个项目都是一个用空格填充的列,因此每行上的相同列始终具有相同的长度。不幸的是,当我真正去打印这个时,并不是所有的列都排成一行。我怀疑这与 ASCII 转义序列有关——因为 len 函数似乎无法识别这些:

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

因此,虽然根据len,每一列的长度相同,但在屏幕上打印时它们实际上并不是相同的长度。

有没有什么办法(除了用正则表达式做一些我不想做的黑客行为)来获取转义的字符串并找出打印的长度是多少,以便我可以适当地间隔填充?也许有某种方法可以将其“打印”回字符串并检查其长度?

【问题讨论】:

这些实际上是“ANSI”颜色代码,而不是“ASCII”,如在 ANSI 彩色终端或使用 ANSI.SYS 驱动程序的 PC 上显示的那样。 【参考方案1】:

pyparsing wiki 包含此 helpful expression 用于匹配 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

【讨论】:

从技术上讲,分隔列表中也可以包含字符串,尽管您不太可能遇到这样的序列。另请参阅***.com/questions/1833873/… 哦,我不知道吗!在我年轻的时候,我们让那些 VT100 跳舞,闪烁它们的 LED,改变它们的滚动区域,输出双高双宽字体,以粗体反转视频 - 啊,那是多么令人兴奋的日子...... 谢谢,效果很好!我希望在我忽略的某个地方只有一些 blahlibrary.unescape() 方法,但这是下一个最好的事情!【参考方案2】:

我不明白两件事。

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

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

Update-2

在 OP 的评论之后:

我还是更喜欢保罗的回答,因为它更简洁

比什么更简洁?以下正则表达式解决方案对您来说不够简洁吗?

# === 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 指出它不起作用后更正]

【讨论】:

感谢约翰的回答。我想去掉它们并计算长度 after 字符串包含颜色代码的原因是因为所有数据都是以编程方式建立的。我有一堆着色方法,我正在构建这样的数据: str = "%s/%s/%s" % (GREEN(data1), BLUE(data2), RED(data3)) 它会事后很难为文本着色。至于hackery,也许我应该说的是,“我想这是一个已解决的问题,我只是找不到合适的库”。猜不出来,但我仍然更喜欢保罗的回答,因为它更简洁。 好的,我会咬这个。我看到你在计算长度时得到了什么。您提出的解决方案不太奏效,只是因为提前知道所需的列长度要求您知道要存储在该列的单元格中的最大字符串的长度。尽管如此,我认为编写一些绕过这个并且不需要在事后剥离颜色序列的东西并不难。 至于正则表达式注释,我使用 Python 的内置支持没有任何问题。我通常倾向于回避进行正则表达式解析,因为它很容易搞砸并忘记一些边缘情况。请参阅此处关于 SO 的无休止的问题列表,这些问题来自试图将正则表达式与 html 结合使用的人们来证明这一点。或者,只需查看 Paul 帖子上的评论,其中指出他提供的内容实际上并未考虑非颜色控制代码。也就是说,当只担心颜色时,正如您所展示的那样非常简单。 “忘记”边缘案例的实现独立于实现工具(pyParsing、regex、汇编语言)。正则表达式无法正确解析 HTML;不知道的人提出的无休止的问题清单证明不了任何事情。实际上,对 Paul 帖子的评论提到了带有字符串常量参数而不是整数常量的序列,并提到它们很少见。与颜色相关的序列是设置图形再现命令的一小部分,而这只是众多命令之一。您还没有提到“更简洁”。 去除序列的代码不起作用:'_sre.SRE_Pattern' 对象不可调用 -- 你不需要调用一些“替换”函数或其他什么吗?【参考方案3】:

查看ANSI_escape_code,您的示例中的序列是 选择图形再现(可能是粗体)。

尝试使用 CUrsor Position (CSI n ; m H) 序列控制列定位。 这样,前面文本的宽度不会影响当前列位置,也无需担心字符串宽度。

如果您以 Unix 为目标,更好的选择是使用 curses module window-objects。 例如,可以在屏幕上定位一个字符串:

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

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

【讨论】:

谢谢 - 我会看看 curses。【参考方案4】:

如果您只是为某些单元格添加颜色,您可以将 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))

给予

【讨论】:

你能描述一下你在这里做什么吗?我的意思是您将格式与颜色混合在一起,因此看起来有些混乱。输出看起来如何? (如果没有看到,很难知道它是如何完成的。 @not2qubit 当然,我添加了一些 cmets 和屏幕截图 - 希望能澄清一点

以上是关于在 Python 中为具有 ANSI 颜色代码的字符串获取正确的字符串长度的主要内容,如果未能解决你的问题,请参考以下文章

Python `string.format()`、填充字符和 ANSI 颜色

python怎么用turtle打出不同颜色的字?

在最新的VScode中运行Minitest - 显示Ansi颜色代码

如何阻止 ANSI 颜色代码弄乱 printf 对齐?

VIM 中的 ANSI 颜色代码

[oeasy]python0074_设置高亮色_color_highlight_ansi_控制终端颜色