正则表达式中的 (*SKIP) 或 (*F) 是如何工作的?

35

我正在学习正则表达式的高级用法,并注意到许多帖子中使用了(*SKIP)(*F)

我发布了一个问题,其想法是匹配不包含yellow但仅在blue存在且在brown之后的行。 正确答案是:

.*yellow.*(*SKIP)(*F)|^.*\bblue\b(?=.*brown).*$

我也尝试了像下面这样的环视表达式,但并没有适用于所有情况:

^((?!yellow).)*blue(?=.*brown).*$

我对这些(*SKIP)(*F)标志毫不了解,所以问题是,这些标志如何工作?它们是做什么的?是否还有类似的标记?

谢谢。


@SotiriosDelimanolis 实际上我还没有在Java模式中使用它,我正在regex101上测试它。 - Federico Piazza
@SotiriosDelimanolis 是的,你说得对,但是我的想法是稍后在Java上实现这个正则表达式。首先我想学习正则表达式的方法。顺便问一下,由于我正在使用regex101,你推荐我使用哪个标签而不是java? - Federico Piazza
@TheLostMind 不需要道歉,我不想让任何人感到困惑。感谢你的修复。 - Federico Piazza
2
这些标志是 Perl Compatible Regular Expressions (PCRE) 的一个特性,因此我建议阅读它的文档(在文档中搜索您想了解的标记)。为了使用它们,您需要找到一个支持您选择的语言的正则表达式库。我不知道 Java 是否有这样的库。 - SamYonnou
1
@Fede:我认为(*SKIP)(*F)在Java上不起作用。但是在Java中有其他hack方式可以规避可变长度lookbehind。 - anubhava
2个回答

68

这两个回溯控制动词仅在Perl、PCRE和pypi正则表达式模块中实现。

(*SKIP)(*FAIL)技巧的思想是消耗您想要避免的字符,这些字符不能成为匹配结果的一部分。

使用此技巧的经典模式如下所示:

What_I_want_to_avoid(*SKIP)(*FAIL)|What_I_want_to_match

正则表达式引擎会按照以下方式处理字符串:

  • 默认情况下,模式的第一个标记在从左到右的每个字符上进行测试(大多数时候是这样,但某些正则表达式引擎可以设置为从右到左工作,如果我没记错的话.net可以做到这一点)

  • 如果第一个标记匹配,则正则表达式引擎将使用下一个字符测试模式的下一个标记(在第一个标记匹配之后)等等。

  • 当标记失败时,正则表达式引擎将获取由最后一个标记匹配的字符,并尝试另一种方法使模式成功(如果也不起作用,则正则表达式引擎会对先前的标记执行相同的操作等等)

当正则表达式引擎遇到(*SKIP)动词时(在这种情况下,所有以前的标记显然都已成功),它不再有权回到左侧的所有先前标记,并且不再有权重试所有匹配的字符以使用模式的另一个分支或在字符串中的下一个位置,直到(*SKIP)动词右侧的模式失败时,最后匹配的字符(包括)。

(*FAIL)的作用是强制模式失败。因此,所有在(*SKIP)左侧匹配的字符都将被跳过,并且正则表达式引擎将在这些字符之后继续其工作。

示例模式仅在第一个分支在(*SKIP)之前失败以允许测试第二个分支时才能成功。

您可以在这里找到另一种解释方式。

关于Java和其他没有这两个特点的正则表达式引擎

回溯控制动词在其他正则表达式引擎中未实现,也没有等价物。

但是,您可以使用几种方法来做同样的事情(更明确地说,避免可能由模式的其他部分匹配的内容)。

使用捕获组:

方法1:

What_I_want_to_avoid|(What_I_want_to_match)

你只需要提取第一组捕获组(或测试它是否存在),因为这是你要查找的内容。如果您使用该模式执行替换,则可以使用匹配结果的属性(偏移量、长度、捕获组)使用字符串函数进行替换。其他像JavaScript、Ruby这样的语言允许使用回调函数作为替换。

方法2:

((?>To_avoid|Other_things_that_can_be_before_what_i_want)*)(What_I_want)

这是一种更简便的替换方式,无需回调函数,替换字符串只需要以\1 (或者$1)开始即可。

使用lookaround:

例如,您想要查找一个单词,该单词不嵌套在两个其他单词之间 (假设这两个单词为S_wordE_word, 且这两个单词不同(请参见Qtax评论))。

(在本示例中,允许出现边缘情况S_word E_word word E_wordS_word word S_word E_word。)

采用回溯控制语句的方法如下:

S_word not_S_word_or_E_word E_word(*SKIP)(*F)|word

为了使用这种方式,正则表达式引擎需要在一定程度上允许可变长度的反向断言。对于 .NET 或新的正则表达式模块,没有问题,反向断言可以具有完全可变的长度。在Java中也是可能的,但必须限制大小 (例如:(?<=.{1,1000}))

Java 的等效写法为:

word(?:(?!not_S_word_or_E_word E_word)|(?<!S_word not_E_word{0,1000} word))

请注意,在某些情况下,仅需要前瞻。还要注意的是,以文字字符开头的模式比以后顾断言开头更有效,这就是为什么我在单词后面(即使需要在断言中再次重写该单词)放置它的原因。


2
非常好的解释。这就是我一直在寻找的解释。我正在寻找Java中的这些行为,所以我想我需要发布另一个问题。 - Federico Piazza
1
@Fede:我会添加一些关于Java没有这些功能的补充。 - Casimir et Hippolyte
1
“使用前瞻”示例并不完全正确,前瞻表达式与(*SKIP)(*F)表达式不匹配相同的字符串,也不执行示例文本所述的操作。例如,在字符串“word E_word”中,“word”应该匹配,但前瞻表达式却无法匹配它。 - Qtax
另一个环视问题:假设您想匹配所有未被直接引用的 \w+,这由 "\w+"(*SKIP)(?!)|\w+ 定义。使用此方法将其转换为环视会得到类似于 \w+(?:(?![^"\w]*")|(?<!"[^"\w]{0,1000}\w{0,1000}))(或者只是 (\w+)(?:(?!")|(?<!"\1)))。对于字符串 "foo"bar"baz",跳过表达式将像应该一样匹配 barbar 在两个引用字符串之间),但是环视表达式将不会匹配任何内容。 - Qtax
@CasimiretHippolyte,感谢您的详细说明。您在过去还帮助过我解决一个困难的问题!对于这个问题,我想知道是否有一种方法可以在JavaScript正则表达式中模仿它? - antoni
显示剩余6条评论

7
(*SKIP)(*F)(又称为*FAIL)模式在Perl手册中有详细说明:http://perldoc.perl.org/perlre.html。然而,它们只能在Perl及模仿Perl的正则表达式(例如PHP使用的PCRE库)中使用。Java内置的正则表达式引擎不支持这些扩展,我也不知道有哪个引擎支持。在Java中,我的一般建议是保持正则表达式简单,并使用其他字符串操作方法来实现无法用短正则表达式清晰地完成的任务。

在 PHP 中确认可用性。 - NVRM

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