正则表达式的注意事项和处理方式

04-13Ctrl+D 收藏本站

关灯 直达底部

Care and Handling of Regular Expressions

本章开头列出的第二点需要注意的就是:正则表达式的句法规则(syntactic packaging),它告诉应用程序:“嘿,这儿有一个正则表达式,我需要你做这些”。egrep是一个简单的例子,因为正则表达式是作为命令行参数传过去的。其他的“语法诀窍(syntactic sugar)”,例如我在第1章坚持使用的单引号,是因为考虑到shell,而不是egrep。复杂的系统,例如程序设计语言中的正则表达式,需要更多的包装,系统才能知道哪些部分是正则表达式,需要如何处理。

下一步是考察我们能够对匹配结果进行的操作。同样,egrep在这方面很简单,因为它做的都是同样的事情(显示包含匹配文本的行),但是,我们在前一章的开头已经说过,真正有意义的是更复杂的操作。其中最基本的是匹配(检查一个正则表达式是否能匹配一个字符串,或者从字符串中提取信息),以及查找和替换,根据匹配的结果修改字符串。这些操作可以以多种形式进行,不同的语言对此也有不同的规定。

一般来说,程序设计语言有 3 种处理正则表达式的方式:集成式(integrated)、程序式(procedural)和面向对象式(object-oriented)。在第一种方式中,正则表达式是直接内建在语言之中的,Perl就是如此。但是在其他两种方式中,正则表达式不属于语言的低级语法。相反,普通的函数接收普通的字符串,把它们作为正则表达式进行处理。由不同的函数进行不同的、关系到一个或多个正则表达式的操作。大多数语言(不包括Perl)采用的都是这两种方式之一,包括Java、.NET、Tcl、Python、PHP、Emacs、lisp和Ruby。

集成式处理

Integrated Handling

我们已经看过Perl的集成式处理方法,例如第55页的例子:

为清楚起见,我用斜体标注变量名,正则表达式相关的部分则用粗体标注,正则表达式本身用下画线标注。Perl 会把正则表达式「^Subject:·(.*)」应用到$line 保存的文本中,如果能够匹配,则执行下面的程序段。其中,变量$1代表括号内的子表达式匹配的文本,将它们赋值给$subject。

另一个集成式处理的例子是把正则表达式作为配置文件的一部分,例如 procmail(Unix 下的一个邮件处理程序)。在配置文件中,正则表达式用于将邮件信息发布到对应的处理程序中。这个例子比Perl更简单,因为不需要指明操作对象(邮件信息)。

这两个例子背后的原理要复杂一些。集成式处理方法减轻了程序员的负担,因为它隐藏了一些工作,例如正则表达式的预处理,准备匹配,应用正则表达式,返回结果。省略这些操作减轻了常见任务的完成难度,不过我们之后将会看到,有些情况下,这样处理反而更慢,更复杂。

不过,在深入细节之前,我们先打量打量其他的处理方式,然后再来揭示这些被隐藏的步骤。

程序式处理和面向对象式处理

Procedural and Object-Oriented Handling

程序式处理和面向对象式处理非常相似。这两种方式下,正则功能不是由内建的操作符来提供,而是由普通函数(函数式)或构造函数及方法(面向对象式)来提供的。这种情况下,并没有专属于正则表达式的操作符,只有平常的字符串,普通的函数、构造函数和方法把这些字符串作为正则表达式来处理。

下面几节给出了几个Java、VB.NET、PHP和Python的例子。

Java中的正则处理

现在来看“Subject”例子在Java中的实现方式,使用Sun提供的java.util.regex包(第8章详细介绍Java)。

我仍然用斜体标注变量名,粗体标注正则表达式相关的元素,下画线标注正则表达式本身。准确地说,是用下画线标注表示作为正则表达式处理的普通的字符串。

这个类说明了面向对象式处理方法,它使用Sun提供的java.util.regex包的两个类——Pattern和Matcher。其中执行的操作有:

∂ 检查正则表达式,将它编译为能进行不区分大小匹配的内部形式(internal form),得到一个“Pattern”对象。

● 将它与欲匹配的文本联系起来,得到一个“Matcher”对象。

÷应用这个正则表达式,检查之前与之建立联系的文本,是否存在匹配,返回结果。

≠ 如果存在匹配,提取第一个捕获括号内的子表达式匹配的文本。

任何使用正则表达式的语言都需要进行这些操作,或是显式的(explicitly)或是隐式的(implicitly)。Perl隐藏了大多数细节,Java的实现方式则暴露这些细节。

函数式处理的例子。不过,Java 也提供了一些函数式处理的“便捷函数(convenience functions)”来节省工作量。用户不再需要首先声称一个正则表达式对象,然后使用该对象的方法来操作。下面的静态函数提供了临时对象,执行完之后,这些对象就会被自动抛弃。这个例子用来说明Pattern.matches(…)函数:

这个函数包装了一个隐式的「^…$」的正则表达式,返回一个Boolean值,说明它是否能够匹配输入的字符串。Sun的package同时提供程序式和面向对象式的处理方式是常见的做法。两种接口的差别在于便捷程度(程序式处理方式在完成简单任务时更容易,但处理复杂任务则很麻烦)、功能(程序式处理方式的功能和选项通常比对应的面向对象式的要少)和效率(在任何情况下,两类处理方式的效率都不同——第6章详细论述这个问题)。

Sun 有时也会把正则表达式整合到 Java 的其他部分,例如上面的例子可以使用 string 类的matches功能来完成:

同样,这种办法不如合理使用面向对象的程序有效率,所以不适宜在对时间要求很高的循环中使用,但是“随手(casual)”用起来非常方便。

VB和.NET语言中的正则处理

尽管所有的正则引擎都能执行同样的基本操作,但即使是采用同样方法的各种实现方式(implementation)提供给程序员完成的任务,以及使用服务的方式也各有不同。下面是VB.NET中的“Subject”例子(.NET在第9章详细论述):

总的来说,它很类似Java的例子,只是.NET将第●和第÷步结合为一步,第≠步需要一个确定的值。为什么会有这样的差异?两者并没有本质上的优劣之分——只是开发人员采用了自己当时觉得最好的方式(稍后我们会看到这点)。

.NET同样提供了若干程序式处理的函数。下面的代码用于判断空行:

Java 的 Pattern.matches函数会自动在正则表达式两端添加「^…$」,微软则提供了更为一般的函数。Java的做法只是对核心对象的简单包装,但程序员需要使用的字符和变量更少,而代价只是一点点性能下降。

PHP中的正则处理

下面是使用PHP的preg套件中的正则表达式函数处理「Subject」的例子,这是纯粹的函数式方法(第10章详细介绍PHP)。

Python中的正则处理

最后我们来看Python中「Subject」的例子,Python采用的也是面向对象式的办法。

这个例子与我们之前看过的非常类似。

差异从何而来

为什么不同的语言采用不同的办法呢?可能有语言本身的原因,不过最重要的因素还是正则软件包的开发人员的思维和技术水准。举例来说,Java 有许多正则表达式包,因为这些作者都希望提供 Sun 未提供的功能。每个包都有自己的强项和弱项,不过有趣的是,每个软件包的功能设定都不一样,所以Sun最终决定自己提供正则表达式包。

另一个关于这种差异的例子是 PHP,PHP 包含了三种完全独立的正则引擎,每一种都对应一套自己的函数。PHP 的开发人员在开发过程中,因为对原有的功能不满意,添加新的软件包和对应的接口函数套件来升级 PHP 核心(一般认为,本书讲解的“preg”套件是最优秀的)。

查找和替换

A Search-and-Replace Example

“Subject”的例子太简单,还不足以说明3种方法之间的差异。在本节我们将看到更复杂的例子,它进一步揭示了不同处理方式在设计上的差异。

在前一章,我们看到了在Perl中利用查找和替换将E-mail地址转换为超链接的例子(☞73):

Perl的查找和替换操作符是“原地生效”的,也就是说,替换会在目标变量上进行。其他大多数语言的替换都是在目标文本的副本上进行的。如果不需要修改原变量,这样操作就很方便,不过如果需要修改原变量,就得把替换结果回传给原变量。下面给出了一些例子。

Java中的查找和替换

下面是使用Sun提供的java.util.regex进行查找-替换的例子:

请注意,字符串中的每个‘’都必须转义为‘\\’,所以,如果我们像本例中一样用文本字符串来生成正则表达式,「w」就必须写成‘\\w’。在调试时,System.out.println(r.pattern())可以显示正则函数确切接收到的正则表达式。我在这个正则表达式中包括换行符的原因是,这样看起来很清楚。另一个原因是,每个#引入一段注释,直到该行结束,所以,为了约束注释,必须设定某些换行符。

Perl使用/g、/i、/x之类的符号来表示特殊的条件(这些修饰符分别代表全局替换、不区分大小写和宽松排列模式☞135),java.util.regex则使用不同的函数(replaceAll而不是replace),以及给函数传递不同的标志位(flag)参数(例如Pattern.CASE_INSENSITIVE和Pattern.COMMENTS)来实现。

VB.NET中的查找和替换

VB.NET的程序与Java的类似:

因为VB.NET的字符串文字(literal)不便于操作(它们不能跨越多行,也很难在其中加入换行符),长一点的正则表达式使用起来不如其他语言方便。另一方面,因为‘’不是VB.NET中的字符串的元字符,这个表达式看起来要更清楚些。双引号是 VB.NET 字符串中的元字符,为了表示这个字符,我们必须使用两个紧挨着的双引号。

PHP中的查找和替换

下面是PHP中的查找和替换的例子:

就像Java 和VB.NET 一样,查找和替换操作的结果必须回传给$text,除去这一点,这个例子和Perl的很相似。

其他语言中的查找和替换

Search and Replace in Other Languages

下面我们简要看看其他传统工具软件和语言中查找和替换的例子。

Awk

Awk 使用的是集成式处理方法,/regex/,来匹配当前的输入行,使用“var~…”来匹配其他数据。你可以在 Perl 中看到这种匹配表示法的影子(不过,Perl 的替换操作符模仿的是sed)。Awk的早期版本不支持正则表达式替换,不过现在的版本提供了sub(…)操作符。

sub(/mizpel/,"misspell")

它会把正则表达式「mizpel」应用到当前行,将第一个匹配替换为 misspel。请注意,在Perl(和sed)中的对应做法是s/mizpel/misspell/。

如果要对该行的所有匹配文本进行替换,Awk使用的不是/g修饰符,而是另一个运算符:gsub(/mizpel/,“misspell”)。

Tcl

Tcl采用的是程序式处理方法,对不熟悉Tcl引用惯例(quoting conventions)的人来说可能很迷惑。如果我们要在Tcl中修正错误的拼写,可以这样:

它会检查变量var中的字符串,把「mizpel」的第一处匹配替换为misspell,把替换后的字符串存入变量 newvar(这个变量并没有以$开头)。Tcl 接收的第一个参数是正则表达式,第二个参数是目标字符串,第三个是 replacement 字符串,第四个是目标变量的名字。Tcl的 regsub同样可以接收可能出现的标志位,例如-all用来进行全局替换,而不是只替换第一处匹配文本。

同样,-nocase选项告诉正则引擎进行不区分大小写的匹配(它等于 egrep 的-i参数,或者Perl的/i修饰符)。

GNU Emacs

GNU Emacs(下文中简称Emacs)是功能强大的文本编辑器,它可以使用elisp(Emacs lisp)作为内建的编程语言。它提供了正则表达式的程序式处理接口,以及数量众多的函数来提供各种服务。其中主要的一种是“正则表达式搜索-前进(re-search-forward)”,接收参数为普通字符串,将它作为正则表达式来处理。然后从文本的“当前位置”开始搜索,直到第一处匹配发生,或者如果没有匹配,就一直前进到字符串的末尾(用户调用编辑器的“正则表达式搜索(regexp search)”的功能时,就会执行re-search-forward)。

如表 3-3(☞92)所示,Emacs 所属的正则流派严重依赖反斜线。例如,「<([a-z]+)([n·t]|<[^>]+>)+1>」是查找重复单词的表达式,可以用来解决第 1 章的问题。但我们不能直接使用这个正则表达式,因为Emacs的正则引擎不能识别t和n。不过Emacs中的双引号字符串则可以,它会把这些标记转换为我们需要的制表符和换行符,传给正则引擎。在使用普通字符串提交正则表达式时,非常有用。但其缺陷——尤其是elisp的正则表达式的缺陷——在于,此流派过分依赖反斜线了,最终得到的正则表达式好像插满了牙签。下面是查找下一组重复单词的函数:

这段程序加上(define-key global-map"C-xC-d"\'FindNextDbl),就可以使用“Control-x+Control-d”来迅速查找重复单词了。

注意事项和处理方式:小结

Care and Handling:Summary

我们已经看到,函数很多,内部的机制也很多。如果你不熟悉这些语言,可能现在还有些困惑。不过请不必担心。学习任何特定的工具软件都比学习原理要容易。