取势 明道 优术

一个正则表达式的故事 1,330 views

作者为 扶 凯 发表

By perlpilot

在 advent 的第十天,我们有一个故事做为礼物……

曾几何时,在比你想象的更近的时候,一个叫 Tim 的学 Perl 6 程序的学生,工作中出现了一个简单的解析相关的问题.他的老板(我们叫他 C 先生)曾问过他,解析日志文件中包含着库存信息,确保在文件内是唯一有效的行.文件中每行内是这样的:
    <part number> <quantity> <color> <description>

所以这个 Perl 6 的学生,他用熟悉正则表达式写了一个可爱的小正则表达式,可以用来找出有效的行.代码检查每行内容是这样写的:

next unless $line ~~ / ^^ \d+ \s+ \d+ \s+ \S+ \s+ \N* $$ /

使用 ~~ 的操作符的原因是因为,右侧的正则表达式会匹配左侧标量.在正则内部,^^ 是匹配行的开头,\d+ 是用来匹配一个或者多个数字(由零件编号 part number 和数量 quantity 组成的),\S+ 是用来匹配一个或者多个非空白字符,
 \N* 来匹配零个或者多个非换行符,\s+ 匹配空白之间的这些东西和 $$ 用来匹配行结束.
在 Perl 6 中,正则表达式的每个单独的部分可以使用空格来让它更具可读性,所以更加好,这个空格不会是正则的一部分只用来分隔.

但 C 先生决定最好信息的每个部分都可以从提取来验证. Tim 想了一下,“没问题,我只要使用括号来捕获”.下面就是全部需要做的:

next unless $line ~~ / ^^ (\d+) \s+ (\d+) \s+ (\S+) \s+ (\N*) $$ /

在成功的模式匹配以后,每个括号内都存着匹配到的对象本身($/),可以通过 $/[0],$/[1],$/[2] 或 $/[3].它可以通过特殊的变量 $0,$1,$2,$3访问.Tim 和他老板 C 先生都很高兴.

但随后发现了一些行中,没有从描述信息中给颜色信息分开,这些行其实也是有效的.在行中颜色信息和描述信息有个特殊的组合方式.他们总是象下面这样:

    <part number> <quantity> <description> (<color>)

在这像以前一样,可以加入包括任意数量的空格在字符中. Tim 认为,“现在这个本来简单的解析程序似乎突然更加复杂了”.幸运的是,Tim 可以找一个地方寻求帮助.他迅速登录到 irc.freenode.org,加入 #perl6 通道 并请求大家协助.有人建议他使用名字来命名他的正则表达式的各个部分,来使事情变得更容易.然后使用交替的方法来匹配这个正则表达式的最后一部分的多种可能.

首先,Tim 尝试给正则能捕获到的每个部分都加上一个名字,详细信息可以见 Perl 6 正则的纲要,下面是他所做的:

next unless $line ~~ 
    / ^^ $<product>=(\d+) \s+ $<quantity>=(\d+) \s+ $<color>=(\S+) \s+ $<description>=(\N*) $$ /

现在,成功的匹配后,每各部分都可以匹配到对象中不同的东西,通过特殊的变量 $<Product>,$<quantity>,$<color> 和 $<description>.
这比预期的更容易,让 Tim 感到非常有信心.接着,他需要补充:交替区分两种不同的有效行:

    next unless $line ~~ / ^^
        $<product>=(\d+) \s+ $<quantity>=(\d+) \s+
        [
        | $<description>=(\N*) \s+ '(' $<color>=(\S+) ')'
        | $<color>=(\S+) \s+ $<description>=(\N*)
        ]
      $$
    /

为了从正则表达式中的交替和其余部分隔离开,Tim 使用了分组括号([ and ])在要交替检查的部分.
这个分组是正则的一部分,其中像圆括号是唯一没有捕捉到 $0 的, 由于必须匹配到精确的圆括号, Tim 使用了另一个有用的 Perl6 正则表达式的优势:带引号的字符串字面匹配.因为分配给正则表达式的中 $<color> 和 $<description> 总是会包含字符串在适当部分.

Tim 非常的扬眉吐气!他展示了他的代码给 Mr.C,并表扬到 "干得好 Tim!";

然而,经过成功过后,Tim 开始以更挑剔的眼光来看他的工作.对于一些行中描述之后颜色,它有可能是 “( color)” or “(color )” or “( color )”.他目前正则表达式是正常的,但如果描述中包括的颜色的部分象前面一样时,并不是所有匹配颜色的会设置 $<color>.Tim 初步修复,通过加入更多的 \s*:

    next unless $line ~~ / ^^
        $<product>=(\d+) \s+ $<quantity>=(\d+) \s+
        [
        | $<description>=(\N*) \s+ '(' \s* $<color>=(\S+) \s* ')'
        | $<color>=(\S+) \s+ $<description>=(\N*)
        ]
      $$
    /

这运行的非常良好,但正则表达式的开始显得有点凌乱.Tim 再次使用 #perl6 来让人帮助.

这时候有个名叫 PerlJam 告诉他,“你为什么不把你的正则表达式放到 grammar 中?这可以让你分配给每片到变量来匹配对象”“ Wha?? Tim 不知道 PerlJam 讲的是什么.通过简短的交流后,Tim 了解后,并知道在哪里查看必须的相关信息后.然后感谢 PerlJam,并在次回到了程序上.这一次的正则表达式几乎消失,因为它使用了 grammar.什么是 grammar ?,看下面匹配的代码:

grammar Inventory {
    regex product { \d+ }
    regex quantity { \d+ }
    regex color { \S+ }
    regex description { \N* }
    regex TOP { ^^ <product> \s+ <quantity>  \s+
                [
                | <description> \s+ '(' \s* <color> \s*  ')'
                | <color> \s+ <description>
                ]
                $$
    }
}
# ...在来到代码开始的地方
 next unless Inventory.parse($line);

以前的正则表达式中各自的变量变成了 grammar 中的命名正则表达式.在 Perl 6 的正则表达式中的命名正则是由括在尖括号内的名称来匹配(< and >).当 Grammar.parse 调用来匹配一个标量时(会操作这特定的命名正则 TOP)行为是完全和以前一样,因为命名的正则表达相当于其它正则表达式的一部分,匹配的文本保存到匹配对象中,并引用该名称.

虽然仍然有改进的余地,Tim 和 Mr.C 对这个结果感到非常高兴.


注:默认情况下,允许启用空格注解; 所以,虽然在 Perl 5 中您可以用“hello there”本身来匹配“hello there”,但在 Perl 6 中,您必须将其改为 /hello <sp> there/.这样就可以在正则表达中将条件清晰地分离开来.

Perl 6 正则表达式可以被复用.在匹配单一的词时,复用正则表达式是很荒谬的;但在解析配置文件时,几乎必须要复用正则表达式(这取决于配置文法的复杂度、发生修改的频率等).这样性能也会高很多.

在 Perl 5 中, Regexp::Common 模块,已经在尝试复用正则表达式,但是,因为 Perl 5 不允许复用正则表达式,所以不得不将它们封装在一个模块接口中. Perl 6 完全支持这种复用.
其它参数资料:http://www.ibm.com/developerworks/cn/linux/l-cpregex.html

来了就留个评论吧! 没有评论