[译]递归正则表达式(转载)

原文在此rex译于2009年12月15~17日,翻译过程中使用的是google docs@prism@firefox@ubuntu 9.10,很爽的体验。感谢余晟老师在正则和翻译方面的悉心指导。

递归正则表达式

平时我们用到的正则表达式,其实没那么“规则”。多数编程语言所支持的扩展的正则表达式,其运算能力比起形式语言理论所定义的“规则”正则表达式要强得多。

rex注:在这里其实可以看到,如果将正则表达式译为正规表达式,一切就都通顺了:
平时我们用到的正规表达式,其实没那么“正规”。多数编程语言所支持的扩展的正规表达式,其运算能力比起正式语言理论所定义的“正规”正规表达式要强得多。

regular expression的日文是“正规表现”,在鸟哥书中,好像也将其称为正规表达式。via

例如,经常用到的捕获缓存,就是用来帮助我们临时存储任意正则表达式模式,以便重复使用。 又如,“环视断言”能让正则表达式引擎在做决定之前先偷偷看看环视一下。这些扩展让正则表达式非常强大,足以描述一些“上下文无关语法”。

Perl语言的正则表达式引擎特性异常丰富,其特征之一是懒惰正则子表达式(Lazy regular subexpressions),格式为(??{code}),其中的“code”可以是任意一段perl程序,该子表达式可能匹配时,这段程序就会执行。

我们可以利用这一特征来编写出非常有趣的东西,即将正则表达式自身嵌在它的“code”部分,由此生成递归的正则表达式(a recursive regular expression)!

一直以来,正则表达式无法匹配0n1n这种表达式,也就是由若干个0以及同等数量的1所组成的字符串。如果使用懒惰正则子表达式,这一经典问题就迎刃而解。

下面是匹配0n1n字串的perl正则表达式代码。

1
$regex = qr/0(??{$regex})?1/;

rex注:紫龙书第四章有云:“文法是比正则表达式表达能力更强的表示方法。每个可以使用正则表达式描述的构造,都可以使用文法来描述,但是反之不成立。换句话说,每个正则语言都是一个上下文无关语言,但是反之不成立。”书中交待的正则,也应该是指的常规正则表达式,而非现代语言中的扩展的正则表达式。一般使用正则表达式来构造小部件,而使用文法来组建语言框架。

此正则表达式匹配一个字符0,之后是正则表达式自身0或1次,之后是字符1。如果正则表达式自身部分不能匹配,那么它只能匹配01;如果自身部分可以匹配,则正则表达式匹配的是00($regex)?11,此时若不能匹配自身则结果是0011,若可以匹配就是000($regex)?111,……依次顺延。

下面是匹配050000150000的Perl程序:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/perl                                                                                                                         

$str = "0"x50000 . "1"x50000;
$regex = qr/0(??{$regex})*1/;

if ($str =~ /^$regex$/) {
  print "yes, it matches";
}
else {
  print "no, it doesn't match";
}

现在来看题图所示的Yo Dawg正则表达式。你先猜猜它的作用?正确答案是,它匹配(foo(bar())baz)这样完全嵌套的括号表达式(fully parenthesized expression)或((()()())())这样的平衡括号表达式(balanced parentheses)。

1
2
3
4
5
6
7
8
9
$regex = qr/
  \(                 # (1) match an open paren (                               #(1),此处匹配开括号
    (                # followed by                                                      #(2),紧接着是
      [^()]+       #   (3) one or more non-paren character            #(3),1个或多个非括号字符
    |               # OR                                                                    #(4),或
      (??{$regex})   #   (5) the regex itself                                   #(5),正则式自身
    )*              # (6) repeated zero or more times                     #(6),重复0或多次
  \)                 # (7) followed by a close paren )                         #(7),紧接着是闭括号
/x;

rex注:关于Yo Dawg图片的含义,可以参考这里。基本上是全是“Yo dawg, I herd you like X, so we put a Y in your Y so you can Z while you Z”的结构的配图文字。

构造此正则表达式的思路是这样的。对于完全嵌套的括号表达式,它的开始字符是一个开括号。这是最简单的一步,我们直接写出(上面程序中的(1))。同理,它的结束字符是闭括号,于是得到(7)。现在该动脑筋了,括号中间是什么呢?对,它可以是既不是开括号又不是闭括号的任意字符(第(3)点),也可以是另一个完全嵌套的表达式(即第(5)点)。所有的这些,既可以只匹配0次(第(3)点),以便构造最小的完全嵌套括号表达式(),也可以匹配多次来匹配较复杂的表达式。

去掉 /x 选项(即,不再使用多行风格的注释模式),可以简记为:

1
$regex = qr/\(([^()]+|(??{$regex}))*\)/;

但是切勿在正式产品中使用这一特性,它太诡异,不好把握。建议使用较稳定成熟的Text::Balanced 或 Regexp::Common模块。

rex注:对于(??{code}),perl官方的提示是:此正则表达式仅作测试使用,可能有更新而不作提示。代码执行时产生的副作用,因版本而异,运行结果或有不同,取决于正则引擎的后期优化。

最后提醒大家,在Perl 5.10中已经可以使用递归捕获缓存来替代懒惰代码子表达式了,运行结果相同。

下面是匹配0n1n的递归捕获缓存语法(?N)的实现:

1
my $rx = qr/(0(?1)*1)/;

(?1)*的含义是“匹配第一组0或多次”,这里的第一组是指整个正则表达式。

请自行动手,重写平衡括号的正则表达式,当作练习。

祝玩得开心

posted on 2011-12-25 20:21  argb  阅读(688)  评论(0)    收藏  举报

导航