Recursive Expressions
preg 引擎所属的流派的大多数方面都在第3章中有所介绍,但是此流派还提供了某些新的有意思的功能用于匹配嵌套结构:递归的表达式。
序列「(?R)」表示“在此处递归应用整个表达式”,而「(?num)」表示“在此处递归应用 num所对应编号的捕获型括号中的序列”。命名捕获的括号则使用「(?P>name)」表示法。
下面几节展示了常见的递归。递归在扩展的“tagged data”例子(☞481)中占有重要地位。
匹配嵌套括号内的文本
Matching Text with Nested Parentheses
基本的递归的例子是匹配嵌套的括号内的文本,下面是一种办法:「(?:[^()]++。
这个表达式匹配任意多个双多选分支结构。第1个多选分支「[^()]++」匹配除括号之外的任何字符。因为外面有「(?:…)*」,这个多选分支要求使用占有优先的加号,避免“无休止匹配”(☞226)。
另一个多选分支「((?R))」才是问题的关键。它匹配一对括号,其中可以包括任何字符(只要括号的嵌套是格式正确的)。这里的“之间”的部分是整个正则表达式希望匹配的内容,也就是我们可以通过「(?R)」直接递归使用整个正则表达式的原因。
这个表达式本身可以正常工作,但如果要添加任何字符,请务必小心,因为调用「(?R)」时添加的任何字符同样会递归。
如果使用这个正则表达式来校验一个括号配对不正确的字符串,你可能希望在两端加上「^…$」来确保“整个字符串”。这是不对的,因为添加的行锚点会被递归应用到整个字符串之中,导致匹配失败。
递归引用一组捕获型括号
「(?R)」结构会递归引用整个正则表达式,但也可以使用「(?num)」结构引用到其中的子集。它递归引用编号为numth的捕获型括号内的子表达式(注4)。如果用「(?num)」来思考,「(?0)」就等于「(?R)」。
我们可以使用这种部分递归因来解决前面那一节中出现的问题:在添加「^…$」之前,我们用一个捕获型括号把正则表达式的主体部分围起来,然后在以前使用「(?R)」的地方使用「(?1)」。添加捕获型括号是为了让「(?1)」能够引用,你或许还记得,这就是上一节匹配嵌套括号的表达式。「^…$ 」加在这些括号之外,这样我们就不会对它们进行递归调用:。
正则表达式中下画线的部分是第1组捕获型括号,所以每次遇到「(?1)」都会重新应用。
下面PHP代码中的正则表达式会报告$text中的括号能否配对:
通过命名捕获进行递归引用
如果需要递归调用的自表达式处于命名捕获(☞138)中,就可以使用「(?P>name)」进行递归引用,而不是之前的「(?num)」表示法。使用这个表示法,我们的例子就成了:
「^(?P<stuff> (?:[^]++|((?P>stuff)))*)$.」
这个表达式可能看起来很复杂,用模式修饰符x可以看得更清楚一点:
关于占有优先量词的补充
我会对表达式中的占有优先量词做最后的补充。如果外部的「(?:…)*」是占有优先的,内部就不必使用「[^()]++」。为了阻止这个表达式进入无休止匹配,其中之一(或者是两者)必须是占有优先的。如果不能使用占有优先量词和固化分组(☞259),则需要去掉所有的量词。
这样会降低效率,但是至少不会进入无法终止的匹配。要提高效率,可以使用第 6 章介绍的“消除循环”的技巧,得到「[^()]*(?:((?R))[^()]*)*」。
不能回溯到递归调用之内
No Backtracking Into Recursion
preg正则流派的递归语意的重要特性之一是,它会把递归结构匹配的所有内容当作固化分组括号匹配的内容(☞259)。也就是说,递归结构不会部分“交还(unmatch)”某些已经匹配的内容来实现全局匹配(而是导致整个匹配失败)。
这里说的“部分”是很重要的,因为回溯时,递归调用匹配的所有文本可以作为一个整体来交还。但不容许回溯到递归调用之内的某个状态。
匹配一组嵌套的括号
Matching a Set of Nested Parentheses
上文已经介绍过如何匹配包含规范配对括号的行,这里我会介绍匹配对称括号的办法(其中可能包含更多的嵌套括号):「((?:[^()]++|(?R))*)」。
这个表达式的组成部分与之前的一样,但是顺序安排略有不同。同样,如果你希望把它当作大的正则表达式的一部分,应该把它包括在捕获型括号中,并且修改「(?R)」来递归地引用对应的自表达式,例如「(?1)」(必须使用这组捕获型括号对应的编号)。