以特殊字符开头或结尾的单词边界会导致意外结果

11

假设我想要在这个短语中匹配短语 Sortes\index[persons]{Sortes} 的存在:test Sortes\index[persons]{Sortes} text

使用Python的re模块,我可以这样做:

>>> search = re.escape('Sortes\index[persons]{Sortes}')
>>> match = 'test Sortes\index[persons]{Sortes} text'
>>> re.search(search, match)
<_sre.SRE_Match object; span=(5, 34), match='Sortes\\index[persons]{Sortes}'>

这个方法是可行的,但我希望避免在短语 test Sortes\index[persons]{Sortes} text 中搜索模式Sortes后得到肯定的结果。

>>> re.search(re.escape('Sortes'), match)
<_sre.SRE_Match object; span=(5, 11), match='Sortes'>

我使用\b模式,像这样:

search = r'\b' + re.escape('Sortes\index[persons]{Sortes}') + r'\b'
match = 'test Sortes\index[persons]{Sortes} text'
re.search(search, match)

现在,我找不到匹配项。

如果搜索模式不包含任何[]{}中的字符,它就能够工作。例如:

>>> re.search(r'\b' + re.escape('Sortes\index') + r'\b', 'test Sortes\index test')
<_sre.SRE_Match object; span=(5, 17), match='Sortes\\index'>

此外,如果我删除最后的r'\b',它也可以正常工作:

re.search(r'\b' + re.escape('Sortes\index[persons]{Sortes}'), 'test Sortes\index[persons]{Sortes} test')
<_sre.SRE_Match object; span=(5, 34), match='Sortes\\index[persons]{Sortes}'>

此外,文档关于\b 的说明如下:

请注意严格定义,\b 是一个\w字符和\W字符(或反之)之间的边界,或者是\w字符和字符串开头/结尾之间的边界。

因此,我尝试将最后一个\b替换为(\W|$):
>>> re.search(r'\b' + re.escape('Sortes\index[persons]{Sortes}') + '(\W|$)', 'test Sortes\index[persons]{Sortes} test')
<_sre.SRE_Match object; span=(5, 35), match='Sortes\\index[persons]{Sortes} '>

哎呀,它起作用了!这是怎么回事?我错过了什么吗?


1
},您的模式的最后一个字符是非单词字符,其后的空格也是如此。因此没有单词边界,也没有匹配项。如果最后一个字符是s,它是单词字符,因此存在单词边界。 - Sebastian Proske
2个回答

12

查看单词边界匹配的方式:

单词边界可以出现在以下三个位置之一:

  • 在字符串中的第一个字符之前,如果第一个字符是单词字符。
  • 在字符串中的最后一个字符之后,如果最后一个字符是单词字符。
  • 在字符串中的两个字符之间,其中一个是单词字符,另一个不是单词字符。

在您的模式}\b中,只有在}之后存在一个单词字符(字母、数字或_)时才进行匹配。

当您使用(\W|$)时,您需要显式地要求非单词字符或字符串结尾。

一种解决方法是 自适应单词边界

re.search(r'(?:(?!\w)|\b(?=\w)){}(?:(?<=\w)\b|(?<!\w))'.format(re.escape('Sortes\index[persons]{Sortes}')), 'test Sortes\index[persons]{Sortes} test')

或相当于:

re.search(r'(?!\B\w){}(?<!\w\B)'.format(re.escape('Sortes\index[persons]{Sortes}')), 'test Sortes\index[persons]{Sortes} test')

这里使用自适应动态单词边界,意思如下:

  • (?:(?!\w)|\b(?=\w))(相当于(?!\B\w))- 左边界,确保如果下一个字符是单词字符,则当前位置位于单词边界上;如果下一个字符不是单词字符,则不应用上下文限制。(注意,如果您希望在下一个字符不是单词字符时禁止其左侧立即出现单词字符,则需要使用(?:\B(?!\w)|\b(?=\w))
  • (?:(?<=\w)\b|(?<!\w))(相当于(?<!\w\B))- 右边界,确保如果前一个字符是单词字符,则当前位置位于单词边界上;如果前一个字符不是单词字符,则不应用上下文限制。(注意,如果您希望在前一个字符不是单词字符时禁止其右侧立即出现单词字符,则需要使用(?:(?<=\w)\b|\B(?<!\w))

在这些情况下,您还可以考虑基于负向先行断言的明确单词边界

re.search(r'(?<!\w){}(?!\w)'.format(re.escape('Sortes\index[persons]{Sortes}')), 'test Sortes\index[persons]{Sortes} test')

在这里,(?<!\w) 负向零宽断言会在当前位置左边紧跟一个单词字符时匹配失败,而 (?!\w) 负向预测先行断言会在当前位置右边紧跟一个单词字符时匹配失败。

该如何选择? 自适应的单词边界与明确的单词边界相比更为宽松,因为后者假定匹配两端都不能有单词字符,而前者允许在任何上下文中匹配前导和尾随的非单词字符。

:可以很容易地进一步自定义这些环视模式(例如,仅在模式周围有字母时才使匹配失败,使用[^\W\d_] 代替 \w,或者如果只允许在空格周围进行匹配,则使用 空白边界 (?<!\S) / (?!\S) 环视边界)。


我喜欢关于负向先行断言的建议。这个正则表达式匹配在我的代码中是一个非常热门的部分,所以我担心匹配的性能。使用先行断言会有这方面的顾虑吗? - Stenskjaer
@Stenskjaer \b 也是一种零宽度断言,就像其他任何环视一样。由于这些环视模式只包含单个原子,因此开销与您已经使用的 \b 差别不大。如果您担心性能问题,可以进行快速性能测试,但这是我能想到的唯一正确的正则表达式解决问题的方法。 - Wiktor Stribiżew
真的!我刚刚亲测了一下。在性能上没有(可检测到的)差异。谢谢。 - Stenskjaer
自适应单词边界可以写为:(?:(?!\w)|\b(?=\w)) => (?!\B\w),以及 (?:(?<=\w)\b|(?<!\w)) => (?<!\w\B) - Casimir et Hippolyte
此外,我录制了一段名为“动态自适应词边界”的YT视频,在其中详细解释了这些结构。 - Wiktor Stribiżew

1
我认为这就是你遇到的问题: \b 位于 \w\W 的边界上,但在这个不起作用的例子中,'{Sortes}\b'\W\W 之间的边界,因为 '}' 不匹配 [a-zA-Z0-9_],这是 \w 的普通集合。

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