Matching Text with Regular Expressions
Perl可以以多种方式使用正则表达式,最简单的就是检查变量中的文本能否由某个正则表达式匹配。下面的代码检查$reply中所含的字符串,报告这个字符串是否全部由数字构成:
第一行的代码也许有些奇怪:正则表达式是「^[0-9]+$ 」,两边的 m/…/告诉 Perl 该对这个正则表达式进行什么操作。m代表尝试进行“正则表达式匹配(regular expression match)”,斜线用来标记界限(注2)。之前的=~用来连接m/…/和欲搜索的字符串,即本例中的$reply。
请不要混淆=~、=和==。运算符==用来测试两个数字是否相等(我们将会看到,运算符 eq用来测试两个字符串是否相等)。运算符=用来给变量赋值,例如$Celsius=20。最后,=~用来连接正则表达式和待搜索的目标字符串。在这个例子中,要搜索的正则表达式是m/^[0-9]+$/,而目标字符串是$reply。此程序在其他语言中的思路有所不同,我们会在下一章看到例子。
把=~读作“匹配(matches)”可能比较省事,所以
if ($reply=~m/^[0-9]+$/)
读作:
“如果变量$reply所含的文本能够匹配正则表达式「^[0-9]+$」,那么…”
如果「^[0-9]+$」能够匹配$reply的内容,$reply=~m/^[0-9]+$/的返回值就为true,否则为false。if语句使用true/false值来决定输出什么信息。
请注意,如果$reply中包含任意的数字字符,$reply=~m/[0-9]+/(相比之前的表达式,去掉了开头的脱字符和结尾的美元符)的返回值就是 true。两端的「^…$」保证整个$reply只包含数字。
现在把上面两个例子结合起来。首先提示用户输入一个值,接收这个输入,用一个正则表达式来验证,确保输入的是一个数值。如果是,我们就计算相应的华氏温度,否则,我们输出一条报警信息:
请注意最后的print语句有两个转义的双引号,它们的作用并不是标记引用字符串的边界。对大多数语言的文字字符串(literal string)来说,有时候需要转义某些字符,做法跟正则表达式中元字符的转义很相似。在 Perl 中,字符串与正则表达式的区别并非很重要,但是在Java、Python 等语言中却极为重要。“一点题外话——数量丰富的元字符”这一节(☞44)更详细地讨论了这个问题(VB.NET 是个明显的例外,在那里转义双引号用‘””’而不是‘”’)。
如果我们把这段程序保存为c2f,则运行结果如下:
哎呀,看来(至少在某些系统上),Perl的简单的print并不能很好地处理浮点数。
我不想在本章中讨论Perl的细节,但是我告诉你用printf(“格式化输出(print formatted)”)可以解决这个问题:
printf /"%.2f C is%.2f Fn/",$celsius,$fahrenheit;
这里的printf类似C语言中的printf,或者Pascal、Tcl、elisp和Python中的format。它不会更改变量的值,而只是改变显示的方式。现在的结果好看多了:
向更实用的程序前进
Toward a More Real-World Example
让我们扩展这个例子,容许输入负数和可能出现的小数部分。这个问题的计算部分没问题——Perl通常情况下不区分整数和浮点数。不过我们需要修改正则表达式,容许输入负数和浮点数。我们添加一个「-?」来容许最前面的负数符号。实际上,我们可以用「[-+]?」来处理开头的正负号。
要容许可能出现的小数部分,我们添加「(.[0-9]*)?」。转义的点号匹配小数点,所以「.[0-9]*」用来匹配小数点和后面可能出现的数字。因为「.[0-9]*」被「(…)?」所包围,整个子表达式都不是匹配成果所必须的(请注意,它与是截然不同的,对后一个表达式中,即使「.」无法匹配,「[0-9]*」也能够匹配接下来的数字)。
把这些综合起来,就得到这样的条件判断语句:
它能够匹配 32、-3.723、+98.6这样的文字。不过还不够完善:它不能匹配以小数点开头的数(例如.357)。当然,用户可以添加一个整数位 0 来匹配(例如 0.357),所以我认为这并不是一个严重的问题。这个浮点数问题处理起来得靠些诀窍,我们会在第 5 章详细讲解(☞194)。
成功匹配的副作用
Side Effects of a Successful Match
我们再进一步,让这个表达式能够匹配摄氏和华氏温度。我们让用户在温度的末尾加上 C或者 F 来表示。我们可以在正则表达式的末尾加上「[CF]」来匹配用户的输入,但还需要修改程序的其他部分,以便识别用户输入的温度类型,并进行相应的转换。
在第1章,我们看到过某些版本的egrep支持作为元字符的「1」、「2」、「3」,用来保存前面的括号内的子表达式实际匹配的文本(☞21)。Perl 和其他许多支持正则表达式的语言都支持这些功能,而且匹配成功之后,在正则表达式之外的代码仍然能够引用这些匹配的文本。
我们会在下一章看到各种语言是如何做到这一点的(☞137),但是 Perl 的办法是通过变量$1、$2、$3等等,它们指向第一组、第二组、第三组括号内的子表达式实际匹配的文本。这未免有点奇怪,它们都是变量,而变量名则是数字。正则表达式匹配成功一次,Perl就会设置一次。
总结一下,在尝试匹配时,正则表达式中的元字符「1」指向之前匹配的某些文本,匹配成功之后,在接下来的程序中用$1来引用同样的文本。
为了保持例子的简洁,集中表现新的地方,我先不考虑小数部分,之后再来看它。所以,我们来看$1,请比较:
添加的括号改变了正则表达式的意义吗?为了回答这个问题,我们需要知道,这些括号是否改变了星号或者其他量词的作用对象,或是「|」的意义。答案是,都没有改变,所以这个表达式仍然能够匹配相同的文本。不过,他们确实围住了我们期望匹配字符串中“有价值”文本的子表达式。如图2-1 所示,$1保存那些数字,而$2保存 C或者 F。下一页的图2-2是程序的流程图,我们发现,这个图让我们很容易地决定匹配之后应该干什么。
图2-1:捕获型括号
图2-2:温度转换程序的逻辑流程
示例2-1:温度转换程序
如果上一页的程序名叫convert,我们可以这样使用:
错综复杂的正则表达式
Intertwined Regular Expressions
在 Perl 之类的高级语言中,正则表达式的使用与其他程序的逻辑是混合在一起的。为了说明这一点,我们对这个程序做三点改进:像之前一样能够接收浮点数,容许 f或者 c是小写,容许数字和字母之间存在空格。这三点全都完成之后,程序就能够接收‘98.6·f’的输入。
我们已经知道,添加「(.[0-9]*)?」就能够处理浮点数:
请注意,它添加在第一个括号内部,因为我们用第一组括号内的子表达式来捕获温度的值,我们当然希望它能够包含小数部分。不过,增加了这组括号之后,即使它只是用来分组问号限定的对象,也会影响到引用捕获文本的变量。因为这组括号的开括号在整个表达式中排在第二位(从左向右数),所以它匹配的文本存入$2(见图2-3)。
图2-3:嵌套的括号
图2-3说明了括号的嵌套关系。在「[CF]」之前添加一组括号并不会直接影响整个正则表达式的意义,但是会产生间接的影响,因为现在「[CF]」所在的括号排在第 3 位。这也意味着我们需要把$type的赋值从$2改为$3(如果不希望这么做,可以参考下一页的补充内容)。
接下来,我们要处理数字和字母之间可能出现的空格。我们知道,正则表达式中的空格字符正好对应匹配文本中的空格字符,所以「·*」能够匹配任意数目的空格(但并不是必须出现空格):
但这样还不够灵活,而我们希望的是开发一个能够实际应用的程序,所以必须容许其他的空白字符(whitespace)。例如常见的制表符(tabs)。但是「*」并不能匹配空格,所以我们需要一个字符组来匹配两者「[]*」。
请把上面这个子表达式与「(·*|*)」进行对比,你能发现这其中的巨大差异吗?ϖ请翻到下一页查看答案。
本书中空格字符和制表符都很常见,因为我使用·和来表示它们。不幸的是,在屏幕上却不是如此。如果你见到*,在没有实际测试过以前,只能猜测这是空格符还是制表符。为了方便使用,Perl提供了「t」这个元字符。它能够匹配制表符——相比真正的制表符,它的好处就在于看得更清楚,所以我会在正则表达式中采用这个元字符。于是「[· ]*」变成「[·t]*」。
Perl还提供了一些简便的元字符,例如「n」(表示换行符),「f」(ASCII的进纸符formfeed),和「b」(退格符)。不过,确切地说,「b」在某些情况下是退格符,有些情况下又表示单词分界符。它怎么能身兼数职呢?下一节我们会看到。
一点题外话——数量丰富的元字符
在前面的例子里我们见到了n,但是n都出现在字符串而不是正则表达式中。就像多数语言一样,Perl的字符串也有自己的元字符,它们完全不同于正则表达式元字符。新程序员常犯的错误就是混淆了这两个概念(VB.NET是个例外,因为其中字符串的元字符少得可怜)。字符串的元字符中有一些跟正则表达式中对应的元字符一模一样。你可以在字符串中用t加入制表符,也可以在正则表达式中用元字符「t」来匹配制表符。
这种相似性无疑方便了使用,但是我必须强调区分这两种元字符的重要性。对于t这样简单的情况来说或许并不重要,但对于我们将要看到的各种不同的语言和工具来说,知道在什么情况下应该使用什么元字符是极其重要的。
我们已经见过不同的字符组之间的冲突。在第1章,使用egrep时,我们把正则表达式包含在单引号中。整个egrep命令行写在command-shell提示符,shell能够认出它自己的元字符。例如,对shell来说,空格符就是一个元字符,它用来分隔命令和参数,或者参数与参数。在许多shell中,单引号是元字符,单引号内的字符串中的字符不需要被当作元字符处理(DOS使用双引号)。
在shell中使用引号容许我们在正则表达式中使用空格。否则,shell会把空格认作参数之间的分隔符,而不是把整个正则表达式传递给egrep。许多shell能够识别的元字符包括$、*、?之类——我们在正则表达式中也会用到这些元字符。
目前,所有关于shell的元字符和Perl字符串的元字符的讨论都还与正则表达式本身没有任何关联,但它们会影响到现实环境中正则表达式的使用。随着阅读的深入,我们会见到许多(有时候还很复杂)情况,我们需要同时在不同层级上使用元字符交互(multiple levels of simultaneously interacting metacharacters)。
那么「b」的情况呢?这是一个正则表达式的问题:在Perl的正则表达式中,「b」通常是匹配一个单词分界符的,但是在字符组中,它匹配一个退格符。单词分界符作为字符组的一部分则没有任何意义,所以Perl完全可以用它来匹配其他的字符。第1章曾提醒我们,字符组“子语言”的规范不同于正则表达式主体,这条规则也适用于Perl(包括任何其他流派的正则表达式)。
用s匹配所有“空白”
讨论空白的问题时,我们最后使用的是「[·t]* 」。这样做没问题,但许多流派的正则表达式提供了一种方便的办法:「s」。「s」看起来类似「t」,「t」代表制表符,而「s」则能表示所有表示“空白字符(whitespace character)”的字符组,其中包括空格符、制表符、换行符和回车符。在我们的例子中,换行符和回车符并不需要特别考虑,但是「s*」显然比「[·t]*」要简洁。而且不久你就会习惯这种表示法,在复杂的表达式中,「s*」更加易于理解。
现在我们的程序变成:
最后,我们还必须能够处理表示温度制式的小写字母。简单的办法是直接把小写字母添加到字符组中,「[CFcf]」。不过,我更愿意使用另一种办法:
$input=~m/^([-+]?[0-9]+(.[0-9]*)?)s*([CF])$/i
添加的这个i称作“修饰符(modifier)”,把它放在m/…/结构之后,告诉Perl进行不区分大小写的匹配。修饰符其实不是正则表达式的一部分,而是m/…/结构的一部分,这个结构告诉Perl使用者的意图(应用一个正则表达式),以及采用的正则表达式(在斜线之间的部分)。我们曾看到过这种功能,即egrep的-i参数(☞15)。
时时刻刻说“i修饰符(the i modifier)”有点麻烦,所以我们通常说“/i”,即使真正的写法并不是“/i”。/i 只是在 Perl 中指定修饰符的办法之一——在下一章,我们会看到其他的办法,以及其他语言实现此功能的写法。在本章后面的部分,我们还会看到其他的修饰符,例如/g(表示“全局匹配(global match)”)以及/x(表示“宽松排列的表达式(free-form expressions)”)。
现在,我们已经做了不少修改了,来看看新的程序:
哎呀!你是否注意到了,第二次运行时我们输入的是摄氏50度,结果被认成了华氏50度?看看程序的逻辑,你找出问题了吗?
再来看程序的片段:
虽然我们的正则表达式能够接受小写的f,程序的其他部分却没有相应的修改。在这个程序里,只有$type是‘C’的时候,才作为摄氏度处理。因为程序同样可以接受小写的 c,我们需要修改$type的判断:
if ($type eq /"C/" or $type eq /"c/") {
实际上,因为本书是关于正则表达式的,我或许这样做:
if ($type=~m/c/i) {
现在,大小写的情况都能应付了。最终的程序如下所示。这个例子告诉我们,正则表达式的使用方式,可能会影响到程序的其他部分。
示例2-2:温度转换程序——最终版本
暂停片刻
Intermission
尽管本章的大部分篇幅是关于熟悉Perl的,但也遇到了许多新的关于正则表达式的知识。
1.许多工具都有自己的正则表达式流派。Perl 和egrep 可能属于同一个流派,但是Perl 的正则表达式中的元字符更多。许多其他的语言,类似Java、Python、.NET和TCL,它们的流派类似Perl。
2.Perl用$variable=~m/regex/来判断一个正则表达式是否能匹配某个字符串。m表示“匹配(match)”,而斜线用来标注正则表达式的边界(它们本身不属于正则表达式)。整个测试语句作为一个单元,返回true或者false值。
3.元字符——具有特殊意义的字符——的定义在正则表达式中并不是统一的。之前在关于shell和双引号引用的字符串的例子中我们讲过,元字符的含义取决于具体的情况。了解具体情况(shell、正则表达式、字符串),其中的元字符及其作用,对学习和使用Perl、PHP、Java、Tcl、GNU Emacs、awk、Python 或其他高级语言是非常重要的(当然,在正则表达式内部,字符组有自己的“子语言”,其中的元字符是不同的)。
4.Perl和其他流派的正则表达式提供了许多有用的简记法(shorthands):
5./i修饰符表示此测试不区分大小写。尽管写法是“/i”,其实“i”只是跟在表示结尾的斜线之后。
6.「(?:…)」这个麻烦的写法可以用来分组文本,但并不捕获。
7.匹配成功之后,Perl可以用$1、$2、$3之类的变量来保存相对应的「(…)」括号内的子表达式匹配的文本。使用这些变量,我们能够用正则表达式从字符串中提取信息(其他的语言所使用的方式有所不同,我们会在下一章看到例子)。
子表达式的编号按照开括号的出现先后排序,从 1 开始。子表达式可以嵌套,例如「(Washington(·DC)?)」。如果只是希望分组,也可以使用「(…)」,但副作用是,它们捕获的文本仍然会保存到特殊的变量中。