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

169
有时候当我从文件或用户处获取输入时,会得到一个带转义序列的字符串。我希望能够以Python处理字符串字面值中的转义序列的方式来处理这些转义序列
例如,假设myString被定义为:
>>> myString = "spam\\neggs"
>>> print(myString)
spam\neggs

我希望有一个函数(我将其称为process),它可以执行以下操作:

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

重要的是该函数能够处理Python中的所有转义序列(在上面链接的表格中列出)。

Python是否有此功能的函数?


1
你认为包含'spam'+"eggs"+'''some'''+"""more"""的字符串会被如何处理呢? - Nas Banov
5
这个问题的大多数答案都存在严重问题。似乎在Python中没有标准的方法可以在不破坏Unicode的情况下处理转义序列。@rspeer发布的答案是我在Grako中采用的,因为它迄今为止处理了所有已知的情况。 - Apalala
1
我不同意Apalala的观点;在正确使用latin1编码输入的情况下,使用unicode_escape是完全可靠的,并且正如Hack5在他对user19087答案的评论中所展示的问题,这是Python开发人员推荐的方法。 - Glen Whitney
这个回答解决了你的问题吗?如何取消转义反斜杠转义的字符串? - Glen Whitney
显示剩余6条评论
8个回答

178

正确的做法是使用'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。使用字符串编解码器更安全。


3
无疑,这是最佳解决方案!顺便提一下,它的文档应该是“string_escape”(带下划线),但由于某种原因,它接受模式中的任何内容,如“string escape”,“string@escape”等等......基本上是'string\W+escape' - Nas Banov
37
这个解决方案不够好,因为它无法处理原始字符串中存在合法Unicode字符的情况。如果你尝试:>>> print("juancarlo\\tañez".encode('utf-8').decode('unicode_escape'))你会得到:juancarlo añez - Apalala
3
同意 @Apalala 的看法:这还不够好。请查看下面 rseeper 给出的答案,其中提供了适用于 Python2 和 3 的完整解决方案! - Christian Aichinger
3
由于 unicode_escape 假定为 latin1,因此需要重新进行编码/解码处理,例如 s.encode('utf-8').decode('unicode_escape').encode('latin1').decode('utf8') - metatoaster
2
@DonovanBaarda 不,没有任何多字节的utf-8表示法可以将Unicode码点大于127的字符转换为bytes并落在ascii范围内(0-127),因为所有多字节字符都在128-255的范围内(即0x80 - 0xff),这是因为Unicode和UTF-8的设计者理解了这个确切问题。换句话说,不,使用str.encode('utf-8')除了Unicode码点U+005C之外,不可能产生bytes b'\x5c'0x5c)。 - metatoaster
显示剩余10条评论

165

unicode_escape在一般情况下不起作用

事实证明,string_escapeunicode_escape解决方案在一般情况下不起作用 - 特别是在存在实际Unicode字符的情况下。

如果您可以确保每个非ASCII字符都将被转义(请记住,超过第一个128个字符的任何内容都是非ASCII字符),那么unicode_escape将为您完成正确的操作。但是,如果字符串中已经存在任何字面上的非ASCII字符,事情就会出错。

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

这只有在您首先将文本编码为字节时才能正常工作。UTF-8是所有文本的合理编码方式,所以应该可以工作,对吗?

以下示例是在Python 3中,因此字符串文字更清晰,但在Python 2和3上也存在稍微不同的问题。

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

完全不是。 (此外,上述在Python 2中是一个UnicodeError。)
尽管其名称为“unicode_escape”编解码器,但事实证明它假定所有非ASCII字节都是以Latin-1(ISO-8859-1)编码的。因此,您需要像这样处理它:
>>> print(s.encode('latin-1').decode('unicode_escape'))
naïve    test

但这太糟糕了。这限制了你只能使用256个Latin-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

5
我们需要更全面的答案,比如这样的。谢谢。 - v.oddou
1
@Pureferret,我不太确定你在问什么,但你可能不应该在具有不同含义的反斜杠的字符串上运行此程序,例如Windows文件路径。(这是你的os.sep吗?)如果你的Windows目录名称中有反斜杠转义序列,那么情况几乎无法挽回。 - rspeer
这对我不起作用,因为unicode-escape不能正确处理:test = "\\xe2\\x80\\xa6" test_bytes = test.encode() test = test_bytes.decode("unicode-escape")值: test_bytes == b'\\xe2\\x80\\xa6' test == 'â¦' - Mark Ingram
@MarkIngram -- 这个正则表达式是一个关于Unicode转义的Unicode正则表达式,其中\xe2实际上意味着"unicode字符E2"而不是"字节E2"。它与字节无关。如果您能够使其尝试匹配字节字符串,则必须更改了代码或使用了Python 2强制转换。 - rspeer
1
@MarkIngram 是的,我正在使用Python 3。我不明白你发布的例子与我的代码无关。我的代码在任何步骤都没有使用字节串。 - rspeer
显示剩余10条评论

45

对于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。
  • 我最初从这个回答中发现了 codecs.escape_decode,该回答是针对“如何在 Python3 中进行 .decode('string-escape')”这个问题的。正如该回答所述,该函数目前未在 Python 3 中记录。

这是真正的答案(: 可惜它依赖于一个文档不完善的函数。 - jwd
6
这是针对你拥有的 \x 转义序列是 UTF-8 字节的情况下的答案。但由于它将字节解码为字节,所以它不会也不能解码任何非 ASCII Unicode 字符的转义序列,比如 \u 转义序列。 - rspeer
3
提醒一下,这个函数在技术上不是公开的。请参见 https://bugs.python.org/issue30588。 - Hack5
此外,在Hack5提供的链接中,Python维护者明确表示escape_decode可能会在任何未来版本中没有警告地被删除,并且推荐使用"unicode_escape"编解码器来处理这个问题。 - Glen Whitney

11

ast.literal_eval函数接近需求,但它要求字符串被正确引用。

当然,Python对反斜杠转义的解释取决于字符串如何被引用(使用双引号""、原始字符串r""、Unicode字符串u""、三重引号等),所以您可能需要将用户输入的内容包装在适当的引号中,再传递给literal_eval函数。用引号包装还可以防止literal_eval返回数字、元组、字典等。

如果用户键入了未经引用的引号,则仍可能会变得棘手。


我明白了。正如你所说,这似乎是潜在的危险:myString = "\"\ndoBadStuff()\n\"", print(ast.literal_eval('"' + myString + '"')) 似乎尝试运行代码。那么 ast.literal_evaleval 有什么不同/更安全的地方呢? - dln385
9
@dln385:literal_eval 永远不会执行代码。根据文档,“这可用于安全地评估来自不受信任来源的包含 Python 表达式的字符串,而无需自己解析值。” - Greg Hewgill

4

目前,Jerub的答案对于python2是正确的,但对于python3则可能产生乱码的结果(正如Apalala在评论中指出的那样)。这是因为unicode_escape编解码器要求其源代码使用latin-1进行编码,而不是utf-8,根据官方的Python文档。因此,在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发现了那个解决方案中的错误)。

当我发布这个问题时,我没有意识到已经有相同的问题并且已经有了完全相同的答案:https://dev59.com/sHI-5IYBdhLWcg3wYnOQ#57192592 - Glen Whitney
重要的不仅仅是使用latin-1,而是通过“backslashreplace”错误处理将非latin-1字符转换为转义序列。这恰好给出了.decode步骤尝试替换的确切格式。因此,例如,myString ='日本\u8a9e'可以正确地给出日本語。但是,它无法处理我答案中描述的真正恶劣的情况。 - Karl Knechtel
另一方面,可以认为带有单个尾随反斜杠的输入应该失败... - Karl Knechtel
它真的总是Latin-1编码吗?还是取决于您所使用的Python版本的默认编码?例如,在Linux上也是如此吗? - undefined
嗯,在我上面链接的Python文档中的表格中,在“unicode_escape”条目中,它明确写着“从Latin-1源代码解码”。所以对我来说,这似乎非常清晰和明确。 - undefined

0

正确引用字符串,使其看起来像等效的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字符。(我承认用肉眼验证结果有点困难!)


0

这种做法并不好,但当我尝试解释传递给字符串参数的转义八进制数时,它对我起了作用。

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

值得一提的是,eval和ast.literal_eval之间存在差异(eval更加不安全)。请参见使用Python的eval()与ast.literal_eval()的区别?

1
只是为了确保提前警告:请不要对可能来自程序外部的输入使用 eval。这会允许提供该输入的用户在您的计算机上运行任意代码。这并不是非常简单的沙盒操作。 - Karl Knechtel

-3
以下代码应该可以正常工作,\n需要在字符串中显示。
import string

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

4
这段代码存在问题(正斜杠使replace函数无效),使用的API已经过时(此类的“string”模块函数自Python 2.0起被弃用,被“str”方法替代,并在Python 3中完全消失),并且仅处理替换单个换行符的特定情况,而不是一般的转义处理。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接