前面没有出现奇数个给定字符的正则匹配
我需要在PCRE正则中匹配那些未被转义的字符。
设定我的转义字符是 #,未转义时要匹配的字符是 a1。
一个被一个转义字符前置的字符,否定后向断言是可行的。
例如 (?<!#)a 将匹配“abc”或“cab”,但不会匹配“#abc”,也不会匹配“c#ab”。
但如何处理转义的转义字符呢?
示例:
- bar应该匹配“a”
- b#ar不应匹配,因为“a”已被转义
- b##ar应该匹配“a”,因为第一个# 转义了第二个#
- b###ar不应匹配,因为第三个# 未被转义,因此它对“a”产生了转义
对“不被前一个奇数个其他特定字符所前置的字符”的正则表达式的答案是不可用的,因为PCRE强制了固定宽度的负向后向断言:
1实际上,我需要匹配“{”但前面不能有一个反斜杠,也不能有像“\\”这样的转义,但最终应以“\\”作为前缀。我在此题中选择了“普通”字符以便于阅读,因此在regexp的特性字符方面不再需要进一步转义。
解决方案
由我本人与 @CAustin、@fpierrat、@sln以及 @Thefourthbird整理并扩展的注释:
启用变长长度的后向断言
在Perl中,变长的后向断言仍然是一个实验性特性,因此PCRE2的实现可能不支持它。然而,从 [perlre] 文档中关于lookaround断言的部分可以看出:
这个结构的一个特殊形式,被称为
\K(自Perl 5.10.0起可用),它使正则引擎“保留”在\K之前已经匹配的所有内容,并且不包括在$&内。这实际上提供了任意长度的非实验性变量长度后向断言。另外,还有一种技术可以用于早期版本中处理可变长度后向断言,且长度超过255个字符。它在 http://www.drregex.com/2019/02/variable-length-lookbehinds-actually.html 中有所描述。
虽然这是一个巧妙的技巧,但drregex文章中的这种方法可能并不明智使用。然而 \K 应该非常高效,并且在PHP的 PCRE2实现中应该可用。它可以这样使用:
b(?:##)*\Ka
如果 b 可能不存在,就需要有另一种方法来确保正则表达式消耗掉所有前导的 #。这可以通过一个额外的(固定宽度的)负向后向断言来锚定匹配实现:
(?<!#)(?:##)*\Ka
或者像你观察到的那样,通过匹配一个非 # 的字符,或行/字符串的起始:
(?:^|[^#])(?:##)*\Ka
使用捕获组
另一种做法是使用捕获组而不是后向断言,前提是你的代码能够访问它们。例如:
(?:^|[^#])(?:##)*(a)
在第一个捕获组中返回所需的匹配。
提高回溯效率的占有性量词
对于上面的任意正则示例,使用一个 [占有性量词](*+ 代替 *)是可行的,且应更高效(尽管差异可能只有在存在极长的 # 且其后没有 a 的情况下才会明显。)
