PHP - Manual: 一次性子组
2024-12-23
对于同时有最大值和最小值量词限制的重复项, 在匹配失败后, 紧接着会以另外一个重复次数重新评估是否能使模式匹配。 当模式的作者明确知道执行上没有问题时, 通过改变匹配的行为或者使其更早的匹配失败以阻止这种行为是很有用的。
考虑一个例子,模式 \d+foo 应用到目标行
123456bar
时:
在匹配了 6 个数字后匹配 ”foo” 时失败,通常的行为时匹配器尝试使 \d+ 只匹配 5 个数字,
只匹配 4 个数字,在最终失败之前依次进行尝试。 一次性子组提供了一种特殊的意义,
当模式的一部分得到匹配后,不再对其进行重新评估,
因此匹配器在第一次匹配 ”foo” 失败后就能立刻失败。语法符号是另外一种特殊的括号,
以 (?> 开始,比如
(?>\d+)bar
这种括号对模式的一部分提供了”锁定”,当它包含一个匹配之后, 会阻止未来模式失败后对它内部的后向回溯。后向回溯在这里失效, 其他工作照常进行。
换一种说法,如果在目标字符串中当前匹配点是锚点, 这种类型的子组匹配的字符串等同于一个独立的模式匹配。
一次性子组不是捕获子组。如上面的例子,简单而言, 就是尽其所能吃掉尽可能多的匹配字符。因此, 尽管 \d+ 和 \d+? 都会调整要匹配的数字的个数以便模式的其他部分匹配, (?>\d+) 却仅能匹配整个数字序列。
这个(语法)结构可以包含任意复杂度的字符, 也可以嵌套。
一次性子组可以和后瞻断言结合使用来指定在目标字符串末尾的有效匹配。
考虑当一个简单的模式比如
abcd$
应用到一个不匹配的长字符串上。
由于匹配时从左到右处理的,
PCRE会从目标中查找每一个 ”a” 然后查看是否紧接着会匹配模式的剩余部分。
如果模式是
^.*abcd$
,
那么初始的 .* 将首先匹配整个字符串,但是当它失败后(因为紧接着不是 ”a”),
它会回溯所有的匹配,依次吐出最后 1 个字符,倒数第 2 个字符等等。
从右向左查找整个字符串中的 ”a”, 因此,我们不能很好的退出。然而,
如果模式写作
^(?>.*)(?<=abcd)
那么它就不会回溯 .* 这一部分,
它仅仅用于匹配整个字符串。后瞻断言对字符串末尾的后四个字符做了一个测试。
如果它失败,匹配立即失败。对于长字符串,
这个模式将会带来显著的处理时间上的性能提升。
当一个模式中包含一个子组自己可以无限重复并且内部有无限重复元素时,
使用一次性子组是避免一些失败匹配消耗大量时间的唯一途径。
模式
(\D+|<\d+>)*[!?]
匹配一个不限制数目的非数字字符或由 <> 闭合的数字字符紧跟着 ! 或 ?。
当它匹配的时候,运行时快速的。然而,
如果它应用到
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
上将会在报告错误之前消耗很多时间。
这是因为字符串可以用于两种重复规则,并且需要为两种重复规则都分配进行尝试。
(示例的结尾使用 [!?] 而不是单个的字符,
是因为 PCRE 和 perl 都会对模式最后是一个单独字符时的快速报错有优化。
它们会记录最后需要匹配的单个字符,当它们没有出现在字符串中时快速报错。)
如果模式修改为
((?>\D+)|<\d+>)*[!?]
就会快速得到报错。(译注:
对于这里给出的模式,当目标字符串更长的时候,消耗时间会迅速增加,慎用。)
官方地址:https://www.php.net/manual/en/regexp.reference.onlyonce.php