正则表达式练习:带有先行断言的勉强量词

RegExp exercise: reluctant quantifier with a lookahead assertion

你能解释一下这是怎么回事吗?这是一个例子:

<!-- The quick brown fox 
              jumps over the lazy dog -->

<!--[if IE 7]>
    <link rel="stylesheet" type="text/css" href="/supersheet.css" />
<![endif]-->

<!-- Pack my box with five dozen liquor jugs -->

首先,我尝试使用如下正则表达式来匹配条件注释中的内容:

/<!--.*?stylesheet.*?-->/s

失败,因为正则表达式匹配了第一个<!--和最后一个-->之前的所有内容。然后我尝试使用另一种带有前瞻断言的模式:

/<!--(?=.*?stylesheet).*?-->/s

它工作正常,完全符合我的需要。但是,以下正则表达式也适用:

/<!--(?=.*stylesheet).*?-->/s

最后一个正则表达式在先行断言中没有不情愿的量词。现在我很困惑。任何人都可以向我解释它是如何工作的吗?也许这个例子有更好的解决方案?

更新:

我尝试在另一个文档中使用带有先行断言的正则表达式,但无法处理注释之间的内容。所以,这个 /<!--(?=.*?stylesheet).*?-->/s(以及这个 /<!--(?=.*stylesheet).*?-->/s)是不正确的。不要使用它并尝试其他建议。

更新:

解决方案已被 Jonny 5 找到(参见答案)。他提出了三个选项:

  1. 使用否定连字符来限制匹配。此选项仅在标签之间没有连字符时才有效。如果样式表有 URL /style-sheet.css,它将不起作用。
  2. 转义序列的使用:\K。它就像一个魅力。缺点如下:
    • 它非常慢(在我的例子中,它比其他解决方案慢 8-10 倍)
    • 仅适用于 PHP 5.2.4
  3. 使用前瞻来缩小匹配范围。这是我试图实现的目标,但我使用环视断言的经验不足以执行该任务。

我认为以下是我的示例的一个很好的解决方案:

/(?s)<!--(?:(?!<!).)+?stylesheet.+?-->/

相同,但末尾带有 s 修饰符:

/<!--(?:(?!<!).)+?stylesheet.+?-->/s

正如我所说,这是一个很好的解决方案,但我设法改进了模式并找到了另一个在我的情况下运行速度更快的模式。

所以,最终的解决方案如下:

/<!--(?:(?!-->).)+?stylesheet.+?-->/s

感谢所有参与者有趣的回答。

字符串 stylesheet 在您的测试文档中仅被提及一次,因此您尝试的两个正则表达式将以不同的方式匹配相同的内容。

<!--(?=.*?stylesheet).*?-->/s

这一个执行以下操作:

  • 捕获<!--.
  • 向前看,捕获最多 stylesheet 个字符并包括在内。如果找不到则失败。
  • 捕获最多 --> 个字符并包括 -->
<!--(?=.*stylesheet).*?-->/s

这一个执行以下操作:

  • 捕获<!--.
  • 向前看,捕捉任何角色,直到不再可能。原路返回,不断尝试匹配stylesheet。如果找不到则失败。
  • 捕获最多 --> 个字符并包括 -->

基本上,一个需要大量回溯,而另一个则不需要。

如果您的主题是...

<!-- The quick brown fox 
              jumps over the lazy dog -->

<!--[if IE 7]>
    <link rel="stylesheet" type="text/css" href="/supersheet.css" /> <![endif]-->

<!-- Pack my box with five dozen stylesheets -->

你得到两个不同的结果。前者会找到第一个 stylesheet,而后者会找到第二个(也是最后一个),因为它从字符串的末尾开始搜索。

只匹配<!--...stylesheet...-->部分有很多种方式:

1.) 使用 negated 连字符 [^-] 来限制匹配并停留在 <!--stylesheet

之间
(?s)<!--[^-]+stylesheet.+?-->

[^-] 只允许不是连字符的字符。参见 test at regex101


2.) 要获得 "last" 或最接近的匹配而不需要太多正则表达式的努力,也可以在贪婪之后放一个 greedy dot before to ᗧ eat up. Makes sense if not matching globally / only one item to match. Use \K to reset:

(?s)^.*\K<!--.+?stylesheet.+?-->

参见 test at regex101. Also can use a capture group and grab : (?s)^.*(<!--.+?stylesheet.+?-->)


3.) 使用 lookahead 缩小范围通常成本更高:

(?s)<!--(?:(?!<!).)+?stylesheet.+?-->

参见 test at regex101(?!<!). 向前看 <!--stylesheet 之间的每个字符,如果不开始另一个 <!... 留在一个元素内。类似于取反连字符的解决方案。


而不是 .* 我使用 .+ 作为 一个或多个 - 取决于要匹配的内容。这里 + 更合适。
使用什么解决方案取决于具体要求。对于这种情况,我会使用第一个。