主要是介绍Perl中的正则表达式的应用。
简单例子:
C_to_F.pl:
#!/usr/bin/perl
$celsius=30;
$fahrenheit = ( $celsius * 9 / 5 ) + 32;
print "$celsius C is $fahrenheit F.\n";
执行这段程序:perl C_to_F.pl
(可以使用perl 脚本名字运行,也可是chmod u+x 脚本名字,然后使用./脚本名字执行。我懒得打两个命令,就直接用perl+脚本名字来执行)
执行的结果是:
稍微解释一下,这段程序,
- 变量:
$celsius
和$fahrenheit
之类的普通变量一般以$
开头,可以保存一个数值或者任意长度的文本,本例子中只保存了数值。 行注释:
#
到行尾之间的都是注释再来看一个例子:
C_to_F2.pl:
#!/usr/bin/perl
$celsius = 20;
while($celsius<=45)
{
$fahrenheit=($celsius*9/5)+32;
print "$celsius C is $fahrenheit F.\n";
$celsius=$celsius+5;
}
Perl也提供了跟其他类型的语言类似的控制结构,在上面条件为真时,while循环控制的部分会重复执行。
运行:perl -w C_to_F2.pl
,运行结果如下。
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.
-w参数不是必须的,与正则表达式也没有直接的联系,他只是告诉Perl,仔细检查运行的程序,在Perl认为可疑的地方发出警报。
好的上面这两个程序就算是简单的入门了,下面来看在Perl中如何使用正则表达式。
使用正则表示式匹配文本
Perl可以以多种方式使用正则表表达式,最简单的就是检查变量中的文本能否匹配某个正则表达式。下面的代码检查中所包含的字符串,报告这个字符串是否全部由数字组成。
testREGX.pl:
#!/usr/bin/perl
$reply="123456abc";
if($reply=~m/^[0-9]+$/)
{
print "$reply-->全部都是数字.\n";
}
else
{
print "$reply-->不全都是数字.\n";
}
运行:perl -w testREGX.pl
,结果如下:
123456abc-->不全都是数字.
对if语句中的条件表达式$reply=~m/^[0-9]+$/
解释,
- 两个反斜杠之间的
^[0-9]+$
是正则表达式。 - 正则表达式两边的
m/.../
告Perl该对这个正则表达式进行什么操作。其中m表示尝试进行”正则表达式匹配”,斜线用来标注正则表达式的界限。 - 前面的
=~
用来连接m/.../
与想要匹配的字符串,即前面的$reply
。这里把=~
理解为能够匹配比较省事。
所以,if($reply=~m/^[0-9]+$/)
读作:如果变量$reply
中的文本能够匹配正则表达式^[0-9]+$
的话,就…(执行if语句中的代码)
如果^[0-9]+$
能够匹配变量$reply
中的文本的话$reply=~m/^[0-9]+$/
就返回true
,不能匹配就返回false
,if
语句使用true/false
来决定输出什么信息。
注意的是,如果$reply
包含任意的数字字符,则$reply=~m/[0-9]+/
就会返回true
因为,相比$reply=~m/^[0-9]+$/
少了行开头和行结尾的限制,而有行开头和行结尾的限制就能保证整个$reply
变量中在只包含数字的情况下才匹配。
实例:摄氏温度和华氏温度的计算
首先提示用户输入一个值,接收这个输入,用一个正则表达式来验证,确保输入的是一个数值。如果是数字的话,我们就计算相应的华氏温度,否则我们就输出一条报警信息。
c2f0.pl:
#!/usr/bin/perl
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 "\"$celsius\" is not a number \n";
}
运行上面的程序:perl -w c2f0.pl
,运行结果如下所示。
Perl
中也和提供了和C
语言类似的格式化输出函数printf
,所以上面的打印代码也可以写为:printf "%.2f C is %.2f F\n",$celsius,$fahrenheit;
,prinf
不会改变计算的结果,不过会改变显示方式。
把if
语句中的print
改成上面的printf
形式运行结果如下:
优化上述程序,向更实用的程序前进
让我们扩展这个例子,允许输入负数和可能出现的小数部分。不过这个问题的计算部分我们就不需要改动了,因为Perl
通常情况下不区分整数和浮点数。不过我们需要修改正则表达式,允许输入负数和浮点数。
我们添加一个-?
容许最前面面的负号,实际上,我们可以用[-+]?来处理开头的正负号。
要容许出现小数部分,我们可以添加(\.[0-9]*)?
,转义点号匹配小数点,所以\.[0-9]*
来匹配小数点和后面可能出现的数字,
因为\.[0-9]*
被(...)?
所包围,所以整个表达式都不是匹配成功所必须的。
所以得到的正则表达式为:^[-+]?[0-9]+(\.[0-9]*)?$
判断语句为if($celsius=~m/^[-+]?[0-9]+(\.[0-9]*)?$/)
,它能够匹配32,-3.723,+98.6这样的文字,不过它还不够完善,它不能匹配以小数点开头的数,例如(.357)。当然,可以在前面多输入一个0来表示(0.357),所以这不是一个很严重的问题。
修改后的新文件为:c2f1.pl
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
# 接收一个用户的输入
$celsius=<stdin>;
chomp($celsius);
# if($celsius=~m/^[-+]?[0-9]+(\.[0-9]*)?$/)
if($celsius=~m/^[0-9]+$/)
{
$fahrenheit = ($celsius*9/5 )+32;
print "$celsius C is $fahrenheit F\n";
}
else
{
print "\"$celsius\" is not a number \n";
}
测试:
不过其实如果非要匹配.375
这样诡异的写法,只需要把正则表达式[-+]?[0-9]+(\.[0-9]*)?
改成[-+]?[0-9]*(\.[0-9]*)?
就行了,修改后的运行结果如下。
成功匹配的副作用
我们再进一步,让这个表达式能够匹配摄氏温度和华氏温度。我们让用户在温度的末尾加上C
表示输入的是摄氏温度,在温度的末尾加上F
表示华氏温度。我们可以在正则表达式的末尾加上[cfCF]
来匹配用户的输入,但这样就需要修改程序的其他部分,以便识别用户输入的是那种类型的温度。
在前面的博客中,我们看到某些版本的egrep
支持作为元字符的\1
,\2
,\3
,用来保存前面的括号内的子表达式实际匹配的文本,Perl
和其他许多支持正则表达式的语言都支持这些功能,而且匹配成功之后,在正则表达式之外的代码仍然能够引用到这些匹配的文本。
与egrep
不同的是,Perl
使用$1,$2,$3...
来指向第1组,第2组,第3组括号内的子表达式实际匹配的文本。注意这些括号是按照从左到右开括号的顺序进行编号的。
为了保持例子的简洁,先不考虑小数部分。对比下面两个正则表达式
$input=~m/^[-+]?[0-9]+[CFcf]$/
$input=~m/^([-+]?[0-9]+)([CFcf])$/
可以看到第二个正则表达式比第一个多了两个括号,添加的括号改变了正则表达式的意思了吗?显然是没有的,添加括号没有改变量词(?
,+
,*
)的作用对象,同时这里也没有多选结构|
.所以第二个正则表达式添加括号之后并没有改变正则表达式匹配的结果。
不过这些括号围住了我们期望匹配字符串中有价值的文本的子表达式,这样就可以第一个括号中匹配的内容(数字)就保存在$1
变量中了,第二个括号匹配的内容(温度制式:C
或F
)就保存在$2中。这样我们在程序中就可以很方便使用$1
的取得数字和$2
取得温度制式。
修改后的程序c2f3.pl:
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+)([CFcf])$/)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,是数字
$type=$2;#获取模式,到底是C还是F
#如果输入的是摄氏度
if($type eq "C" || $type eq "c")
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 is %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
#
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
运行效果:
同时支持小数和模式
现在改进上面的程序,使得新的程序能支持浮点数,允许数字和温度制式之间存才空格。这样程序就能够接收类似98.6 F
这样的输入。
支持浮点数
添加(\.[0-9]*)?
就能处理浮点数:
if($input=~m/^([-+]?[0-9]+(\.[0-9]*)?)([CFcf])$/)
不过要注意的是,支持小数的正则表达式(\.[0-9]*)?
添加在第一括号的内部,添加这个括号之后,就会影响到捕获文本的变量:
$1
表示第一个括号里面的子表达式([-+]?[0-9]+(\.[0-9]*)?)
匹配的文本,这部分包括小数点之前的整数部分和小数点之后的小数部分,这个两部分合起来就是浮点数。所以我们使用$1就能获取整个浮点数。- 而
$2
则表示第二个括号(\.[0-9]*)
里面匹配的文本,也就是小数点之后的部分,这部分已经包含在$1
中了,显然使用$2
没有多大意义。 $3
变量中存放的是([CFcf])
中匹配的文本,这一的$3
与上一个程序中的$2
的效果是一样的,所以,把上面的程序中的$2
改为$3
即可。
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+(\.[0-9]*)?)([CFcf])$/)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,就是浮点数
$type=$3;#获取模式,到底是C还是F
#如果输入的是摄氏度
if($type eq "C" || $type eq "c")
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 等于 %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
运行结果:
可以看到现在if($input=~m/^([-+]?[0-9]+(\.[0-9]*)?)([CFcf])$/)
,可以支持正负数,浮点数,以及选择输入输出温度制式了,不过还没有支持在数字和温度制式实现可以输入任意空格的功能,下面就来支持输入空格的功能。
在数字和温度制式之间可以有任意空格
正则表达式中用\s
来匹配空白符(空格,制表\t
等)。所以,把上面的正则表达式式改成if($input=~m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CFcf])$/)
即可。加入的\s*
表示数字和字母之间可以有多个空白符,也可以没有。
c2f5.pl:
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CFcf])$/)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,是数字
#$2对应小数部分,两个加起来才是完整的温度
$type=$3;#获取模式,到底是C还是F
#如果输入的是摄氏度
if($type eq "C" || $type eq "c")
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 等于 %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
运行结果如下
可以看到现在支持正负数,浮点数,整数,支持指定温度制式,支持温度数字和温度制式之间有空白符。
非捕获型括号(?:...)
在上面的程序中,我们加入(\.[0-9]*)?
支持浮点数之后带来一新的问题,那就是打乱了原来程序中捕获组的顺序,原来的正则表达式([-+]?[0-9]+)([CFcf])$
中只有两个括号,所以有两个捕获组,捕获组$1
对应于数字,捕获组$2
对应于温度制式。
而改成([-+]?[0-9]+(\.[0-9]*)?)\s*([CFcf])$
这个正则表达式之后,就有了3个括号,也就有3个捕获组。虽然捕获组$1
还是对应于数字,但是由于多了一个括号捕获组$2
不再对应于温度制式,而是多出来的捕获组$3
对应于温度制式。这样就要修改源代码,把$2
改成$3
。
如果有一种这样的括号,它只能用于分组,而不影响文本的捕获和捕获组变量$.
的保存,这样就不用修改源代码,问题就好办多了。
好在Perl和近期出现的其他正则表达式流派提供了这个功能。
用(...)
来表示同时分组和捕获,而用(?:...)
表示只分组不捕获。
注意:不捕获分组的开括号是(?:
这三个字符组成的序列。
所以上面匹配浮点数的,正则表达式可以改为
c2f6.pl :
if($input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CFcf])$/)
现在虽然([CFcf])
是第3个括号,但是它匹配的文本就保存在$2中,因为(?:\.[0-9]*)?
中的括号(?:...)?
只分组不捕获,这样就不用修改源代码了。
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CFcf])$/)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,是数字
# $type=$3;#获取模式,到底是C还是F
$type=$2;#获取模式,到底是C还是F
#如果输入的是摄氏度
if($type eq "C" || $type eq "c")
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 等于 %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
#
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
运行结果:
使用非捕获型分组的好处有两点:
- 避免了不必要的捕获操作,提高了匹配的效率。
- 选择合适的括号能让程序更清晰,看代码的人不被括号的具体细节所困扰。
Perl正则表达式忽略大小写:-i
参数
在前面我们为了能够处理温度制式的时候能支持大小写,使用了字符组[CFcf]
表示:$input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CFcf])$/
这里介绍另一种方法:$input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i
,最后面添加的这个i
称为修饰符,修饰符不是正则表达式的一部分,而是m/.../
结构以一部分,这个结构告诉Perl使用者的意图(引用一个正则表达式),以及使用的正则表达式(斜线之间的部分就是正则表达式)。这里的修饰符i
和前面讲的egrep
的-i
参数类似,都是表示使用正则表达式进行匹配的时候忽略大小写。
修改后的代码为:
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,是数字
# $type=$3;#获取模式,到底是C还是F
$type=$2;#获取模式,到底是C还是F
#如果输入的是摄氏度
if($type eq "C" || $type eq "c")
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 等于 %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
#
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
再来看上面代码中温度模式匹配的问题,在匹配温度模式的时候,我们使用的是if($type eq "C" || $type eq "c")
,其中||
表示或运算,这和C语言中的运算符一样,在Perl中或运算也可以写成or
,所以写成if($type eq "C" or $type eq "c")
也是可以的。不过既然我们讨论的是正则表达式,运用上面忽略大小写的知识,我们还可以写成if($type=~m/c/i)
这样也能匹配温度模式C
或者c
温度转行程序最终版本
c2f8.pl :
#!/usr/bin/perl
print "Enter a temperature in Celsius:\n";
$input=<stdin>;# 接收用户输入一行文件
chomp($input);# 去掉文本的换行符
if($input=~m/^([-+]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i)
{
#如果匹配成功,
#这里的$1,相当与前面的egrep中的\1,反向引用
$inputNum=$1;#$1对应第一个括号里面匹配的,是数字
# $type=$3;#获取模式,到底是C还是F
$type=$2;#获取模式,到底是C还是F
#如果输入的是摄氏度
#if($type eq "C" || $type eq "c")
if($type=~m/c/i)
{
$celsius=$inputNum;#取得摄氏度
#计算华氏温度
$fahrenheit=($celsius*9/5)+32;
printf "%.2f 摄氏度 等于 %.2f 华氏度\n",$celsius,$fahrenheit;
}
else
{
#如果是华氏温度,取得华氏温度
$fahrenheit=$inputNum;
#计算对应的摄氏温度
$celsius=($inputNum-32)*5/9;
printf "%.2f 华氏度 等于 %.2f 摄氏度\n",$fahrenheit,$celsius;
}
}
else
{
#
print "输入错误,请输入一个数字,以\"C\"或者\"F\"结尾\n"
}
运行结果:
小结
Perl
用$viriable=~m/regex/
来判断一个正则表达式是否能匹配某个字符串。m
表示匹配(match
),而斜线使用来标注正则表达式的边界的,斜线本身不是正则表达式的一部分。整个$viriable=~m/regex/
测试语句作为一个整体,并根据测试的结果返回true
或者false
- Perl和其他流派的正则表达式提供了许多有用的简记法:
序号 | 简记符 | 描述 |
---|---|---|
1 | \t | 制表符 |
2 | \n | 换行符 |
3 | \r | 回车符 |
4 | \s | 任何的一个空白符,如空格符,制表符,进纸符 |
5 | \S | 除了\s 之外的任意一个字符 |
6 | \w | 即[a-zA-Z0-9] ,一个字母或者数字,在\w+ 的时候很有用,可以用来匹配一个单词 |
7 | \W | 这里的W 是大写,表示出了\w 之外的任何字符,也就是[^a-zA-Z0-9] |
8 | \d | 即[0-9] ,也就是一个数字 |
9 | \D | 表示除了\d 之外的任意字符,也就是[^0-9] |
- 修饰符i表示匹配式忽略大小写,
i
要紧跟在m/.../
最后一个斜线之后,也就是m/.../i
(?:...)
这种写法可以用来分组文本,单不捕获,(...)
是同时分组和捕获。匹配成功之后,Perl中$1
,$2
,$3
…之类的变量保存了相对应的(...)
货号里的子表达式匹配的文本。所以直接使用$1
,$2
,$3
…这些变量从字符串中提取相应的信息。