在 Python 中处理字符串中的转义序列

有时,当我从文件或用户获得输入时,我得到一个包含转义序列的字符串。我想处理逃逸序列 与 Python 处理字符串字面值中的转义序列的方式相同

例如,假设 myString定义为:

>>> myString = "spam\\neggs"
>>> print(myString)
spam\neggs

我想要一个函数(我称之为 process)来完成以下工作:

>>> print(process(myString))
spam
eggs

函数必须能够处理 Python 中的所有转义序列(在上面链接的表中列出)。

Python 有这样做的函数吗?

128932 次浏览

ast.literal_eval函数很接近,但它希望字符串首先被正确引用。

当然,Python 对反斜杠转义的解释取决于字符串的引用方式("" vs r"" vs u"",三重引号等) ,因此您可能希望用合适的引号包装用户输入并传递给 literal_eval。用引号包装它也可以防止 literal_eval返回数字、元组、字典等。

如果用户键入要包围该字符串的类型的无引号引号,那么事情可能会变得棘手。

正确的做法是使用“ string-escape”代码来解码字符串。

>>> myString = "spam\\neggs"
>>> decoded_string = bytes(myString, "utf-8").decode("unicode_escape") # python3
>>> decoded_string = myString.decode('string_escape') # python2
>>> print(decoded_string)
spam
eggs

不要使用 AST 或 eval,使用字符串编解码器要安全得多。

unicode_escape通常不起作用

事实证明,string_escapeunicode_escape解决方案通常不能正常工作——特别是,在实际 Unicode 存在的情况下,它不能正常工作。

如果您能够确定 每个非 ASCII 字符将被转义(并且记住,超过前128个字符的任何内容都是非 ASCII 的) ,那么 unicode_escape将为您做正确的事情。但是如果字符串中已经有任何字面非 ASCII 字符,那么就会出错。

unicode_escape的基本设计是将字节转换为 Unicode 文本。但是在许多地方——例如,Python 源代码——源数据已经是 Unicode 文本。

唯一可以正确工作的方法是首先将文本编码为字节。UTF-8是所有文本的合理编码,因此应该可以正常工作,对吗?

下面的示例在 Python3中,因此字符串文字更清晰,但是同样的问题在 Python2和 Python3上的表现略有不同。

>>> s = 'naïve \\t test'
>>> print(s.encode('utf-8').decode('unicode_escape'))
naïve   test

那就错了。

使用将文本解码为文本的编解码器的新推荐方法是直接调用 codecs.decode。这有帮助吗?

>>> import codecs
>>> print(codecs.decode(s, 'unicode_escape'))
naïve   test

一点也不。(另外,上面是 Python2上的 UnicodeError。)

unicode_escape编解码器,不管它的名字是什么,都假定所有非 ASCII 字节都是拉丁文 -1(ISO-8859-1)编码。所以你必须这样做:

>>> print(s.encode('latin-1').decode('unicode_escape'))
naïve    test

但是,这太可怕了。这将您限制在256个拉丁1字符之内,就好像根本没有发明过 Unicode 一样!

>>> print('Ernő \\t Rubik'.encode('latin-1').decode('unicode_escape'))
UnicodeEncodeError: 'latin-1' codec can't encode character '\u0151'
in position 3: ordinal not in range(256)

添加正则表达式来解决问题

(令人惊讶的是,我们现在没有两个问题。)

我们需要做的是只应用 unicode_escape解码器的东西,我们肯定是 ASCII 文本。特别是,我们可以确保只将其应用于有效的 Python 转义序列,这些转义序列保证是 ASCII 文本。

计划是,我们将使用一个正则表达式找到转义序列,并使用一个函数作为 re.sub的参数,用它们的未转义值替换它们。

import re
import codecs


ESCAPE_SEQUENCE_RE = re.compile(r'''
( \\U........      # 8-digit hex escapes
| \\u....          # 4-digit hex escapes
| \\x..            # 2-digit hex escapes
| \\[0-7]{1,3}     # Octal escapes
| \\N\{[^}]+\}     # Unicode characters by name
| \\[\\'"abfnrtv]  # Single-character escapes
)''', re.UNICODE | re.VERBOSE)


def decode_escapes(s):
def decode_match(match):
return codecs.decode(match.group(0), 'unicode-escape')


return ESCAPE_SEQUENCE_RE.sub(decode_match, s)

就这样:

>>> print(decode_escapes('Ernő \\t Rubik'))
Ernő     Rubik

对于 python 3,实际上正确且方便的答案是:

>>> import codecs
>>> myString = "spam\\neggs"
>>> print(codecs.escape_decode(bytes(myString, "utf-8"))[0].decode("utf-8"))
spam
eggs
>>> myString = "naïve \\t test"
>>> print(codecs.escape_decode(bytes(myString, "utf-8"))[0].decode("utf-8"))
naïve    test

有关 codecs.escape_decode的详情:

  • codecs.escape_decode是字节到字节的解码器
  • codecs.escape_decode解码 ascii 转义序列,如: b"\\n"-> b"\n"b"\\xce"-> b"\xce"
  • codecs.escape_decode不关心或需要知道字节对象的编码,但转义字节的编码应该与对象其余部分的编码相匹配。

背景:

  • @ rspeer 是正确的: 对于 python3,unicode_escape是不正确的解决方案。这是因为 unicode_escape对转义字节进行解码,然后将字节解码为 unicode 字符串,但是没有收到关于第二次操作使用哪个编解码器的信息。
  • @ Jerub 是正确的: 避免 AST 或 eval。
  • 我最初是从 这个问题的答案是“我如何在 Python 3中解码(‘ string-escape’) ?”中发现 codecs.escape_decode的。正如那个答案所说的,那个函数目前没有为 python3编写文档。

下面的代码应该适用于需要显示在字符串上的 n。

import string


our_str = 'The String is \\n, \\n and \\n!'
new_str = string.replace(our_str, '/\\n', '/\n', 1)
print(new_str)

这是一种糟糕的方法,但是当我试图解释字符串参数中传递的转义八进制时,它起作用了。

input_string = eval('b"' + sys.argv[1] + '"')

值得一提的是 eval 和 ast.Literal _ eval 之间存在差异(eval 更不安全) ,请参阅 使用 python & # 39; s eval () vs. ast.Literal _ eval () ?

Jerub (目前)接受的答案对 python2是正确的,但对 python3是不正确的,并且可能产生混乱的结果(正如 Apalala 在对该解决方案的评论中指出的那样)。这是因为 unicode _ escape 编解码器要求其源代码按照官方 巨蟒文档以拉丁文 -1而不是 utf-8编码。因此,在 python3中使用:

>>> myString="špåm\\nëðþ\\x73"
>>> print(myString)
špåm\nëðþ\x73
>>> decoded_string = myString.encode('latin-1','backslashreplace').decode('unicode_escape')
>>> print(decoded_string)
špåm
ëðþs

这个方法还避免了 metatoaster 对 Jerub 解决方案的注释中字符串和字节之间不必要的往返(但是向 metatoaster 致敬,因为它识别出了解决方案中的 bug)。

正确地引用字符串,使其看起来像等效的 Python 字符串文字,然后使用 ast.literal_eval。这个 是安全的,但是比你想象的要复杂得多。

"添加到字符串的开头和结尾非常容易,但是我们还需要确保字符串中的任何 "都被正确转义。如果我们想要完全兼容 Python 的翻译,我们需要考虑到 无效转义序列的过时行为

我们需要把 再加一个反斜杠变成:

  • 甚至反斜杠数后跟双引号的任何序列(这样我们在需要时转义一个引号,但不转义反斜杠,如果已经转义了就取消转义该引号) ; 以及

  • 输入结尾反斜杠数的 奇怪序列(否则反斜杠将转义我们的双引号)。

下面是一个酸性测试输入,显示了一系列棘手的案例:

>>> text = r'''\\ \ \" \\" \\\" \'你好'\n\u062a\xff\N{LATIN SMALL LETTER A}"''' + '\\'
>>> text
'\\\\ \\ \\" \\\\" \\\\\\" \\\'你好\'\\n\\u062a\\xff\\N{LATIN SMALL LETTER A}"\\'
>>> print(text)
\\ \ \" \\" \\\" \'你好'\n\u062a\xff\N{LATIN SMALL LETTER A}"\

我最终找到了一个正则表达式,它可以正确地处理所有这些情况,允许使用 literal_eval:

>>> def parse_escapes(text):
...     fixed_escapes = re.sub(r'(?<!\\)(\\\\)*("|\\$)', r'\\\1\2', text)
...     return ast.literal_eval(f'"{fixed_escapes}"')
...

测试结果:

>>> parse_escapes(text)
'\\ \\ " \\" \\" \'你好\'\nتÿa"\\'
>>> print(parse_escapes(text))
\ \ " \" \" '你好'
تÿa"\

这应该可以正确处理所有内容——包含单引号和双引号的字符串,每个带反斜杠的奇怪情况,以及输入中的非 ASCII 字符。(我承认用眼睛看 核实的结果有点困难!)