看别人写的递归正则,往往会在分组后加上*号量词,即(\(\g<1>*\))*,针对于这种模式的嵌套,其实这个*是多余的,它要匹配成功,这个量词必须只能选0或1次。如果选择多于1次,那么匹配的字符串模式就变成了"((_*))" * N,更标准一点的表示方式是( "(" * n + ")" * n ) * N,当然,前面也说了,这还有无数种其他的匹配可能。
所以,在这里我不在分组的后面加*或+这样的量词。要继续刚才的讨论。
使用\g<1>?这种量词方式可以吗?当然可以,上面分析\g<1>*的时候,是说当每一轮递归时的*次数选择都是1次或0次,就能匹配无限嵌套的小括号。对于\g<1>?来说当然也可以,因为?也可以表示0或1次。
/(\(\g<1>?\))/ =~ "(" * 1 + ")" * 1 #=> 0 /(\(\g<1>?\))/ =~ "(" * 3 + ")" * 3 #=> 0 /(\(\g<1>?\))/ =~ "(" * 10 + ")" * 10 #=> 0
这两种递归正则表达式,都是符合要求的,都能匹配无限嵌套的小括号。
下面是命名捕获版本的:
/(?<var>\(\g<var>?\))/ =~ "(" * 3 + ")" * 3 #=> 0
也能直接使用\g<0>作为嵌套表达式,这时甚至可以去掉分组:
/(?<var>\(\g<0>?\))/ =~ "(" * 3 + ")" * 3 #=> 0 # 去掉分组,直接递归这种本身 /\(\g<0>?\)/ =~ "(" * 3 + ")" * 3 #=> 0
这样看上去,写递归正则好像也不难。其实嵌套模式简单的递归正则确实不难,只要理解递归的含义基本上就能写出来。再看另一个示例。
深入递归(3):写递归正则(进阶)
假设要匹配的字符串模式为:(abc(d(xy)e)fgh),其中每个括号内的字符长度任意。这似乎正是本文开头所举的例子。
这一个递归写起来其实非常非常简单:
# 为了可读性,使用了x修饰符忽略表达式内的空白符号 /\( [^()]* \g<0>* [^()]* \)/x # 匹配: reg = /\( [^()]* \g<0>* [^()]* \)/x reg =~ "(abc(d(xy)e)fgh)" #=> 0 reg =~ "(abc(d(xy)))" #=> 0 reg =~ "((()e)fgh)" #=> 0 reg =~ "((()))" #=> 0
其中\([^()]*和[^()]*\)是头和尾,中间使用\g<0>来无限嵌套头和尾。逻辑其实很简单。
相比于网上流传的版本/\( ( (?>[^()]+) | (\g<0>) )* \)/x,此处所给出的写法应该容易理解的多。
再回头扩充刚才的递归匹配需求,如果需要匹配的字符串是ab(abc(d(xy)e)fgh)df这种模式呢?另一个问题,这种字符串模式和(abc(d(xy)e)fgh)有什么区别呢?
仔细比对一下,(abc(d(xy)e)fgh)按左右括号划分配对的话,它左右刚好能够成对数:(abc (d (xy ) e) fgh)(这里用一个空格分隔,从内向外互相成对)。但ab(abc(d(xy)e)fgh)df按左右括号划分配对的话,得到的是ab( abc( d( xy )e )fgh )df,显然,它中间多了一层无法成对的内容xy。
为了写出按照这种成对划分的递归表达式,先不考虑多出来无法成对的xy这一层。那么对应的递归正则表达式为:
/[^()]* \( \g<0>* \) [^()]*/x
其中[^()]*\(是头部,\)[^()]*是尾部,中间用\g<0>*实现头尾成对的无限嵌套。
再来考虑中间多出来的无法成对的xy这部分。其实直接将这部分放在\g<0>*的左边或右边都无所谓。例如:
# 放\g<0>*的左边 /[^()]* \( [^()]* \g<0>* \) [^()]*/x # 放\g<0>*的右边 /[^()]* \( \g<0>* [^()]* \) [^()]*/x
没错,写递归的正则表达式就是这么简单粗暴。
只是,现实并不这么美好,上面将多余的无法配对的部分放在了递归表达式的左边或右边,但有时候这样是不行的。
解决多余无法成对内容的更通用方法是使用二选一的分支结构,即|结合递归表达式一起使用,参见下一小节。
深入递归(4):递归结合二选一分支
要处理上面多出的无法成对的数据,可以通过二选一结构|改写成如下更通用的方式:
/[^()]* \( \g<0>* \) [^()]* |./x
进行匹配测试:
reg = /[^()]* \( \g<0>* \) [^()]* |./xreg =~ "ab(abc(d(xy)e)fgh)df"#=> 0
当递归正则表达式结合了|提供的二选一分支功能时,|左边或右边(和\g<>相反的那一边)都可以用来提供这些"孤儿"数据。
例如,上面示例中,当递归进行到发现xy这部分是多余的时候将无法继续匹配,这时候将可以从二选一的另一个分支来匹配这个多余的数据。
但是这个二选一分支带来了一个新的问题:只要有无法匹配的,都可以去另一个分支匹配。假如右边的分支是个.,这就相当于多了一个万能箱,什么都可以从这里匹配。