内容


功能丰富的 Perl

Perl 6 语法和正则表达式

对比 Perl 6 语法和正则表达式与 Perl 5 Parse::RecDescent 模块

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 功能丰富的 Perl

敬请期待该系列的后续内容。

此内容是该系列的一部分:功能丰富的 Perl

敬请期待该系列的后续内容。

对所有 Perl 编程人员而言,Perl 6 项目是一个热门话题。Perl 一直是一门不断发展的语言,几乎从任何可以想像得到的角度都可以确定,Perl 6 确实是由 Perl 5 进化而来(不过,您也可以说它们的起源相同)。Perl 6 将运行于 Parrot 之上,Parrot 是一种通用虚拟机,不但可以加载和解释 Perl 6 字节码,还可以加载和解释其他许多语言。

不要让将来的问题困扰着您。如果您曾经用了几个月的时间来观察某个建筑物的建筑过程,您就会知道, 选好地基后要进行挖掘,金属骨架似乎总是矗立着。工人们来来往往,工作一直在进行,但是表面看到的却总是陈旧的、 丑陋的、生锈的金属。然后,若干天以后,突然间,建筑物就完成了。Perl 6 项目当前正是处于那个长期的中间阶段,表面 看到的只是生锈的金属,而工人们正在深入地下进行幕后工作。如果您想洞悉项目的进展,那么可以去查看最新的 Parrot 发行版本以及 Perl 6 每周的更新(请参阅 参考资料中的链接)。

本文将向您介绍 Perl 6 语言的语法和正则表达式,并将这些与当前可用的 Perl 5 Parse::RecDescent 模块进行对比。如果以前对 Perl 5 有所了解,熟悉 Parse::RecDescent,并且有词法分析(lexing)和句法分析(parsing)方面的经验,那么这些会对您掌握本文有很大帮助,此外,本文是为那些对 Perl 6 语法和 正则表达式感兴趣的所有 Perl 编程人员撰写的。

Perl 6 正则表达式和语法概述

首先需要声明一件事:Perl 将通过使用 :p5 修饰符来支持 Perl 5 正则表达式。对于那些对 Perl 6 正则表达式不感兴趣或者不想转到这方面来的人而言,这是一个福音。此外,Perl 6 正则表达式可能 (但不是必须)与 Perl 5 中对应的正则表达式有本质上的区别。

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

在 Perl 5 中, Regexp::Common 模块(请参阅 参考资料)已经在尝试复用正则表达式,但是,因为 Perl 5 不允许复用正则表达式,所以不得不将它们封装在一个模块接口中。 Perl 6 完全支持这种复用。

尽管您可以编写类似 Perl 5 正则表达式的模糊而晦涩的 Perl 6 正则表达式,但在默认情况下,允许启用空格注解; 所以,虽然在 Perl 5 中您可以用“hello there”本身来匹配“hello there”,但在 Perl 6 中,您必须将其改为 /hello <sp> there/。这样就可以在正则表达中将条件清晰地分离开来。

更重要的是,在语法(grammars)内部使用正则表达式时,Perl 6 正则表达式必须不那么晦涩。编程人员会发现(我希望如此, Larry Wall 也是),对清单 2 的理解与维护要比对清单 1 的容易得多:

清单 1. 没有语法的正则表达式
# note this is just a language example, not an accurate name matcher
# Perl 6 <[A-Z]> is equivalent to the Perl 5 [A-Z]
# Perl 6 :w modifier surrounds all tokens with "automagic" whitespace,
# which basically means it will match what most people would call
# "words"
$name = m:w/ <[A-Z]><[a-z]>+ <[A-Z]><[a-z]>+ /;
清单 2. 在语法中作为规则的正则表达式
# note this is just a language example, not an accurate name matcher
grammar English
{
 rule name :w { <singlename> <singlename> };
 rule singlename { <[A-Z]><[a-z]>+ };
};

清单 2 不仅更容易读懂,而且维护起来也更容易。例如,Perl 6 本身已经定义了 <upper><lower> 规则,这使事情变得更为简单:

清单 3. 在语法中作为规则的经过改进的正则表达式
# note this is just a language example, not an accurate name matcher
grammar Names
{
 rule name :w { <singlename> <singlename> };
 rule singlename { <upper><lower>+ };
};

瞧!使用 <upper><lower> 之后, 我们就复用了代码。此外,我们现在还可以处理 Unicode 名称,而这之前,我们只能局限于处理从 A 到 Z 开头 的名称。代码复用是一项出色的技术。

在进行更进一步的维护时,几乎总是需要对名称中的破折号或其他名称(比如 Don Quixote de la Mancha)进行修正(举例来说)。同 样,在将对个别规则的更改隔离出来,或者在需要时创建一个新规则的时候,您会注意到这是多么简单。

语法(Grammars) 是相当简单的概念。它们是具有专用名称空间(namespace)和专用子例程的程序包; 每一个子例程被称为一个规则。语法可以继承其他语法。这样就使得编程人员既可以复用其他人的代码,也可以编写能够 复用的代码。从 Perl 模块的 CPAN 存档文件(archive)获得的成功中可以明显地看出这种复用的价值。Perl 6 语法在规则中使用正则表达式,然后可以将这些规则用于其他规则之中。

对比 Parse::RecDescent 与 Perl 6 的语法

熟悉 Parse::RecDescent 的人都知道,它是一个功能强大的工具。 Parse::RecDescent 是一个 Perl 5 模块,只使用很少代码就可以生成非常强大的语法。这些语法与 Perl 6 的语法是否非常相似呢? 是这样的, Parse::RecDescent 的作者 Damian Conway 深入参与了 Perl 6 的许多工作。因此,很多在 Parse::RecDescent 中证明好用的思想都被应用到 Perl 6 中 也就不足为奇了。它们的一些语法有很多类似之处。

Parse::RecDescent(此后称之为 P::RD)使用 new() 模块文法来创建新的语法。每个 P::RD 语法成为 P::RD 类中的一个对象,语法中的每一个规则都可以作为用来执行动作的方法。 P::RD 语法可以将动作(action)与每一个规则关联起来,将其作为解析过程中的 一个 完整部分。在 Perl 5 本身中,解析是一个事件,而使用了扩展语法的动作则是达成目标途径中 的不幸牺牲品(roadkill),那些扩展的语法被证明是造成迷惑的罪魁祸首。这一区别使得 P::RD 比 Perl 5 正则表达式更为有效,原因在于当检测到匹配对象时,它会 使某些事情发生

Perl 6 语法吸取了 P::RD 的经验,它意识到了这些动作的实用性,现在,这些动作已经成为其首要的组成部分。每发现一个匹配对象,就会执行一个动作(代码块)。即使匹配对象的内容可能已经被修改也是如此!此外,这些动作的语法与 P::RD 中的语法同样简单。

清单 4. 包含动作的 Parse::RecDescent 语法
# small extract from my cfperl.pl program's global parser
my $parse_global = new Parse::RecDescent (q{
  input:  blank | comment | class | section
  comment: /^\s*/ '#' { 1; }
  blank: /^\s*$/ { 1; }
  section: /\w+/ ':'
   { $::current_section = $item[1];
     $::current_classes = 'any'; 1;
   }
  class: compound_class '::'
   { $::current_classes = $item{compound_class}; 1; }
  compound_class: /[-!.|\w]+/
});
$parse_global->input("TEXT GOES HERE");

上面的语法只有一个规则,即 input,它将匹配 blankcommentclass 或者 section 规则。这些规则中的每一个规则都有一个定义,它们可以是独立的或是基于另一个规则的,也可以同时具备这两种特性。

注意像普通的代码块那样封装在大括号 { } 内的动作。对于一个片断(section),动作将全局变量 $current_section 设置为正在进行匹配的片断,并重新设置 $current_classes 全局变量。对于类,动作将全局的 $current_classes 变量设置为匹配的条目。

这个语法在 Perl 6 中会是什么样的呢?

清单 5. 清单 4 语法的 Perl 6 译本
# this may be buggy - it's certainly untested
# every input is known to be one line, without newline characters
grammar Global
{
 rule input { <blank> | <comment> | <class> | <section> }
 rule blank { ^^ \s* $$ }
 rule comment { ^^ \s* \# }
 rule section
 { (\w+) \s* \:
  {
   $::current_section = $1;
   $::current_classes = 'any';
  }
 }
 rule class { (<compound_class>) \s* \:\:
  {
   $::current_classes = $1;
  }
 }
 rule compound_class { <[-!.|\w]>+ }
}

Perl 5 的正则表达式

如果您对 Perl 5 正则表达式非常熟悉,那么可以跳过这一节。

所有 Perl 5 编程人员都熟悉 Perl 5 正则表达式。在进行匹配时,要用 m// 操作符来标识这些正则表达式(有时是可选的),而当匹配并替换时,则要用 s/// 操作符来 标识它们。在特定的情况下, / 字符可以由其他字符取代,并且有一些特定 的操作符,它们与正则表达式有几分类似,不过这样的操作符不多(例如 tr///)。Perl 5 正则 表达式要指明的或者是“寻找此内容”,或者是“寻找此内容,并用另一内容取代之”。

看起来很简单,不是吗?是的,通常是这样。正则表达式中有修饰符,它们既可以在表达式内部也可以在表达式外部: 区别大小写、向前查找(lookahead)、多重匹配、忽略空格、匹配的数目等。您甚至可以重新编译一个 正则表达式来提高速度。

让我们忽略较复杂的选项,只关注正则表达式的基本语法。以下是一些示例:

清单 6. Perl 5 正则表达式示例
# 1: look for "color"
# matches "green and red are colors"
m/color/
# 2: look for "color" at the beginning of the line
# matches "color me blue" but not "this is my color"
m/^color/
# 3: look for "frog" followed by anything, followed by "jump"
# matches "the frog jumped" but not "jump, you frog"
m/frog.*jump/
# 4: look for a numeric digit, then 1 or more spaces, then another digit
# matches "671 2" but not "numbers 444, 222"
m/\d\s+\d/
# 5: save the first number seen (multiple digits)
# matches AND returns "46755332" but not "how do you do?"
m/(\d+)/
# 6: replace "wall" with "plaster" everywhere
s/wall/plaster/g
# 7: replace the FIRST number seen with N
s/\d+/N/

您要注意的第一点是,这些正则表达式都在一行上。Perl 5 正则表达式可以跨多个行,而且可以包含注释, 但是大部分编程人员并不在意这些事情。

尽管可以分为多行并且可以包含注释,但人们还是认为 Perl 5 的正则表达式很晦涩,对初级 Perl 编程人员来说,它们类似于线路噪音(line noise)。不过,这些晦涩背后隐藏着大量的信息。Damian Conway 宣称,Perl 5 正则表达式是“神秘的(arcane)、结构复杂的(baroque)、矛盾的(inconsistent)、晦涩的(obscure)”, 这说明他对它们的风格有偏见。我认为,Perl 5 的正则表达式的晦涩,也正是这门语言如此强大的原因之一, 尽管它同时还相当难以维护。对 Perl 6 来说,其挑战在于保持强大功能的同时使语法更简单。

看起来我好像在不停地介绍 Perl 5 的可读性,我这样做是有充足理由的:新的 Perl 编程人员会被 Perl 5 正则表达式吓倒。在各个 新闻组和邮件列表中,这样的情况我已经见过多次。当一门语言的 特性将它的用户吓跑时,也就是它要发生变化的时候了。

Perl 5 正则表达式所缺少的不止是可读性 —— 它们还缺少结构和复用性。这些属性通常与高层次编程结构有关, 我们将看到 Perl 6 正则表达式如何解决这三个问题。无论如何,主要的问题是可读性。

Perl 6 解析和词法分析

Perl 5 提供了非内置的解析工具,但是它没有通过正则表达式提供词法分析工具。

应该给出关于解析和词法分析的一个简短指南。从定义来看, 词法分析是将输入内容分解为有意义的单词 (也叫做词法分析记号(lexing tokens)或词位(lexemes))的动作。根据具体实现的不同,它的功能可能 会更多或更少,但是其大意是指以纯文本的形式给出一些程序(program),词法分析可以告诉我们文本中每个有意义的单词的用途和界定。

从简单的固定域到相当复杂的嵌套模式,正则表达式可以封装多种词法分析模式。尽管词法分析不是必须通过 正则表达式来完成,但是它们往往可以便捷地解决程序编制和数据语言的难题。如果人们以固定的格式编写程序 和数据,那么词法分析将非常简单 —— 但是我们不那么做。

词法分析结束后,无意义的数据流被转变为一个有意义的单词序列,提供给解析器。解析器是一种软件,它从文本获得那些 有意义的单词,并通过识别这些单词的类型与用途,从中构建解析树。

按我们自己的语言知识,这实际上非常简单。词法分析器是我们的语言知识中识别“这是一个句子; 这是标点;twenty-three 是一个单一的词”的那一部分。解析器则是识别“这个句子包含一个动词、一个主语、 一些形容词和一些代名词”的那一部分。在完成解析之后,没有意义的(对计算机来说)数据流就成为了计算机可以理解的内容。

以下是对一个句子进行词法分析和解析的示例。它完全是虚构出来的,所以请不要尝试过度地分析它,只是观察每一个分析层负责的分离操作即可。

清单 7. 词法分析和解析
Sentence:
The sky is blue, wow!
Lexer: [The] [sky] [is] [blue] [%%comma%%] [wow] [%%exclammation%%]
(Note how the punctuation was inserted with special symbols.)
Parser:
Declarative Sentence =
 Specific_Subject + Verb + Adjective + Optional_Exclammation =
  [The sky]         [is]     [blue]          [, wow!]

在这个虚构的示例中,解析器并不关心声明的实际内容,因为它与句子没什么关系。然而,它非常关心主语是特定的(“the sky”)还是非特定的(“sky”),因为这一区别会使句子的语义内容发生根本的改变。

Parse::RecDescent 模块使用 Perl 5 的正则表达式来无缝地进行词法分析 (当一个 P::RD 规则不使用其他规则来进行匹配时,它可以胜任一个词法分析器 规则),并在其上构建解析工具。因此,Perl 5 之上的 P::RD 是一个 解析器与词法分析器的强大组合。

如前所述,Perl 5、 P::RD 和 Perl 6 之间的有着深层次的联系。Perl 6 中解析与 词法分析的完成方式与上面所描述的 Perl 5 和 P::RD 的组合非常类似,这是不足为奇的。Perl 6 正则表达式得到了加强,它还可以包含其他正则表达式,这就使得即使没有语法它们也可以得到复用。例如,对于 词法分析目的而言,这意味着可以用整数的词法分析器定义来构建实数和分数的词法分析器定义。

在 Perl 6 正则表达式之上与其紧密集成的是 Perl 6 语法。类似于 P::RD,那些语法 使用简单的规则来进行词法分析,然后使用更复杂的规则来解析经过词法分析后的输入内容。因此,Perl 6 语法和正则表达式 是 Perl 社区所需要的词法分析和解析的完整解决方案,它建立在经过考验的词法分析和解析方法的基础之上,在 Perl 5 中使用 P::RD 模块时,可以看见这些方法。

其他 Perl 6 正则表达式和语法花絮

在 Perl 6 正则表达式和语法中,有 Perl 5 和 P::RD 所没有提供的其他有趣特性。

提交(commit)指令对优化解析非常有用。它们指出,在解析过程中,如果达到某个条件,则只能匹配当前规则。例如, 如果在一门语言中,在“color”之后出现的只能是“blue”,那么语法将指定(以伪代码的形式) color commit() blue,而且将不会去解析“color red”。Perl 6 语法中包含一些提交指令,它们可以指定当前的单词、选择、语法规则或者一切内容,这取决于匹配操作符找到匹配的对象之后是否将被回溯。 在 P::RD 中并没有这样细粒度(fine-grained)的控制,它只提供了 一个层次上的提交。对于较全面的语法来说,这是一个非常实用的特性。如果这看起来不易理解,那么 只需要记住 Perl 6 允许您决定何时在什么层次上提交给当前匹配即可。

有一个对应于提交指令的指令,名为 failfail 使得 Perl 6 语法规则可以因为逻辑条件而失败。使用 fail 来过滤出一个月中非法日期的 规则(不考虑实际的月份)将是 rule date {:w <month> (\d+): { fail if $1 > 31 } }(\d+) 之后的 : 告诉 Perl 6,如果“32”失败,则 不应该回溯并尝试“3”。

Perl 6 语法和正则表达式允许非分组匹配,不保存它们的结果。在 Perl 5 中,正则表达式 m/(a)(b|c)(d)/ 将返回“a”,然后是“b”或“c”,最后是“d”。 如果我们不关心“b”和“c”呢?实在不幸:在 Perl 5 中,如果不使用某些深奥而且不是永远可用的正则表达 式 ?: 修饰符,您就不能忽略选择。在 Perl 6 中,可以使用非分组匹配指示符 [b|c],这样就可以不保存“b”和“c”。这将是一个颇有价值的优化辅助手段。

Perl 6 正则表达式可以很方便地指定“这是第 N 次出现这种情况”,而在 Perl 5 中,这很不容易实现。

Perl 6 正则表达式对 Unicode 的支持比 Perl 5 中好得多。

在语法片断中可以定义并设置临时的局部语法(grammar-local)变量。这些是假定的变量,只有匹配成功 时它们才会有一个值。

还有很多...

结束语

请参阅 参考资料部分中列出的 Perl 6 在线参考资料。这对 Perl 6 来说很重要,因为它是一个进行中的项目。

我希望那些已经在 Perl 5 中使用过 Parse::RecDescent 的人能够确信,Perl 6 中的正则表达式和语法的变化远不及主版本号的改变可能带来的一些变化。对于那些还没 有使用 P::RD 的人,我希望本文能够说明它是一个值得学习的实用工具。 即使您的兴趣更多在于 Perl 6,了解一下 P::RD 也是值得的,因为它与 Perl 6 语法工具有如此多的相似之处。

最后,我希望您像我一样为 Perl 6 的特性而激动,希望您的兴趣会引导您关注 Perl 6 项目,甚至为其 做出贡献。Perl 6 是对 Perl 5 的重新编写(由某个社区),编写它的正是像你我一样的人,所以,如果 您还没有加入到这个社区中来,我希望您加入该社区。

感谢 Damian Conway 和 Luke Palmer 对本文的关注。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=49128
ArticleTitle=功能丰富的 Perl: Perl 6 语法和正则表达式
publish-date=11022004