正则表达式中的负向后行断言

5
(注意:不是Why can't you use repetition quantifiers in zero-width look behind assertions的重复;请看帖子结尾。)
我试图编写一个 grep -P(Perl)正则表达式,用于匹配B,当B不是在A之前—无论是否有中间空格。
因此,我尝试了这个负向回顾,并在regex101.com上进行了测试:
(?<!A)\s*B

这会导致“AB”不匹配,这是好的,但“A B”会匹配,这不是我想要的。
我不确定为什么会这样。这与\s*匹配空字符串""有关,可以说在A和B之间有无限个\s*匹配。但为什么会影响“A B”而不是“AB”?
以下正则表达式是否正确解决了问题,如果是,它为什么能解决问题?
(?<![A\s])\s*B

我之前发布过这个问题,但被错误地标记为重复问题。我想要的可变长度部分是匹配的一部分,而不是负向回顾本身的一部分,因此这与其他问题非常不同。是的,我可以将 \s* 放在负向回顾中,但我没有这样做(并且这样做不受支持,正如其他问题所解释的那样)。此外,我特别想知道上面发布的备用正则表达式为什么起作用,因为我知道它有效,但我不确定为什么。其他问题没有帮助回答这个问题。


1
(?<![A\s])\s*B 这并不是一个好的做法。其中一个原因是回溯过程非常庞大。也许有一天你会更关心性能而不是实质。由于你正在使用 Perl,请利用它的动词。 (?:A\s*B(*SKIP)(*FAIL)|B) - user557597
1
比较 Regex1: (?<![A\s])\s*B 已完成迭代次数: 50 / 50 ( x 1000 ) 每次迭代找到的匹配项: 1 经过时间: 0.53 秒, 530.18 毫秒, 530185 微秒 Regex2: (?:A\s*B(*SKIP)(*FAIL)|B) 已完成迭代次数: 50 / 50 ( x 1000 ) 每次迭代找到的匹配项: 1 经过时间: 0.18 秒, 180.07 毫秒, 180073 微秒 - user557597
@sln:特殊动词非常有用,因为它们可以在A和B是整个单词而不仅仅是字符时使用。 - std_answ
@ikegami: 很好的发现,由于没有使用“^|”选项,“[^A\s]\s*B”无法匹配“B”或“ B”。 - std_answ
我的错,我没有明确指出它们一开始是整个单词。但这仍然是有用的信息。对于参考,ikegami指出,如果A和B只是字符而不是单词,则[^A\s]\s*B存在问题,而(?:^|[^A\s])\s*B可以解决这个问题。 - std_answ
显示剩余3条评论
1个回答

6
但为什么这会影响"A B",却不影响"AB"呢?
正则表达式在一个位置上进行匹配,这个位置有助于我们考虑它存在于字符之间。在"A B"中,有一个位置(在空格后和B前)可以使得(?<!A)成功(因为前面没有紧接着的A,而是一个空格),并且\s*B也可以成功(\s*匹配空字符串,B匹配B),所以整个模式都能够成功。
在"AB"中就不存在这种情况了。唯一可以匹配\s*B(紧挨着B)的地方,也同时紧挨着A,所以(?<!A)无法成功。没有符合两个条件的位置,所以整个模式都不能成功。
以下正则表达式是否正确,如果正确的话,它是如何解决问题的? (?<![A\s])\s*B 这个正则表达式是正确的,因为(?<![A\s])不会在紧挨着A或者空格之后立即成功。所以现在回顾任何带有空格的匹配位置时,先前出现的空格会被\s*提取,并且匹配位置必须在它们之前。如果这个位置也没有A,那么回溯可以成功,整个模式也可以匹配。
这是一个技巧,这种技巧可以通过这样的事实变得可行:\s是一个固定宽度的模式,在\s*非空匹配的每个位置都能进行匹配。但这不能扩展到任意一个在(non-)A和B之间的模式的特殊情况。

有道理,谢谢!关于你的第一点:我花了一分钟才意识到 "so (?<!A) cannot match" 的意思是负回顾后发现成功识别了 A,并因此导致整个字符串匹配失败。 - std_answ
总结一下,对于任何阅读此文并感到困惑的人:对于原始正则表达式,“A B”是一个棘手的情况,因为在B之前有一个潜在的匹配位置,其中\s*就像空字符串一样作用,并且有一个前导空格,而不是前导的A,所以负回顾没有禁止匹配。为了修复这个问题,修改后的正则表达式确保只有不直接跟在空格后面的匹配位置才能被考虑。 - std_answ
@wdep1 说得对!我把“match”改成了“succeed”,希望这样更清晰(负向先行断言通过不匹配任何内容来实现成功)。 - hobbs

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