如何从 python 中的字符串中删除 ANSI 转义序列

下面是包含我的字符串的代码片段。
'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'

The string was returned from an SSH command that I executed. I can't use the string in its current state because it contains ANSI standardized escape sequences. How can I programmatically remove the escape sequences so that the only part of the string remaining is 'examplefile.zip'.

93814 次浏览

用正则表达式删除它们:

import re


# 7-bit C1 ANSI sequences
ansi_escape = re.compile(r'''
\x1B  # ESC
(?:   # 7-bit C1 Fe (except CSI)
[@-Z\\-_]
|     # or [ for CSI, followed by a control sequence
\[
[0-?]*  # Parameter bytes
[ -/]*  # Intermediate bytes
[@-~]   # Final byte
)
''', re.VERBOSE)
result = ansi_escape.sub('', sometext)

或没有 VERBOSE标志的浓缩形式:

ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
result = ansi_escape.sub('', sometext)

演示:

>>> import re
>>> ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
>>> sometext = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
>>> ansi_escape.sub('', sometext)
'ls\r\nexamplefile.zip\r\n'

上面的正则表达式涵盖了所有7位 ANSI C1转义序列,但是 没有是8位 C1转义序列的开头。后者在今天的 UTF-8世界中从未使用过,因为在 UTF-8世界中,相同的字节范围有不同的含义。

如果您确实需要覆盖8位代码(然后,假设,使用 bytes值) ,那么正则表达式将变成如下字节模式:

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(br'''
(?: # either 7-bit C1, two bytes, ESC Fe (omitting CSI)
\x1B
[@-Z\\-_]
|   # or a single 8-bit byte Fe (omitting CSI)
[\x80-\x9A\x9C-\x9F]
|   # or CSI + control codes
(?: # 7-bit CSI, ESC [
\x1B\[
|   # 8-bit CSI, 9B
\x9B
)
[0-?]*  # Parameter bytes
[ -/]*  # Intermediate bytes
[@-~]   # Final byte
)
''', re.VERBOSE)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

可以浓缩成

# 7-bit and 8-bit C1 ANSI sequences
ansi_escape_8bit = re.compile(
br'(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])'
)
result = ansi_escape_8bit.sub(b'', somebytesvalue)

有关更多信息,请参见:

您给出的示例包含4个 CSI (Control Sequon丢失器)代码,由 \x1B[ESC [开始字节标记,每个代码都包含一个 SGR (Select Graphic Rendition)代码,因为它们都以 m结束。其中的参数(由 ;分号分隔)告诉您的终端使用什么图形呈现属性。因此,对于每个 \x1B[....m序列,使用的3个代码是:

  • 0(或本例中的 00) : 重置,禁用所有属性
  • 1(或示例中的 01) : 大胆
  • 31: 红色(前景)

然而,有更多的 ANSI 不仅仅是 CSI SGR 代码。仅使用 CSI,您还可以控制光标、清晰线条或整个显示,或滚动(当然,前提是终端支持这一点)。除了 CSI 之外,还有代码可以选择其他字体(SS2SS3) ,发送“私人信息”(想想密码) ,与终端(DCS)、操作系统(OSC)或应用程序本身(APC,一种应用程序将自定义控制代码搭载到通信流上的方法)进行通信,还有进一步的代码可以帮助定义字符串(SOS,Start of String,ST String Terminal ator)或将一切重置回基状态(RIS)。上面的正则表达式涵盖了所有这些。

注意,上面的正则表达式只删除 ANSI C1代码,而不删除这些代码可能标记的任何其他数据(例如在 OSC 开头和结束 ST 代码之间发送的字符串)。删除这些内容将需要在此答案范围之外进行额外的工作。

如果要删除 \r\n位,可以通过这个函数(作者是萨诺德)传递字符串:

def stripEscape(string):
""" Removes all escape sequences from the input string """
delete = ""
i=1
while (i<0x20):
delete += chr(i)
i += 1
t = string.translate(None, delete)
return t

Careful though, this will lump together the text in front and behind the escape sequences. So, using Martijn's filtered string 'ls\r\nexamplefile.zip\r\n', you will get lsexamplefile.zip. Note the ls in front of the desired filename.

我将首先使用 stripEscape 函数删除转义序列,然后将输出传递给 Martijn 的正则表达式,这将避免连接不需要的位。

接受的答案只考虑到 ANSI 标准转义序列的格式,以改变前景颜色和文本样式。 许多序列不以 'm'结束,例如: 光标定位、擦除和滚动区域。下面的模式试图涵盖除了设置前景色和文本样式之外的所有情况。


下面是 ANSI 标准化控制序列的正则表达式:
/(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/


其他参考资料:

功能

基于 Martijn Pieters 的回答Jeff 的 Regexp

def escape_ansi(line):
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line)

测试

def test_remove_ansi_escape_sequence(self):
line = '\t\u001b[0;35mBlabla\u001b[0m                                  \u001b[0;36m172.18.0.2\u001b[0m'


escaped_line = escape_ansi(line)


self.assertEqual(escaped_line, '\tBlabla                                  172.18.0.2')

测试

如果您想自己运行它,可以使用 python3(更好的 unicode 支持,等等)。测试文件应该是这样的:

import unittest
import re


def escape_ansi(line):
…


class TestStringMethods(unittest.TestCase):
def test_remove_ansi_escape_sequence(self):
…


if __name__ == '__main__':
unittest.main()

The suggested regex didn't do the trick for me so I created one of my own. The following is a python regex that I created based on the spec found here

ansi_regex = r'\x1b(' \
r'(\[\??\d+[hl])|' \
r'([=<>a-kzNM78])|' \
r'([\(\)][a-b0-2])|' \
r'(\[\d{0,2}[ma-dgkjqi])|' \
r'(\[\d+;\d+[hfy]?)|' \
r'(\[;?[hf])|' \
r'(#[3-68])|' \
r'([01356]n)|' \
r'(O[mlnp-z]?)|' \
r'(/Z)|' \
r'(\d+)|' \
r'(\[\?\d;\d0c)|' \
r'(\d;\dR))'
ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)

我在下面的代码片段中测试了我的正则表达式(基本上是从 ascii-table.com 页面复制粘贴的)

\x1b[20h    Set
\x1b[?1h    Set
\x1b[?3h    Set
\x1b[?4h    Set
\x1b[?5h    Set
\x1b[?6h    Set
\x1b[?7h    Set
\x1b[?8h    Set
\x1b[?9h    Set
\x1b[20l    Set
\x1b[?1l    Set
\x1b[?2l    Set
\x1b[?3l    Set
\x1b[?4l    Set
\x1b[?5l    Set
\x1b[?6l    Set
\x1b[?7l    Reset
\x1b[?8l    Reset
\x1b[?9l    Reset
\x1b=   Set
\x1b>   Set
\x1b(A  Set
\x1b)A  Set
\x1b(B  Set
\x1b)B  Set
\x1b(0  Set
\x1b)0  Set
\x1b(1  Set
\x1b)1  Set
\x1b(2  Set
\x1b)2  Set
\x1bN   Set
\x1bO   Set
\x1b[m  Turn
\x1b[0m Turn
\x1b[1m Turn
\x1b[2m Turn
\x1b[4m Turn
\x1b[5m Turn
\x1b[7m Turn
\x1b[8m Turn
\x1b[1;2    Set
\x1b[1A Move
\x1b[2B Move
\x1b[3C Move
\x1b[4D Move
\x1b[H  Move
\x1b[;H Move
\x1b[4;3H   Move
\x1b[f  Move
\x1b[;f Move
\x1b[1;2    Move
\x1bD   Move/scroll
\x1bM   Move/scroll
\x1bE   Move
\x1b7   Save
\x1b8   Restore
\x1bH   Set
\x1b[g  Clear
\x1b[0g Clear
\x1b[3g Clear
\x1b#3  Double-height
\x1b#4  Double-height
\x1b#5  Single
\x1b#6  Double
\x1b[K  Clear
\x1b[0K Clear
\x1b[1K Clear
\x1b[2K Clear
\x1b[J  Clear
\x1b[0J Clear
\x1b[1J Clear
\x1b[2J Clear
\x1b5n  Device
\x1b0n  Response:
\x1b3n  Response:
\x1b6n  Get
\x1b[c  Identify
\x1b[0c Identify
\x1b[?1;20c Response:
\x1bc   Reset
\x1b#8  Screen
\x1b[2;1y   Confidence
\x1b[2;2y   Confidence
\x1b[2;9y   Repeat
\x1b[2;10y  Repeat
\x1b[0q Turn
\x1b[1q Turn
\x1b[2q Turn
\x1b[3q Turn
\x1b[4q Turn
\x1b<   Enter/exit
\x1b=   Enter
\x1b>   Exit
\x1bF   Use
\x1bG   Use
\x1bA   Move
\x1bB   Move
\x1bC   Move
\x1bD   Move
\x1bH   Move
\x1b12  Move
\x1bI
\x1bK
\x1bJ
\x1bZ
\x1b/Z
\x1bOP
\x1bOQ
\x1bOR
\x1bOS
\x1bA
\x1bB
\x1bC
\x1bD
\x1bOp
\x1bOq
\x1bOr
\x1bOs
\x1bOt
\x1bOu
\x1bOv
\x1bOw
\x1bOx
\x1bOy
\x1bOm
\x1bOl
\x1bOn
\x1bOM
\x1b[i
\x1b[1i
\x1b[4i
\x1b[5i

Hopefully this will help others :)

如果它有助于将来的 Stack Overflower,那么我将使用 蜡笔图书馆给我的 Python 输出带来更多的视觉冲击,这在 Windows 和 Linux 平台上都是有利的。然而,我既显示在屏幕上,也附加到日志文件,并且转义序列影响了日志文件的易读性,所以想把它们去掉。然而,蜡笔插入的转义序列产生了一个错误:

expected string or bytes-like object

解决方案是将参数强制转换为字符串,因此只需对普遍接受的答案进行微小的修改:

def escape_ansi(line):
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', str(line))

对于2020年的 python 3.5,它就像 string.encode().decode('ascii')一样简单

ascii_string = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
decoded_string = ascii_string.encode().decode('ascii')
print(decoded_string)


>ls
>examplefile.zip
>

在我使用 OSC 序列(\x1b])的情况下,没有一个正则表达式解决方案是有效的

要实际呈现可见的输出,你需要一个像 ABc0这样的虚拟终端

#! /usr/bin/env python3


import pyte # terminal emulator: render terminal output to visible characters


pyte_screen = pyte.Screen(80, 24)
pyte_stream = pyte.ByteStream(pyte_screen)


bytes_ = b''.join([
b'$ cowsay hello\r\n', b'\x1b[?2004l', b'\r', b' _______\r\n',
b'< hello >\r\n', b' -------\r\n', b'        \\   ^__^\r\n',
b'         \\  (oo)\\_______\r\n', b'            (__)\\       )\\/\\\r\n',
b'                ||----w |\r\n', b'                ||     ||\r\n',
b'\x1b]0;user@laptop1:/tmp\x1b\\', b'\x1b]7;file://laptop1/tmp\x1b\\', b'\x1b[?2004h$ ',
])
pyte_stream.feed(bytes_)


# pyte_screen.display always has 80x24 characters, padded with whitespace
# -> use rstrip to remove trailing whitespace from all lines
text = ("".join([line.rstrip() + "\n" for line in pyte_screen.display])).strip() + "\n"
print("text", text)


print("cursor", pyte_screen.cursor.y, pyte_screen.cursor.x)
print("title", pyte_screen.title)