精通正则表达式:第二章
本文关注的是正则表达式,只是因为Perl对正则表达式的支持优于其他语言,所以选用Perl,请不要过多的关心Perl是怎么回事,必要的前置知识会在这里提及。下面开始我们的正则之旅。在本文,会使用 · 来代替正则表达式中出现的空格。
一、简单易懂的Perl魔法
下面是一段简单的Perl示例程序,功能是将华氏温度转换为摄氏温度。
$celsius = 30; $fahrenheit = ($celsius * 9 / 5) + 32; #计算华氏温度 print "$celsius C is $fahrenheit F.\n" #输出两种温度
其结果为:
30 C is 86 F.
从这段程序中,我们会发现Perl的几个特点:
普通的变量(如$celsius)以$开头,可以保存数值或者字符串。
\#代表着注释的开始
变量可以出现在引号包围的字符串中,最后会被其实际值替代。口怕!
同时,Perl也提供了流程控制语句,如while:
$celsius = 20; while ($celsius <= 45) { $fahrenheit = ($celsius * 9 / 5) + 32; print "$celsius C is $ fahrenheit F.\n"; $celsius = $celsius + 5; }
运行结果如下:
20 C is 68 F. 25 C is 77 F. 30 C is 86 F. 35 C is 95 F. 40 C is 104 F. 45 C is 113 F.
当条件为真的时候,while循环控制的部分就会重复执行,直到条件为假。如果在终端中运行,则就像下面这样:
/~> perl -w CelToFah.pl
这里的-w
参数不是必须的,但是加上参数以后,Perl会在可疑的地方报错。这算是一种良好的习惯罢了。由于Perl不是本文的重点,所以介绍就到这里为止,下面是Perl中正则表达式的使用。
二、匹配文本
在Perl中,最简单的正则表达式使用方法就是:检查变量中的文本是否能匹配指定正则表达式。实例片段如下:
if ($reply =~ m/^[0-9]+$/){ print "only digits\n"; } else { print "not only digits\n"; }
如你所见,第一行的表达式颇有魔法风范:正则表达式是^[0-9]+$
;m/.../
则通知Perl要对正则表达式进行什么操作,m意味着尝试进行正则表达式匹配,而斜杠则用来标记界限;=~
则用来连接对象字符串和正则表达式。
需要注意的是,=~
、==
、=
三者请勿混淆。=~
用于正则表达式,=
用于变量赋值, 而==
则用于测试数值是否相等。字符串是否相等,使用的是eq
。在这里,表达式
$reply =~ m/^[0-9]+$/
的返回值取决于变量reply。如果其内容能匹配正则表达式m/^[0-9]+$/
,则会返回真。而两端的^$
则保证其只包含数字。接下来则是两个例子的结合。
首先,会提示用户输入一个值,接受这个输入并用正则表达式去验证:如果输入的是数值,则计算相应的华氏温度;否则报错。实例如下:
print "Enter a temperature in Celsius:\n"; $celsius = <STDIN>; #从用户处接受一个输入 chomp($celsius); #去掉换行符 if ( $celsius =~ m/^[0-9]+$/) { $fahrenheit = ($celsius * 9 / 5) + 32; #计算华氏温度 print "$celsius C is $fahrenheit F\n"; } else { print "Expecting a number,so I don't understand \"$celsius\".\n"; }
字符里面的转义就不再赘述了。要注意的是,Perl中,字符串和正则表达式的区别既不明显,也不重要,这是它和其他语言的一大区别。运行结果如下:
Enter a temperature in Celsius: 123 123 C is 253.4 F
该版本的Perl浮点数处理的很好……那我就不黑了。
更进一步
我们可以拓展这个例子,使它支持小数和负数。计算部分就交给Perl吧。负数就是一个可选的负号,而小数则是可选的小数点和任意数字。所以拓展后的正则表达式是这样的:
m/^-?[0-9]+(\.[0-9]*)?$/
现在,他就可以匹配-19、0.343这类的数字了,但是.9834这种数字还是无法匹配,由于不是什么大问题,我们会留到很后面再来处理。
成功匹配的副作用
现在,我们除了要匹配数字,还要用户可以输入C和F来标识输入的温度类型,并进行转换。
我们知道,正则表达式可以捕获匹配文本,并在能够在正则表达式之外进行引用。而Perl则通过临时变量$1/$2/$3
指向分组内的子表达式匹配的文本。
总之,匹配过程中,使用/1
来匹配的文本;而在匹配过后,用$1
指向匹配的文本。为此,我们需要修改表达式。首先,忽略并去掉小数部分的匹配,以突出新特点。
m/^([-+]?[0-9]+)([CF])$/
在这个表达式中,使用括号围住了“有价值”的部分,捕捉过后,我们可以决定要使用它们来做什么。现在,我们打算实现之前提到的事情:匹配数字,还要用户可以输入C和F来标识输入的温度类型,并进行转换。
print "Enter a temperature in Celsius:\n"; $input = <STDIN>; #从用户处接受一个输入 chomp($input); #去掉换行符 if ( $input =~ m/^([-+]?[0-9]+)([CF])$/) { #程序运行到这里就已经匹配好了。$1保存数字,$2保存符号。 $InputNum = $1; $type = $2; if ($type eq "C") { #输入为摄氏温度,计算华氏温度 $celsius = $InputNum; $fahrenheit = ($celsius * 9 / 5) + 32; } else { #否则,应该是"F",那就计算摄氏温度。 $fahrenheit = $InputNum; $celsius = ($fahrenheit - 32) *5 /9; } #现在得到两个温度值,显示结果,并使用格式化字符串。 printf "%.2f C is %.2f F.\n",$celsius,$fahrenheit ; } else{ #如果一开始没有匹配,则报错。 print "Expecting a number followed by \"C\" or \"F\",\n"; print "So I don't understand \"$input\".\n"; }
结果如下:
PS E:\LearnPerl> perl -w .\REdigits1.pl Enter a temperature in Celsius: 22F -5.56 C is 22.00 F. PS E:\LearnPerl> perl -w .\REdigits1.pl Enter a temperature in Celsius: 39C 39.00 C is 102.20 F. PS E:\LearnPerl> perl -w .\REdigits1.pl Enter a temperature in Celsius: oops Expecting a number followed by "C" or "F",So I don't understand "oops".
但这里离成功还有一定距离,比如:
无法接受浮点数
不能容许小写的c和f
不能接受数字和字母之间的空格
为了赶上这些距离,我们还有几件事情要做。首先,我们向正则表达式添加小数部分的匹配。修改如下:m/^([-+]?[0-9]+(\.[0-9]*)?)([CF])$/
这里,我在小数部分添加了一个括号。括号本身虽然没有被我们使用,但是确实影响了引用捕获文本的变量。现在,结果变成了下面这样:
Enter a temperature in Celsius: 11.2F type is .2 InputNum is 11.2
可以明显的看到,$1匹配的整个数字,也就是外围的第一个括号分组([-+]?[0-9]+(\.[0-9]*)?)
;而$2则匹配第一个括号分组嵌套的(\.[0-9]*)
;$3则是原来的变量$2。这样就可以明白,分组的序号由分组的开括号(
在 表达式中的顺序有关(从左到右)。
我们现在可以将$type
变量的赋值改为$3
,或者,使用非捕获型括号。
可以使用(?:...)来表示只分组,不捕获。这样,一是不会影响捕获计数,二是可以提高匹配效率,三是让代码更加清晰。但是,如果是只使用一次的正则,可以考虑弃之不用。
现在,我们可以来处理空格了。我们可以使用·*
来表示。再度修改如下:m/^([-+]?[0-9]+(?:\.[0-9]*)?) *([CF])$/
有人注意到哪里修改了吗?嗯……这样确实很难注意到这边有一个空格。与此同时,如果输入的是制表符(天知道为什么会输入进来),那就匹配不到了。所以,我们可以使用元字符\s
来匹配空白字符,三度修改如下:m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/
好了,现在只剩下小写字母的问题了。我们可以使用一个修饰符(modifier)。
结果变成这样:m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i
`这个修饰符只是Perl中的用法,其他语言有不同的实现方式,如Python使用的在编译的时候指定。
现在,大功告成,来测试一下:
Enter a temperature in Celsius: 33.98 c 1.10 C is 33.98 F.
结果不尽如人意……嗯,再修改一下即可。
最终版本就是这样子的:
print "Enter a temperature in Celsius:\n"; $input = <STDIN>; #从用户处接受一个输入 chomp($input); #去掉换行符 if ( $input =~ m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i) { #程序运行到这里就已经匹配好了。$1保存数字,$2保存符号。 $InputNum = $1; $type = $2; if ($type =~ m/c/i) { #输入为摄氏温度,计算华氏温度 $celsius = $InputNum; $fahrenheit = ($celsius * 9 / 5) + 32; } else { #否则,应该是"F",那就计算摄氏温度。 $fahrenheit = $InputNum; $celsius = ($fahrenheit - 32) *5 /9; } #现在得到两个温度值,显式结果。 printf "%.2f C is %.2f F.\n",\n"; print "So I don't understand \"$input\".\n"; }
到这里就先休息下吧。顺便来道题目思考思考:
(·*|\t*)
和[·\t]*
之间在匹配的结果有什么差别?