编辑:我选择了ridgerunner的答案,因为它包含解决问题所需的信息。但我也想为特定问题添加一个完全充实的解决方案,以防其他人想要完全理解这个例子。你会在下面找到它。
这个问题是关于澄清php的正则表达式引擎对递归表达式的行为。 (如果你想法如何在不使用递归的php正则表达式的情况下正确匹配下面的字符串,这非常酷,但这不是问题。)
a(?:(?R)|a?)a
这是一个简单的表达式,旨在匹配字符“a”或任何内容,嵌套在字符“a”的一个或多个嵌套中。例如,aa,aaa,aaaa,aaaaa。您不需要使用递归:
aa*a
会很棒。但重点是使用递归。
以下是您可以运行的一段代码来测试我的失败模式:
<?php
$tries=array('a','aa','aaa','aaaa','aaaaa','aaaaaa');
$regex='#a(?:(?R)|a?)a#';
foreach ($tries as $try) {
echo $try." : ";
if (preg_match($regex,$try,$hit)) echo $hit[0]."<br />";
else echo 'no match<br />';
}
?>
在模式中,两个“a”构成交替。在交替中,我们要么匹配整个模式的递归(两个“a”构成交替),要么匹配字符“a”,可选地为空。
在我看来,对于“aaaa”,这应该与“aaaa”匹配。
但这是输出:
a : no match
aa : aa
aaa : aaa
aaaa : aaa
aaaaa : aaaaa
aaaaaa : aaa
有人可以解释第三和第五行输出的情况吗? 我试过追踪我想象引擎必须采取的路径,但我必须想象它是错的。为什么引擎返回“aaa”作为“aaaa”的匹配?是什么让它如此渴望?我必须以错误的顺序想象匹配的树。
我意识到了
#(?:a|a(?R)a)*#
有点作品,但我的问题是为什么其他模式没有。
谢谢堆!
答案 0 :(得分:13)
优秀(和困难)的问题!
首先,使用PCRE正则表达式引擎,(?R)
的行为类似于原子组(与Perl不同)。一旦匹配(或不匹配),递归调用内发生的匹配就是最终的(并且丢弃在递归调用中保存的所有回溯面包屑)。但是,正则表达式引擎会保存整个(?R)
表达式所匹配的内容,并且可以将其返回并尝试其他替代方法以实现整体匹配。为了描述正在发生的事情,让我们稍微改变你的例子,这样就可以更容易地谈论并跟踪每一步的匹配内容。我们不使用aaaa
作为主题文字,而是使用:abcd
。然后我们将正则表达式从'#a(?:(?R)|a?)a#'
更改为:'#.(?:(?R)|.?).#'
。正则表达式引擎匹配行为是相同的。
/.(?:(?R)|.?)./
到:"abcd"
answer = r'''
Step Depth Regex Subject Comment
1 0 .(?:(?R)|.?). abcd Dot matches "a". Advance pointers.
^ ^
2 0 .(?:(?R)|.?). abcd Try 1st alt. Recursive call (to depth 1).
^ ^
3 1 .(?:(?R)|.?). abcd Dot matches "b". Advance pointers.
^ ^
4 1 .(?:(?R)|.?). abcd Try 1st alt. Recursive call (to depth 2).
^ ^
5 2 .(?:(?R)|.?). abcd Dot matches "c". Advance pointers.
^ ^
6 2 .(?:(?R)|.?). abcd Try 1st alt. Recursive call (to depth 3).
^ ^
7 3 .(?:(?R)|.?). abcd Dot matches "d". Advance pointers.
^ ^
8 3 .(?:(?R)|.?). abcd Try 1st alt. Recursive call (to depth 4).
^ ^
9 4 .(?:(?R)|.?). abcd Dot fails to match end of string.
^ ^ DEPTH 4 (?R) FAILS. Return to step 8 depth 3.
Give back text consumed by depth 4 (?R) = ""
10 3 .(?:(?R)|.?). abcd Try 2nd alt. Optional dot matches EOS.
^ ^ Advance regex pointer.
11 3 .(?:(?R)|.?). abcd Required dot fails to match end of string.
^ ^ DEPTH 3 (?R) FAILS. Return to step 6 depth 2
Give back text consumed by depth3 (?R) = "d"
12 2 .(?:(?R)|.?). abcd Try 2nd alt. Optional dot matches "d".
^ ^ Advance pointers.
13 2 .(?:(?R)|.?). abcd Required dot fails to match end of string.
^ ^ Backtrack to step 12 depth 2
14 2 .(?:(?R)|.?). abcd Match zero "d" (give it back).
^ ^ Advance regex pointer.
15 2 .(?:(?R)|.?). abcd Dot matches "d". Advance pointers.
^ ^ DEPTH 2 (?R) SUCCEEDS.
Return to step 4 depth 1
16 1 .(?:(?R)|.?). abcd Required dot fails to match end of string.
^ ^ Backtrack to try other alternative. Give back
text consumed by depth 2 (?R) = "cd"
17 1 .(?:(?R)|.?). abcd Optional dot matches "c". Advance pointers.
^ ^
18 1 .(?:(?R)|.?). abcd Required dot matches "d". Advance pointers.
^ ^ DEPTH 1 (?R) SUCCEEDS.
Return to step 2 depth 0
19 0 .(?:(?R)|.?). abcd Required dot fails to match end of string.
^ ^ Backtrack to try other alternative. Give back
text consumed by depth 1 (?R) = "bcd"
20 0 .(?:(?R)|.?). abcd Try 2nd alt. Optional dot matches "b".
^ ^ Advance pointers.
21 0 .(?:(?R)|.?). abcd Dot matches "c". Advance pointers.
^ ^ SUCCESSFUL MATCH of "abc"
'''
正则表达式引擎没有任何问题。对于原始问题,正确匹配为abc
(或aaa
。)可以对相关的其他较长结果字符串执行类似(尽管更长)的步骤序列。
答案 1 :(得分:12)
重要说明:这描述了PHP中的递归正则表达式(使用PCRE library)。递归正则表达式在Perl本身的工作方式略有不同。
注意:这可以按照您可以概念化的顺序进行解释。正则表达式引擎向后执行此操作;它潜入基础案例并继续前进。
由于您的外部a
明确存在,它将匹配两个a
之间的a
,或两个a
之间的整个模式的先前递归匹配秒。因此,它只匹配奇数a
s(中间一加二的倍数)。
长度为3时,aaa
是当前递归的匹配模式,因此在第四次递归时,它在两个a
之间寻找a
(即aaa
)或两个a
之间的先前递归的匹配模式(即a
+ aaa
+ a
)。显然,当字符串不长时,它不能匹配五个a
,所以它可以做的最长匹配是三个。
类似的处理长度为6,因为它只能匹配“默认”aaa
或由a
包围的前一个递归匹配(即a
+ {{1 }} + aaaaa
)。
但是,它不匹配所有奇数长度。
由于您是递归匹配的,因此您只能匹配文字a
或aaa
+(上一次重复匹配)+ a
。因此,每次连续匹配的时间总是比前一次匹配长两a
,或者它会缩短并回落到a
。
长度为7(与aaa
匹配)时,前一次递归的匹配是后备aaaaaaa
。所以这一次,即使有七个aaa
s,它也只会匹配三个(a
)或五个(aaa
+ a
+ aaa
)。
当循环到更长的长度(在此示例中为80)时,请查看模式(仅显示匹配,而不是输入):
a
这里发生了什么?好吧,我会告诉你的! : - )
当递归匹配比输入字符串长一个字符时,它会回到no match
aa
aaa
aaa
aaaaa
aaa
aaaaa
aaaaaaa
aaaaaaaaa
aaa
aaaaa
aaaaaaa
aaaaaaaaa
aaaaaaaaaaa
aaaaaaaaaaaaa
aaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaa
aaa
aaaaa
aaaaaaa
aaaaaaaaa
aaaaaaaaaaa
aaaaaaaaaaaaa
aaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaa
aaaaa
aaaaaaa
aaaaaaaaa
aaaaaaaaaaa
aaaaaaaaaaaaa
aaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaa
aaaaa
aaaaaaa
aaaaaaaaa
aaaaaaaaaaa
aaaaaaaaaaaaa
aaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaa
,正如我们所见。在此之后的每次迭代中,模式开始匹配比前一个匹配多两个字符。每次迭代,输入的长度都会增加1,但匹配的长度会增加2。当匹配大小最终捕获并超过输入字符串的长度时,它会回到aaa
。等等。
或者查看,在这里我们可以看到输入与每次迭代中的匹配长度相比有多少个字符:
aaa
由于现在应该有意义的原因,这发生在2的倍数。
我略微简化了这个例子的原始模式。记住这一点。我们将回到它。
(input len.) - (match len.) = (difference)
1 - 0 = 1
2 - 2 = 0
3 - 3 = 0
4 - 3 = 1
5 - 5 = 0
6 - 3 = 3
7 - 5 = 2
8 - 7 = 1
9 - 9 = 0
10 - 3 = 7
11 - 5 = 6
12 - 7 = 5
13 - 9 = 4
14 - 11 = 3
15 - 13 = 2
16 - 15 = 1
17 - 17 = 0
18 - 3 = 15
19 - 5 = 14
20 - 7 = 13
21 - 9 = 12
22 - 11 = 11
23 - 13 = 10
24 - 15 = 9
25 - 17 = 8
26 - 19 = 7
27 - 21 = 6
28 - 23 = 5
29 - 25 = 4
30 - 27 = 3
31 - 29 = 2
32 - 31 = 1
33 - 33 = 0
34 - 3 = 31
35 - 5 = 30
36 - 7 = 29
37 - 9 = 28
38 - 11 = 27
39 - 13 = 26
40 - 15 = 25
41 - 17 = 24
42 - 19 = 23
43 - 21 = 22
44 - 23 = 21
45 - 25 = 20
46 - 27 = 19
47 - 29 = 18
48 - 31 = 17
49 - 33 = 16
50 - 35 = 15
51 - 37 = 14
52 - 39 = 13
53 - 41 = 12
54 - 43 = 11
55 - 45 = 10
56 - 47 = 9
57 - 49 = 8
58 - 51 = 7
59 - 53 = 6
60 - 55 = 5
61 - 57 = 4
62 - 59 = 3
63 - 61 = 2
64 - 63 = 1
65 - 65 = 0
66 - 3 = 63
67 - 5 = 62
68 - 7 = 61
69 - 9 = 60
70 - 11 = 59
71 - 13 = 58
72 - 15 = 57
73 - 17 = 56
74 - 19 = 55
75 - 21 = 54
76 - 23 = 53
77 - 25 = 52
78 - 27 = 51
79 - 29 = 50
80 - 31 = 49
作者Jeffrey Friedl所指的“(?R)构造对整个正则表达式进行递归引用”是正则表达式引擎将替换整个模式代替{{1尽可能多次。
a((?R)|a)a
当手工追踪时,你可以从内到外工作。在(?R)
中,a((?R)|a)a # this
a((a((?R)|a)a)|a)a # becomes this
a((a((a((?R)|a)a)|a)a)|a)a # becomes this
# and so on...
是您的基本情况。所以我们将从那开始。
(?R)|a
如果匹配输入字符串,请将该匹配项(a
)恢复为原始表达式,并将其替换为a(a)a
。
aaa
如果输入字符串与我们的递归值匹配,则将匹配((?R)
)替换回原始表达式再次递归。
a(aaa|a)a
重复,直到使用上一次递归的结果无法匹配您的输入。
<强> 实施例 强>
输入:aaaaa
正则表达式:a(aaaaa|a)a
从基础案例aaaaaa
开始
输入是否与此值匹配?是的:a((?R)|a)a
通过将aaa
放入原始表达式来递归:
aaa
输入是否与我们的递归值匹配?是的:aaa
通过将a(aaa|a)a
放入原始表达式来递归:
aaaaa
输入是否与我们的递归值匹配?否:aaaaa
然后我们就到此为止。上面的表达式可以重写(为简单起见):
a(aaaaa|a)a
由于它与aaaaaaa
不匹配,因此必须与aaaaaaa|aaa
匹配。我们完成了,aaaaaaa
是最终结果。
答案 2 :(得分:4)
好的,我终于有了。
我给了ridgerunner正确的答案,因为他让我走上解决方案的道路,但我也想写一个完整的答案,以防其他人想要完全理解这个例子。
首先是解决方案,然后是一些注释。
以下是引擎遵循的步骤的摘要。应从上到下阅读这些步骤。它们没有编号。递归深度显示在左列中,从零上升到for,然后下降到零。为方便起见,表达式显示在右上角。为了便于阅读,匹配的“a”显示在字符串中的位置(显示在最顶部)。
STRING EXPRESSION
a a a a a(?:(?R|a?))a
Depth Match Token
0 a first a from depth 0. Next step in the expression: depth 1.
1 a first a from depth 1. Next step in the expression: depth 2.
2 a first a from depth 2. Next step in the expression: depth 3.
3 a first a from depth 3. Next step in the expression: depth 4.
4 depth 4 fails to match anything. Back to depth 3 @ alternation.
3 depth 3 fails to match rest of expression, back to depth 2
2 a a depth 2 completes as a/empty/a, back to depth 1
1 a[a a] a/[detph 2]a fails to complete, discard depth 2, back to alternation
1 a first a from depth 1
1 a a a from alternation
1 a a a depth 1 completes, back to depth 0
0 a[a a a] depth 0 fails to complete, discard depth 1, back to alternation
0 a first a from depth 0
0 a a a from alternation
0 a a a expression ends with successful match
<强> 1。混乱的根源
对我来说,这是违反直觉的。
我们正在尝试匹配a a a a
我假设递归的深度0将匹配为 - - a 并且深度1将匹配为 - a a -
但实际上深度1首先匹配为 - a a a
所以深度0无处可去完成比赛:
a [D1: a a a]
......那又怎样?我们没有人物,但表达还没有结束。
因此深度1被丢弃。请注意,通过回馈字符不会再次尝试深度1,这会导致我们进行不同的深度匹配 - a a -
那是因为递归匹配是原子的。一旦深度匹配,它就是全部或全部,你保持全部或全部丢弃它。
一旦深度1被丢弃,深度0移动到交替的另一侧,并返回匹配: a a a
<强> 2。清晰度来源
最能帮助我的是ridgerunner给出的榜样。在他的例子中,他展示了如何追踪引擎的路径,这正是我想要理解的。
按照这种方法,我为我们的具体例子追踪了引擎的完整路径。 就像我拥有的那样,路径长25步,所以它比上面的摘要长得多。但总结对我追踪的路径是准确的。
非常感谢所有贡献的人,特别是Wiseguy的一个非常有趣的演讲。我仍然想知道我是否会遗漏某些东西而且Wiseguy的回答可能相同!
答案 3 :(得分:-5)
经过大量实验后,我认为PHP正则表达式引擎已被破坏。 Perl 下完全相同的代码可以正常工作,并且可以像我期望的那样从头到尾匹配所有字符串。
递归正则表达式很难想象,但它看起来好像/a(?:(?R)|a?)a/
应该与aaaa
匹配a
.. a
对包含第二个a
1}} .. a
对,之后第二次递归失败,而alternate / a?/匹配为空字符串。