Pegex 语法解析模块

Tags:

Pegex 是一个语法解析框架,全称是 Parsing Expression Grammars (PEG), with Regular Expessions (Regex) 。 作者是Ingy dot net, 相信大家都听过他的另外一个模块 Inline, Inline::C, 最近Ingy的 Inline Module Supprot 项目也是得到了Perl基金会TPF(The Perl Fundation )的赞助, 相信以后的Inline会更加好用。

最近的China Perl Advent也有介绍一个语法解析模块Regexp::Grammar. 我没有用过这个模块,但看起来和Pegex的思路大致是一样的,因为其作者是Damian Conway, 所以也是很有保障的。

还是回到Pegex, 就像它的名字一样, Pegex是一个基于正则表达式来做语法解析的框架, 灵感来自于Perl6的Rules. 相比普通的正则表达式, Pegex的语法更加清晰易懂和结构化.比如我们想定义一个语法表示所有以#开头的都是注释:

comment: / HASH ANY* EOL /

这里以冒号分割定义了一个 rule 叫 comment, / / 扩起来的其实就是表示正则表达式, 只是用 HASH 来代替 #, 看起来干净一些, 以上的写法和下面的正则表示是一样的

comment: /#.*\r?\n/

这里的HASH ANY EOL也是rule,只是事先定义好的默认rule,所以可以看到,Pegex的语法定义其实就是定义一个个子rule,最后把他们串起来。而rule本质上也是正则表达式。

与perl的正则语法不同的是

在正则语句中,用尖括号括起来的是rule

/ ( <rule1> | 'non_rule' ) /

如果你想省略尖括号,可以在rule前留一个空白

/ ( rule1 | 'non_rule' ) /

正则语句默认是忽略空白的,你可以把一个表达式拆成多行来写,也就是相当于正则 /x模式 默认开启

/ (
    rule1+   # 注释
    |
    rule2
) /

用- + 来表示 \s* 和 \s+

/ - rule3 + /  # rule3 前面可以有0-多个空白, 后面有1到多个空白

在perl中任何(?XX ) 形式的语法,在这里都可以把?省略, 也就是下面两个是一样的

/ (: a | b ) /

/ (?: a | b ) /

我们再来看一个解析CSV的例子(来自Pegex::CSV 模块)

csv: row*

row:
  /(= ALL)/
  value* % /- COMMA/
  /- (: EOL | CR | EOS)/

value: /- ( double | plain) /

double: /- DOUBLE (: (: DOUBLE DOUBLE | [^ DOUBLE] )* ) DOUBLE /

plain: /- (: [^ COMMA DOUBLE CR NL]* [^ SPACE TAB COMMA DOUBLE CR NL ] )? /

第一句定义表示csv 是由零到多个row组成

第二句定义表示row 是有逗号分隔的value组成, 这里 % 是Pegex提供的一个操作符,表示用。。分隔的意思,也是来自Perl6的

第三句定义value 是一个double 或者plain

第四句定义double 是一个由双引号扩起来的字符串

第五句定义plain 是一个非逗号,双引号, 换行,空白 等等的 字符串

我们可以看到Pegex的定义本质上也是用的正则. 用DOUBLE, COMMA, EOL 这样的直白的名字来代替'",\r?\n', 用定义rule来将整个语法拆分成每个部分,在写比较复杂的正则表达式时会非常清晰,相信大家都有看一个非常复杂的正则时的头痛。

语法定义好了,我还需要定义一个接受器(Receiver), Pegex提供一个基类Pegex::Tree,供你在此基础上定义你的接收器。

下面是一个CSV的接收器,将解析过的csv转化为一个二维数组的形式(List of List)。

package Pegex::CSV::LoL;
use Pegex::Base;
extends 'Pegex::Tree';

sub got_row {
    my ($self, $got) = @_;
    $self->flatten($got);
}

sub got_value {
    $_[1] =~ s/(?:^"|"$)//g;
    $_[1] =~ s/""/"/g;
    return $_[1];
}

got_* 方法用来定义当Parser捕获了一个相应的rule后进行的操作, *对应之前在语法定义时的rule名字。

比如这里got_value 就是把得到的value的双引号去掉, flatten是将一个多维数组展平成一个一维数组。

Grammar和Receiver都定义好之后,整个工作就算完成了,可以用定义好的语法和接受器来读csv文件了。

my $parser = Pegex::Parser->new(
    grammar => Pegex::CSV::Grammar->new,
    receiver => Pegex::CSV::LoL->new,
);

$parser->parse($csv);

我现在用到Pegex地方主要是解析一些各种各样格式的数据, 以及为配置文件定义自己的语法DSL。相比之前用正则和循环来写, 一个是工作量降低, 基本是语法定义好就可以用了的节奏, Pegex为你做了语法解析的事情, 剩下的只是写Receiver得到想要的数据类型。 另一个是代码干净了许多, 后面维护起来很方便。