用 Perl 模块进行解析
Perl 是用于文本分析的一种出色语言。内置的操作符使得文本搜索、替换和模式匹配轻而易举。程序员在学习 Perl 时,常常会试着编写一些自己的例程来解析文本和数据。幸运的是,CPAN(综合 Perl 档案网络(Comprehensive Perl Archive Network);请参阅 参考资料)汇集了大量模块,有些模块把您从文本和数据分析的困境中解救出来。
将 Perl 模块用于解析、记载和分析
Damian Conway 开发的 Parse::RecDescent 是一个对文本进行记载和解析的功能强大的工具。Kim Ryan 开发的 Lingua::EN::Fathom 可以分析一个文件或一个文本块,并产生有关其输入的各种统计信息。请参阅 参考资料,获取这两个工具。
Parse::RecDescent 的缺点是:由于它使用可扩展文法规则并且实时地进行记载和解析,所以比较慢。如果没有正确使用该模块,性能就会降低。优点是:Parse::RecDescent 擅长记载和解析。下一章演示一个移植到 Parse::RecDescent 文法中的记载文法。记载总是比任何其它工具更好地执行任务。虽然 chef.pl 脚本(请参阅 参考资料)比用 chef.1 记载文法编译的 C 程序运行得慢些,但它做得更多。请确保您了解自己的工具并将适当的工具用于作业。
改编现有的记载文法
John Hagerman 的 Swedish Chef 记载文法是出色的简单文本过滤器示例。它还非常有趣,会给许多计算机科学与工程专业的学生在毕业前夕带来欢乐。我将展示一个使用 Parse::RecDescent 模块将 chef.l 文法移植到 Perl 中的示例(Parse::RecDescent 模块并不是实现这一任务的理想选择 ― Parse::Lex 模块会更好些)。这一节只准备介绍构建 Parse::RecDescent 语法的规则,将包括操作、记忆状态、拒绝产品和对文本进行记载。请记住,自己试一试 chef.pl 脚本 ― 您很可能会对此着迷。
chef.pl 脚本几乎是 chef.l 记载文法完全一样的副本。 $niw 变量在启动时设置为 0,因为许多规则测试它来判断它们应该被接受还是被拒绝。 $niw 表示“不在文字中(not in word)”,而当解析器在文字内时,它设置为 1。如果 Parse::RecDescent 的伪指令中指定的变量非零,则该伪指令会拒绝该规则。因此,请牢记 $niw = 0 意味着解析器不在文字内。
skip 变量设置为 '' (空字符串),所以所有输入(包括空格)都转至标志伪指令。此外,chef 规则以 \z 结束,\z 表示字符串的结束。通常使用 \Z ,但那还可以匹配 Perl 中的换行,它们也都可以在输入中。
chef 规则:文法以 chef 规则开始。chef 规则匹配许多标志,直至表示字符串结束的 \z 。chef 规则的那两个元素称为“产品”。任何规则都必须由产品组成。操作可以是产品的一部分;它由花括号 {} 标出,并包含 Perl 代码。它不匹配任何事物 ― 操作仅用于执行。
token 规则:token 规则可以匹配任何数或序列,这些数和序列是我为匹配 chef.1 文法而指定的(有些随意)。我将说明一些示例,以便使文法对应清晰。
一个文字/非文字字符的基本文法定义:
chef.pl: WC: /[A-Za-z']/
chef.pl: NW: /[^A-Za-z']/
chef.l: WC [A-Za-z']
chef.l: NW [^A-Za-z']
an 规则:最简单的规则不取决于任何情况。an 规则是一个很好的示例:每次它看到 “an” ,就打印 “un” 。而且,它将 $niw 设置为 1(记住,这意味着在文字内)。
chef.pl: an: /an/ { $niw = 1; print 'un' }
chef.l: "an" { BEGIN INW; printf("un"); }
ax 规则:下一个更复杂的规则是 ax 规则。它表明:如果出现一个 “a” ,而且后跟一个文字字符 WC ,就打印 “e” 。 ...WC 产品语法意味着,文字字符必须跟在 a 之后,但不在匹配中使用。因此,通过使用 an 和 ax 规则, “aan” 会产生 “eun” 。规则将 $niw 设置为 1(在文字内)。
chef.pl: ax: /a/ ...WC { $niw = 1; print "e" }
chef.l: "a"/{WC} { BEGIN INW; printf("e"); }
en 规则:en 规则的作用完全象 ax 规则,但希望后跟 NW (非文字)产品。这意味着 “en” 必须在文字结尾。
chef.pl: en: /en/ ...NW { $niw = 1; print "ee" } chef.l: "en"/{NW} { BEGIN INW; printf("ee"); }
ew 规则:仅当在文字内,ew 规则才成功。也就是说如果 $niw 为 0,您就拒绝它。
chef.pl: ew: /ew/ { $niw = 1; print "oo" } chef.l: "ew" { BEGIN INW; printf("oo"); }
i 规则:仅当在文字内,而且尚未看到另一个 i ,i 规则才会成功。它将 $i_seen 增加为 1,仅当看到非文字字符或换行时, $i_seen 才设置回 0。
chef.pl: i: /i/ { $niw=1;$i_seen=1; print "ee" } chef.l: "i" { BEGIN INW; printf(i_seen++ ? "i" : "ee"); }
句结束(end of sectence)规则:将打印任意数量后跟空格的句结束标记符 [.!?] ,再跟着打印著名的(或臭名昭著的,随您喜欢)“ Bork Bork Bork! ”消息。实际行为会与初始的 chef 过滤器略有偏差,仅仅是因为我更喜欢那样(一个人可能永远都不会有足够多的 Bork 消息)。 $item[1] 语法意味着不会匹配空格,因为它们对 Parse::RecDescent 而言是 $item[2] 。
chef.pl: end_of_sentence: /[.?!]+/ /\s+/ { $niw = 0; $i_seen = 0; print $item[1] . "\nBork Bork Bork!\n" } chef.l: [.!?]$ { BEGIN NIW; i_seen = 0; printf("%c\nBork Bork Bork!", yytext[0]); }
动态扩展文法
extend-grammar.pl 脚本是从与 Parse::RecDescent 模块一起提供的 demo_selfmod.pl 脚本发展而来的,它演示了可扩展文法。最初,文法只由一个恰当的名称 Ted(不太谦虚地说,这是个很棒的名称)组成。name 规则匹配 1 到 n 个文字字符(根据 Perl 的 \w 语法 )。 do_you_know 规则匹配由任意数目的空格分隔、任意大小写组合的“do you know”。这就是 Perl i 匹配修饰符派上用场的地方,这样您就可以以简单的方式表达简单概念。
proper_name: /Ted/ name: /\w+/ do_you_know: /do/i /you/i /know/i
扩展文法处理(extend-grammar process)规则:这个 process 规则可以由一个查询或定义组成。除非它找到一个查询或定义,否则它会都在主循环中触发消息。
process: query | definition ... and later ... while (<>) { $parse->process($_) or print "Enter a query (do you know ...)" "or a definition (... exists)\n"; }
扩展文法查询(extend-grammar query)规则:这个 query 规则由 do_you_know 产品组成,后跟一个名称或恰当的名称。对于名称,操作是打印它未知的消息。对于恰当的名称(由 proper_name 规则定义的),操作是打印出已知消息。同名的两个规则等价于一个带有两个可选择产品的规则,所以:
query: do_you_know proper_name { print "I know " . $item{proper_name} . ",sure!\n" } query: do_you_know name { print $item{name} . " does not exist in my little world", ", sorry.\n" }
等价于:
query: do_you_know proper_name { print "I know " . $item{proper_name} . ",sure!\n" } | do_you_know name { print $item{name} . " does not exist in my little world", ", sorry.\n" }
扩展文法定义(extend-grammar definition)规则:该可扩展文法的核心是 definition 规则。如果名称后面跟着 “exists” ,那么操作将用 proper_name 的新规则扩展解析器。在文法运行时,可以修改这一正在执行的特别的文法。
definition: name /exists/i { $thisparser->Extend("proper_name: '$item{name}'"); print "\"$item{name}\" is now a valid proper name\n"; }
$thisparser 是对正在执行文法操作的解析器的引用。除了 Extend,您也可以使用 Replace 方法(例如,想想 C 中的 #ifdef 语句)来更改规则的内容。
要执行 extend-grammar.pl(请参阅 参考资料中的完整清单),只要运行它并输入“ do you know ”或“ exists ”即可。任何其它内容都不会匹配解析器的处理规则,因而会被拒绝。proper_name 规则以“Ted”(一个已知恰当的名称)开始。
分析 C/C++ 源代码注释以提高可读性
stat-comments.pl 脚本(请参阅 参考资料中的清单)将 demo_decomment.pl 脚本的文法用于解析 C/C++ 代码以抽取注释。demo_decomment.pl 脚本与 Parse::RecDescent 模块一起提供。
另外,stat-comments.pl 脚本使用 Lingua::EN::Fathom 模块来分析由 Parse::RecDescent 文法解析出的注释。
首先,stat-comments.pl 创建文法(在脚本末尾的 BEGIN 块中的 $Grammar 变量中)。文法的程序规则返回散列引用,其中包含作为文本的代码、注释和字符串。有关其余的文法,请参阅 Parse::RecDescent 文档。(C 解析文法也与 Java 程序的源代码一起使用。)
然后,stat-comments.pl 读入文本输入或脚本末尾处提供的样本数据,并进行检查以判断注释是否被完全获取。将 $/ 设置为 undef 的作用是将所有输入(包括换行)立刻读入 $text 中。
输入循环,并检查注释是否可以进行 undef $/;
my $text = @ARGV ? <> : ; my $parts = $parser->program($text) or die "malformed C program"; # only work with comments of length > 0 die "No comments found in input" unless length $parts->{comments};
接着,stat-comments.pl 将注释标志转换成句点,这样就用句点分隔注释。这样做只是为了整洁,不会对最终的统计信息产生很大的影响。最后,创建 Lingua::EN::Fathom 对象,并将封装的文本块(请参阅 Text::Wrap)传递给它,供其分析。然后,打印出报告。
stat-comments.pl 的结尾:
$parts->{comments} =~ s#//#. #g; $parts->{comments} =~ s#/\*#. #g; $parts->{comments} =~ s#\*/#. #g;
# we can now evaluate the comments (stored in $parts->{comments})
my $fathom = new Lingua::EN::Fathom;
$fathom->analyse_block(wrap('', '', $parts->{comments}));
# voila, the readability report!
print($fathom->report);
stat-comments.pl 脚本的潜在应用是在代码质量控制方面。文档编制良好的程序更易于维护,这是众所周知的事实,而且许多组织都非常想这样做。stat-comments.pl 脚本不会区分好注释和差注释。然而,它的确会告诉代码管理员,某个程序员的注释是极其简洁的、异常冗长的还是对非编程人员来说是很难理解的。自己尝试一下:在带有以“ We should raise... ”开头的长注释块和不带这一长注释块这两种情况下,运行 stat-comments.pl,并查看统计信息的差异。显然,软件项目经理会有效地使用这些统计信息。
stat-comments.pl 脚本是一个有价值的软件项目管理工具(但仅在正确使用时)。就象函数点计数、每天的代码行数以及许多其它可供软件项目经理使用的统计信息一样,stat-comments.pl 产生的统计信息必须看作是现实世界的补充,而不是其基本要素。许多编程编得很好的程序员写注释不行,而编得不好的程序员能写出出色的注释。重要的是了解他们的工作方法,并认识到其中的模式和变化。
最后说明
目前可用的 Perl 模块可以使任何解析任务更加容易。除了 Parse::RecDescent 之外,还有 Parse::Lex、Parse::CLex 和 Parse::Yapp,它们都可以从 CPAN 获得。查看它们,并研究哪一个最适合于您的情况。在这个列表中,Parse::RecDescent 是最灵活和功能最强大的模块。
如需很好使用 Parse::RecDescent 的示例,请参阅 Abigail 的 CPAN RFC::RFC822::Address。没有 Parse::RecDescent,即使利用 Perl 正则表达式的功能,也几乎不可能对 RFC 822 电子邮件地址进行语法验证。
如果尝试创建自己的工具,那么很难进行文本分析。CPAN Lingua 模块可以在任何文本分析任务中为您提供功能和灵活性。请访问 CPAN 网站(请参阅 参考资料),获取更多其它 Lingua 模块。
本文展示了可以如何有效地重用 Perl 代码。与 Parse::RecDescent 模块一起提供的演示脚本提供了在此介绍的三个脚本中两个脚本的基础。感谢 Damian Conway 编写了 Parse::RecDescent 模块,Helmut Jarausch 编写了 demo_decomment.pl 脚本,Kim Ryan 编写了出色的 Lingua::EN::Fathom,以及 John Hagerman 编写了初始的 chef 过滤器文法。