1

精通正则表达式:第二章


本文关注的是正则表达式,只是因为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", $celsius, $fahrenheit ;
    } else{
        #如果一开始没有匹配,则报错。
        print "Expecting a number followed by \"C\" or \"F\",\n";
        print "So I don't understand \"$input\".\n";
    }

到这里就先休息下吧。顺便来道题目思考思考:

(·*|\t*)[·\t]*之间在匹配的结果有什么差别?


yibohu1899
264 声望17 粉丝