Python 正则表达式转义运算符 \ 在替换和原始字符串中的用法

15
我不太理解Python正则表达式中原始字符串r'和转义运算符\共同使用的逻辑。希望能得到一些帮助。
代码如下:
import re
text=' esto  .es  10  . er - 12 .23 with [  and.Other ] here is more ; puntuation'
print('text0=',text)
text1 = re.sub(r'(\s+)([;:\.\-])', r'\2', text)
text2 = re.sub(r'\s+\.', '\.', text)
text3 = re.sub(r'\s+\.', r'\.', text)
print('text1=',text1)
print('text2=',text2)
print('text3=',text3)

理论上说: 反斜杠字符('\')用于表示特殊形式或允许使用特殊字符而不调用其特殊含义。
至于本问题末尾提供的链接所解释的,r'代表原始字符串,即符号没有特殊含义,就是它本身。
因此,在上面的正则表达式中,我期望text2和text3是不同的,因为在text2中替换文本为“.”,即一个句点,而(原则上)text3中的替换文本为r'.',即一个原始字符串,即应该出现反斜杠和句点。但它们的结果相同:
结果是:
text0=  esto  .es  10  . er - 12 .23 with [  and.Other ] here is more ; puntuation
text1=  esto.es  10. er- 12.23 with [  and.Other ] here is more; puntuation
text2=  esto\.es  10\. er - 12\.23 with [  and.Other ] here is more ; puntuation
text3=  esto\.es  10\. er - 12\.23 with [  and.Other ] here is more ; puntuation
#text2=text3 but substitutions are not the same r'\.' vs '\.'

在我看来,r' 在替换部分的工作方式并不相同,反斜杠也是如此。另一方面,我的直觉告诉我我在这里漏掉了什么。

编辑1: 根据@Wiktor Stribiżew的评论。 他指出(根据他的链接):

import re
print(re.sub(r'(.)(.)(.)(.)(.)(.)', 'a\6b', '123456'))
print(re.sub(r'(.)(.)(.)(.)(.)(.)', r'a\6b', '123456'))
# in my example the substitutions were not the same and the result were equal
# here indeed r' changes the results

这将会给出:

ab
a6b

这更让我感到困惑。

注意: 我阅读了关于原始字符串的这篇Stack Overflow问题,它非常完整。然而,它没有涉及到替换。


1
它没有提到替换,因为替换模式不是正则表达式。'\.' = r'\.',它是一个 \. 字符的组合。由于它是一个替换模式,所以你会在结果中得到这个文本。然而,在你的测试中使用\,这更加棘手:在正则表达式替换模式中,它是特殊的re.sub(r'\s+\.', r'\\.', text)将产生与text2text3相同的字符串。请参见此Python演示 - Wiktor Stribiżew
3个回答

9

首要的是,

replacement patterns ≠ regular expression patterns

我们使用正则表达式模式来搜索匹配项,使用替换模式来替换在正则表达式中找到的匹配项。
注意:替换模式中唯一的特殊字符是反斜杠\。只有反斜杠必须加倍。
Python中的替换模式语法 re.sub docs很令人困惑,因为它们提到了可以用于替换模式(如\n\r)和正则表达式转义序列(\6)的字符串转义序列,以及那些既可用作正则表达式又可用作字符串转义序列的转义序列(\&)。
我使用术语“正则表达式转义序列”来表示由文字反斜杠和字符组成的转义序列,即'\\X'r'\X',而“字符串转义序列”则表示由\和一个字符或一些连续字符组成的有效string escape sequence。它们仅在常规字符串字面量中被识别。在原始字符串字面量中,您只能转义"(这就是为什么您不能以\"结尾原始字符串字面量的原因,但反斜杠仍然是字符串的一部分)。

因此,在替换模式中,您可以使用反向引用:

re.sub(r'\D(\d)\D', r'\1', 'a1b')    # => 1
re.sub(r'\D(\d)\D', '\\1', 'a1b')    # => 1
re.sub(r'\D(\d)\D', '\g<1>', 'a1b')  # => 1
re.sub(r'\D(\d)\D', r'\g<1>', 'a1b') # => 1

你可能会发现,r'\1''\\1'是相同的替换模式,\1。如果你使用'\1',它将被解析为字符串转义序列,一个八进制值为001的字符。如果你忘记了在不含歧义的反向引用中使用r前缀,也没有问题,因为\g不是有效的字符串转义序列,这样,在字符串中,\转义字符仍然保留在其中。请继续阅读我链接的文档:

与标准C不同的是,所有未识别的转义序列都会保持不变,即反斜杠会保留在结果中。

所以,当你将'\.'作为替换字符串传递时,实际上你发送了\.两个字符组合作为替换字符串,这就是为什么你在结果中得到\.的原因。

\是Python替换模式中的特殊字符

如果您使用re.sub(r'\s+\.', r'\\.', text),您将得到与text2text3相同的结果,请参见this demo。这是因为在替换模式中,两个字面意义上的反斜杠\\表示单个反斜杠。如果您的正则表达式模式中没有第二组,但在替换中传递了r'\2'以实际替换为\2字符组合,那么您将会收到一个错误。因此,当您具有动态的、用户定义的替换模式时,您需要将所有用于传递字面字符串的替换模式中的反斜杠加倍:
re.sub(some_regex, some_replacement.replace('\\', '\\\\'), input_string)

感谢您的回答,解决了这个问题!顺便说一句:有趣的是我们必须使用 replace(...) 来执行 re.sub 替换。递归! - Basj
1
@Basj 实际上,预处理动态替换模式以便其保持字面意义是一种常见做法。在Java中,有一个专门设计用于此目的的Matcher.quoteReplacement方法。然而,在替换模式中的特殊字符集因语言而异。 - Wiktor Stribiżew
1
这说明Python应该拥有re.escape_replre.escape(...,repl = True)re.escape(...,mode ='repl')来转义替换模式,除了re.escape用于转义正则表达式搜索模式。你认为呢,@WiktorStribiżew? - Basj
1
@Basj 是的,将其添加到 re API 中会是一个不错的补充。 - Wiktor Stribiżew

3

解决所有这些字符串转义问题的简单方法是使用函数/lambda作为repl参数,而不是字符串。例如:

output = re.sub(
    pattern=find_pattern,
    repl=lambda _: replacement,
    string=input,
)

替换字符串完全不会被解析,只是代替匹配的位置。

1
很遗憾,如果您需要将替换字符串与反向引用组合使用,那么这是无法使用的。 - pabouk - Ukraine stay strong
这是一个很好的替代方案,可以避免转义转义序列。谢谢。 - drudru

1

来自doc(我的强调):

re.sub(pattern, repl, string, count=0, flags=0) 返回字符串,该字符串通过将字符串中的模式的最左非重叠出现替换为替换项而获得。如果未找到模式,则返回未更改的字符串。repl可以是字符串或函数;如果它是字符串,则其中的任何反斜杠转义都将被处理。也就是说,\n会转换为单个换行符,\r会转换为回车符等。 ASCII字母的未知转义符保留供将来使用并视为错误。其他未知转义符如\&不会被处理。与模式中第6组匹配的子字符串将替换为反向引用,例如\6。

repl参数不仅仅是纯文本。它也可以是函数的名称或指向组中位置的引用(例如\g<quote>\g<1>\1)。

此外,从这里:

与标准C不同的是,所有未识别的转义序列都保留在字符串中不变,即反斜杠保留在结果中。

由于.不是特殊的转义字符,'\.'r'\.\相同。


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