递归正则 (?R)

首先,为什么要有递归正则这种方法?

两个目的:一、因为我们在从网站爬数据的过程中,经常会遇到数不清的嵌套结构,如果嵌套深度是不固定的,那么除了为每一种格式写一个正则以外,有没有什么好的办法可以自动适配这种格式呢?这就用到了递归。通过循环引用去匹配不同深度,不同样式的嵌套结构。二、通过递归引用,可以大幅提升正则的简洁诚度,在正则中直接引用之前的表达式,而不用复制。

递归就是引用自身,在正则里,就是引用整个表达式(?R)或指定子组(?n)

先从最简单的递归子组开始,通过方法(?n),n是整数,这个和上一节讲的后向引用如出一辙,只有一点关键不同:

就是普通后向引用\g{n},引用的是子组的捕获值,而这里的递归引用(?n),引用的却是子组的表达式!

来看一下差别,后向引用 (sens|respons)e and \g{n}ibility 匹配 ”sense and responsibility” 和 ”response and responsibility”,但是不匹配 ”sense and responsibility”.

如果用递归引用 (sens|respons)e and (?1)ibility ,它就会匹配 ”sense and responsibility”.

OK,热身完毕,进入正题。

递归正则几个基本的使用方法:

我们必须先了解这个,才能有助于我们读懂递归:

(?R)或(?n)引用的都是表达式,而非捕获值,这个和后向引用不一样,要注意区别;

(?R)在翻译的时候,可以理解为一个”占位符“。它的位置决定了下一次递归时,捕获值的插入位置。

(?R)表示递归整个表达式,无论它在什么位置。如果表达式中有锚点(比如^$)锚点也会参与递归,这样就会造成错误;解决方案是,将(?R)改为(?n),整体引用改为指定递归子组引用。

(?R)与量词搭配使用时,量词使用不当会造成无限循环。比如,(?R)+表示的不是至少递归一次,而是表示(?R)+将始终保留在最后一轮的结果中,因为(?R)总是引用整个表达式,于是就会导致无限递归。同理(?R){3}这种表示严格递归三次的方式也是错误的,因为递归第三次后仍然保留了(?R)占位符及其量词{3},这也将无限递归。

这就牵出了我要说的第4点,递归必须有终点Ending。所以,只有量词*或?和{0}等这种能在重复次数上表示递归0次的方式才是正确的递归正则表达式方法。因为无论递归多少次,最后一次的占位符的量词都可以是0次,从而达到递归的终点,即停止递归。

递归的匹配方式,是从外往里匹配,第0次递归是最外层,第1次递归调用,往里进一层,以此类推;

ok,我举几个直观的例子直观感受一下:

表达式`(abc)(123|def)(?R)?(xyz)`可以匹配什么样的字符串?我们拆开看一下:
先进行一轮匹配,(abc)(123|def)可以匹配对应字符串abc123或abcdef,
(?R)?如果取0次,则直接跳到结尾xyz,那么abc123xyz或abcdefxyz肯定是可以匹配的,
并成功退出;
如果(?R)?取1的话,进行第一次递归,则这时表达式可以翻译成:
(abc)(123|def)(abc)(123|def)(?R)?(xyz)(xyz),
这时(?R)?可以再次取0,则表达式可以匹配abc123abcdefxyzxyz,abc123abc123xyzxyz, 
abcdefabcdefxyzxyz, abcdefabc123xyzxyz,这4个字符串,并成功退出;
同理,我们还可以把(?R)?再取1,那么实际上这个表达式可以匹配
(abc(123|def)\*n+(xyz)\*n这种特征的字符串;

总结一下,首先(?R)的位置,决定了递归时,引用表达式的插入位置;其次(?R)引用的是表达式,而不是上一轮的匹配结果;再次(?R)?必须可以取0,这样才能退出,否则会无限循环下去,导致匹配失败;最后,特征匹配字符串时,总是从外向内进行匹配;

看到这里还没放弃的,递归正则就已经掌握了60%,我们再看一些进阶的用法。

我们再来看一个带锚点的递归,表达式^\(((?R)?|[^()]+)*\)$,可以匹配(z(y(x)y)z)么? 我们直接假设(?R)?取1时,表达式变为了^\(^\(((?R)?|[^()]+)*\)$[^()]+)*\)$,表达式中出现了2对儿开始和结尾的锚点,不可能匹配任何字符串,匹配失败。

那该如何修改呢?可以将表达式改为^(\(((?1)?|[^()]+)*\))$,这里的1,引用的是(\(((?1)?|[^()]+)*\))这个子组,不包含两端的锚点。

先看一下最核心的递归子组内部((?1)?|[^()]+)*,它表明要么进行0次或1次递归,要么匹配任意个非括号字符。后面量词*表示,可以同时重复多次这样分支结构((?1)?|[^()]+)((?1)?|[^()]+)((?1)?|[^()]+)((?1)?|[^()]+)....

现在来翻译一下这个表达式是如何匹配的:

重点解释一下第6步,当时这个也困扰了我很久。我们先回忆一下(a|b)*这个表达式的意义,这个表达式等价于(a|b)(a|b)(a|b)(a|b)...

那么((?1)?|[^()])*的含义是,在同一次递归中,我们可以去多次重复这个子组,所以我们在第一次和第二次递归时,将它重复了3次,并为不同子组中的(?1)?赋予了不同的值。当?=0时只匹配字符,当?=1时做一次递归。

那么在上面例子的正则表达式3阶递归展开后可以看成(AB(AB(ABC)BC)BC)这个结构,其中B指代((?1)?[^()]) A指代(,C指代),这样就可以完美匹配(z(y(x)y)z),当然,如果再推广一下,实际上这个表达式可以匹配任意ABC结构的嵌套组合,比如(AB(AB(ABC)BC(ABC))...大家可以试一下。

另外,不要忽视()中间那个分支符号|,它给了表达式更大的灵活度,(?1)?|[^()]+可以看作a|b模式;如果没有它,我们的特征就变成了ab|b,这是完全不一样的匹配特征,匹配灵活度也降低了很多。

通过上面的例子,我想揭示的是递归子组和分支结构+量词的搭配,可以实现非常灵活的匹配方式。

总结一下,这次介绍了递归子组,与后向引用的区别,以及递归正则的基本使用方法,并通过一个案例,配合讲解了递归与分支结构,量词的组合,最后再补充一点,递归引用中(?0)表达的含义和(?R)是一样的。

感谢阅读, 抛砖引玉, 如有不准确和错误之处请留言指正, 我会及时修正, 拜谢!

欢迎喜欢研究技术的小伙伴儿和我交流, 互相学习, 共同成长:)

总结不易,请勿私自转载,否则别怪老大爷不客气

参考资料:

www.php.net 官方文档

blog.csdn.net/technofiend… 理解正则表达式中的(?R)递归 作者:Technofiend

www.cnblogs.com/f-ck-need-u… 循序渐进掌握递归正则表达式 作者:骏马金龙