但如果无法匹配的多余字符是右括号或左括号这个必须的字符呢?少了任何一个括号,都不再算是成对的嵌套结构,但却因为二选一分支而匹配成功。
如何解决这个问题?第一,需要保证另一分支不是万能的.;第二,需将整个结构做位置锚定。例如:
/\A ( [^()]* \( \g<1>* \) [^()]* | [^()] ) \Z/x
注意,上面加了括号分组,所以\g<0>随之改变成\g<1>,因为递归的时候并不需要将锚定也包含进来。
当然,上面示例中二选一分支的另一个分支所使用的是单字符匹配[^()],如果有多个连续的多余字符,这会导致多次选中该分支。为了减少匹配的测试次数,可以将其直接写成[^()]*。
/\A ( [^()]* \( \g<1>* \) [^()]* | [^()]* ) \Z/x
但这有可能会在匹配失败的时候导致大量的回溯,从而性能暴降。例如,如下失败的匹配:
reg = /\A([^()]* \( \g<1>* \) [^()]* | [^()]* )\Z/x # 匹配失败性能暴降 (st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st) #=> 1.7730072 (st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fghdffds") ; (Time.now - st) #=> 47.5858051 # 匹配成功则无影响 (st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st) #=> 5.9e-06
从结果发现,就这么短的字符串,第一个匹配失败竟需要花费1.8秒,第二个字符串更夸张,仅仅只是多了3个字符,耗费的时间飙升到47秒。
解决方法有很多种,这里提供两种:一种是将*号直接移到分组外,这虽然并不等价,但并不影响最终的匹配结果;另一种是将该多选分支使用固化分组或占有优先的模式。
reg1 = /\A([^()]* \( \g<1>* \) [^()]* | [^()] )*\Z/x reg2 = /\A([^()]* \( \g<1>* \) [^()]* | (?>[^()]*) )\Z/x # 匹配成功 (st=Time.now) ; (reg1 =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st) #=> 6.1e-06 (st=Time.now) ; (reg2 =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st) #=> 5.8e-06 # 匹配失败 (st=Time.now) ; (reg1 =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st) #=> 8.46e-05 (st=Time.now) ; (reg2 =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st) #=> 0.0004223
深入递归(5):小心递归中的分组捕获
在介绍示例之前,先验证一下结论。
在递归过程中,可能也会有分组捕获的表达式,所以,递归正则设置的相关变量值是最后一次分组捕获对应的状态。例如:
reg = /(abc|def) and \g<0>?xyz/ # 只递归一轮 reg =~ "abc and def and xyzxyz" #=> 0 # $~表示本次所匹配到的所有字符串 $~ #=> #<MatchData "abc and def and xyzxyz" 1:"def"> # $1表示第一个分组捕获所对应的内容 $1 #=> "def"
上面结果可以看出,在递归过程中,最后一轮的递归操作(此处示例即第一轮递归)设置了一些正则匹配时的变量,它会覆盖在它之前的递归设置的结果。
再来看一个示例。现在有个需求:匹配任何长度的回文字符串(palindrome),比如1234321、abcba、好不好、abccba、好、好好、123321,该示例只能使用二选一的分支来实现。
这里简单分析一下,如何通过递归正则来实现该需求。
假设要匹配的这个字符串是abcdcba,先把多余的字符d去掉,那么要匹配的是abccba,这也是我们想要匹配的一种字符串模式。首先,左右配对的部分必须是完全一致的数据,这个递归正则其实很容易实现,用占位符来描述,大概模式为:(.)_*\1。将其替换成递归正则表达式:
/(.) \g<0>* \1/x
再来考虑多余的那个字符,直接将其放在二选一分支的另一分支即可:因为二选一分支,所以这里的\g<0>就可以不用量词修饰来保证递归的终点
/(.) \g<0> \1 |./x
最后,加上位置锚定。
/\A ( (.) \g<1> \2|.) \Z/x
似乎已经没问题了,去测试匹配下:
/\A ( (.) \g<1> \2|.) \Z/x =~ "abcba" #=> nil
结果却并不如想象中那样成功。
不过,这个正则表达式的逻辑确实是没有问题的。例如,使用grep -P(使用PCRE)执行等价的正则去匹配回文字符串。
$ grep -P "^((.)(?1)\2|.)$" <<<"abcdcba" abcdcba # 下面的则失败 $ grep -P "^((.)(?1)\2|.)$" <<<"abcdcbad"
但是这个"正确的"正则表达式在Ruby中却无法达到目标。这是因为Ruby中的递归也会设置分组捕获,每个\2所反向引用的就不再是每轮递归中同层次的分组捕获(.)的内容了,而是真正的从左向右的第二个分组捕获括号所捕获的内容。