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

Pau*_* D. 18 python escaping ansi-colors

我有一些Python代码会以一种漂亮的列格式自动打印一组数据,包括放入适当的ASCII转义序列来为各种数据着色以便于阅读.

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

>>> a = '\x1b[1m0.0\x1b[0m'
>>> len(a)
11
>>> print a
0.0
Run Code Online (Sandbox Code Playgroud)

因此,当每列的长度相同时len,它们在屏幕上打印时的长度实际上并不相同.

有没有办法(除了用正则表达式做一些hackery,我宁愿不这样做)来获取转义字符串并找出打印长度是什么所以我可以适当地填充空间?也许某种方式将其"打印"回字符串并检查其长度?

Pau*_*McG 11

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

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

以下是如何将其转换为转义序列 - 剥离器:

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)
Run Code Online (Sandbox Code Playgroud)

打印:

0.0 3
Run Code Online (Sandbox Code Playgroud)


Joh*_*hin 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))
Run Code Online (Sandbox Code Playgroud)

更新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"]))
    )
Run Code Online (Sandbox Code Playgroud)

如果您认为标点符号负担过重,您可以执行以下操作:

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))
Run Code Online (Sandbox Code Playgroud)

(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
>>>
Run Code Online (Sandbox Code Playgroud)

更新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)
Run Code Online (Sandbox Code Playgroud)

[以上代码在 @Nick Perkins 指出它不起作用后已更正]