前面学习的是模式匹配的规则,现在要学习的是如何利用模式匹配来处理文本。干活是时候到了!
1、s///替换
将制定变量合乎模式的那个部分替换为另一个部分内容
$_="He's out bowling with Barney toneght."; s/Barney/Fred/; #把Barney替换为Fred print "$_\n";批注:如果匹配失败,不发生任何事,原变量也保持不变。现在的输出为“ He's out bowling with Fred tonight .”
$_="He's out bowling with Barney toneght."; s/with (\w+)/against $1's team/; print "$_\n";
批注:输出的是“
He's out bowling against Barney's team tonight.”$_="green scaly dinosaur"; s/(\w+) (\w+)/$2, $1/; #替换后为"scaly,green dinosaur" s/^/huge, /; #替换后为"huge, scaly, green dinosaur" s/,.*een//; #huge dinosaur s/green/red/; #匹配失败,此时仍为huge dinosaur s/\w+$/(`$)$&/ #替换后为"huge (huge !)dinosaur" s/s+(!\W+)/$1 / #替换后为"huge (huge!) dinosaur" s/huge/gigantic/ #替换后为"gigantic (huge!) dinosaur"
s///返回的是布尔值,替换成功时为真,否则为假,如下:
$_="fred flintstone" if(s/fren/wilma/){ print "Successfully replaced fred with wilma!\n";}
注意:在前面的例子,可以看到即使有其它可以替换的部分,
s///也只会进行一次替换。当然,这只是默认的行为而已。/g修饰符可让s///进行所有可能的,不重复的替换,如下例:$_="home, sweet home!"; s/home/cave/g; print "$_\n"; #输出"cave, sweet cave!"
替换运算中也可以使用我们常在模式匹配中使用的/i /x /s修饰符。先后顺序无影响。
s#wilma#Wilma#gi; #将所有的WiLmA或者WILMA等一律替换为Wilma s{_END_.*}{}S; #将_END_标记和其后所有的内容都截掉
一个相当常见的全局替换是缩减空白,也就是讲任何连续的空白转换成单一空格:
$_="Input data\t may have extra whitespace."; s/\s+/ /g; #现在它变成了"Input date may have extra whitespace."
删除开头和结尾的空白:
s/^\s+//; #删除开头的空白字符 s/\s+$//; #删除结尾的空白字符 s/^\s+|\s$//g; #去除开头和结尾的空白字符
s///也可以用其它的定界符,对于没有左右之分(非成对)字符,用法跟使用斜线一样,只要重复三次即可。如使用#作为定界符:
s#^https://#http://#;
如果使用左右之分的成对字符,就必须使用两对:一对圈引模式,一对圈引替换字符串,如下:
s{fred}{barney};=s[fren](barney);=s<fred>#barney#;
可以用绑定操作符为s///选择不同的目标
$file_name=~s#^.*/##s; #将$file_name中所有Unix风格的路径全部去除
2、大小写转换
在替换运算中,常常需要把单词全部改成大写(或是小写)。在Perl中使用某些反斜线转义字符就行了。\U转义字符会将其后所有的字符转换成大写:
$_="I saw Barney with Fred."; s/(fred|barney)/\U$1/gi; #$_现在变成了"I saw BARNEY with FRED." s/(fred|barney)/\L$1/gi; #$_现在变成了"I saw barney with fred."
默认情况下,它们会影响之后全部的替换字符串。可以用\E结束大小写转换的影响:
s/(\w+) with (\w+)/\U$2\E with $1/i; #$_替换为"I saw FRED with barney."
使用小写形式的\l和\u时,它们只会影响之后的第一个字符:
s/(fred|barney)/\u$1/ig; #$_替换后为"I saw FRED with Barney."
也可以将它们并用。同时使用\u与\L来表示全部转小写,但首字母大写,且此时\L与\u的先后顺序没有影响:
s/(fred|barney)/\u\L$1/ig; #$_现在成了"I saw Fred with Barney."
现在虽然介绍的是替换时的大小写转换,但它们也使用于任何双引号内的字符串:
print "Hello, \L\u$name\E, would you like to play a game?\n";
3、split操作符
它会根据分隔符拆开一个字符串,这对于处理被制表符、冒号、空白或任意符号分隔的数据相当有用。只要能将分隔符写成模式(通常是很简单的正则表达式),就可以使用split提取数据。它的用法如下:
@fields = split /separator/, $string;
例:@fields = split /:/, "abc:def:g:h"; #得到("abc", "def", "g", "h")
如果两个分隔符连载一起,就会产生空字段:
例:@fields = split /:/, "abc:def::g:h"; #得到("abc", "def", "", "g", "h")
split会保留开头处的空字段,并省略结尾处的空字段。
例:@fields = split /:/, ":::a:b:c:::"; #得到("", "", "", "a", "b", "c")
利用/\s+/模式进行空白分隔也是常见的做法。在此模式下,所有的空白会被当成一个空格来处理:
my $some_input = "This is a \t test.\n"; my $args = split /\s+/, $some_input; #("This", "is", "a", "test.")
split默认会以空白字符分隔$_:
例:my @fields = split; #等效于split /\s+/, $_;
批注:这几乎就等于以/\s+/为模式,只是它会省略开头的空字段。所以,即使该行以空白开头,也不会在返回列表的开头处看到空字段。
请避免在split模式里用到捕获圆括号,因为这会启动所谓的“分隔保留模式”。在split里使用费捕捉圆括号(?:)的写法,就可以安全地进行分组。
4、join函数
join函数不会使用模式,它的功能与split恰好相反:split会将字符串分解为数个片段(子字符串),而join则会吧这些片段联合成一个字符串,它的用法如下所示:
my $result = join $glue, @pieces;
例:my $x = join ":", 4,6,8,10,12; #$x为"4:6:8:10:12"
列表至少要有两个元素,否则胶水无法涂进去:
例:my $y = join "foo", "bar"; #只有一个"bar",这里不会起作用
例:my $empty; #空数组 my $empty = join"baz", @empty; #没有元素,所以得到一个空的字符串
使用上面的$x,我们可以先分解字符串,在用不同的定界符将它接起来:
例:my @values = split /:/, $x; #@values为(4,6,8,10,12)
my $z = join "-", @value; #$z为“4-6-8-10-12”
注意:join的第一个参数是字符串,而不是模式。
5、列表上下文中的m//
在列表上下文中使用模式匹配操作符(m//)时,如果模式匹配成功,那么返回的是所有捕获变量的列表;如果匹配失败,则会返回空列表:
$_ = "Hello there, neighbor!"; my($first, $second, $third) = /(\S+) (\S+), (\S+)/; print "$second is my $third\n";批注:如此就能给那些(可在下一次模式匹配后访问的)匹配变量起既好听又动听的名字。另外因为程序代码中并未使用到=~绑定操作符,所以该模式匹配是针对$_进行的。
/g修饰符也可以用在这里,其效果就是让模式能够匹配到字符串中的许多地方。
my $text = "Fred dropped a 5 ton granite block on Mr.Slate"; my @words = ($text =~ /(a-z)/ig); print "Result: @words\n"; #输出:Fred dropped a 5 ton granite block on Mr.Slate批注:此例中具有一对圆括号的模式,它会在每次匹配成功时返回一个捕获串。这就好像自己动手实现了 split 的功能。不过并非指定想要去除的部分,反而是指定想要流下的部分。如果模式中有多对圆括号,那么每次匹配就能捕获多个串
假设,我们想把一个字符串变成哈希,就可以这样做:
my $data = "Barney Rubble Fred Flinstone Wilma Flintstone"; my %last_name = ($data = ~ /(\w+)\s+(\w+)/g);批注:每次模式匹配成功,就会返回一对被捕获出来的值。这一对值正好称为新哈希的键/值对。
6、贪婪量词与非贪婪量词
+、?、*这些贪婪量词在其后加上?后就变成了非贪婪量词了,即匹配的越短越好。
如要去掉下面这段话中的<BOLD>和</BOLD>可以这样:
I'm talking about the cartooon with Fred and <BOLD>Wilma</BOLD>! s#<BOLD>(.*)</BOLD>#$1#g;但这种方式对于下面这句话就不适用了:
I thought you said Fred and <BOLD>Velma</BOLD>, not <BOLD> Wilma</BOLD>!
批注:现在就不能使用上面那种贪婪量词写法了,会把中间的内容都去掉,必须用下面这种非贪婪量词:
s#<BOLD>(.*?)</BOLD>#$1#g;
非贪婪量词花括号的表示法:{5,10}?或者{8,}?。??这种非贪婪的写法还是会匹配一次或零次,但会优先考虑零次的情况。
7、跨行的模式匹配
处理多行文本,与处理单行文本并无差异。当然,先得有表达式可以表示多行文本才行。
$_ = "I'm much better\nthan Barney is \nat bowling,\Wilma.\n";
^和$通常是用来匹配整个字符串的开始和结束的。但是当模式加上/m修饰符之后,就可以让它们也匹配串内的换行符。这样一来,它们所代表的位置就不再是整个字符串的头尾,而是每行的开头跟结尾。因此,下面的模式就成立了:
print "Found 'wilma' at start of line\n" if /^wilma\b/im;同样,也可以对多行文本逐个进行替换。下面的程序会先把整个文件读进一个变量,然后把文件名前置于每一行的开头:
open FILE,$filename or die "Can't open '$filename': $!" ; my $lines = join '', <FILE>; $lines =~ s/^/$filename: /gm;
8、一次更新多个文件
程序化的更新文件内容时,最常见的做法计是先打开一个新文件,然后把跟旧文件想通的内容写进去,并且在需要的位置进行改写。后面会看到,这样做和直接更新文件的做法效果大致相同,只是有些附带的好处。举例来说,我们现在有几百个格式类似的文件。其中一个叫做fred03.dat,里面都是如下几行的内容:
Program name: granite Author: Gilert Bates Company: RockSoft Department:R&D Phone: +1 503 555-0095 Date: Tues March 9, 2004 Version:2.1 Size: 21k Status: Final beta
我们必须修改这个文件,让它有一些新的信息。下面就是应该改成的样子:
Program name: granite Author: Randal L. Schwartz Company: RockSoft Department:R&D Phone: +1 503 555-0095 Date: June 12, 2008 6:38 pm Version:2.1 Size: 21k Status: Final beta
简单的说,三项改动:Author字段的姓名要改,Date要改成今天的日期,而Phone则要删除。另外几百个文件也都要进行这些改动。
要在Perl中直接修改文件内容可以使用钻石操作符(<>)。下面的程序可以完成上述的要求;
#!/usr/bin/perl -w use strict; chomp(my $date = `date`); $^I = ".bak"; while(<>){ s/^Author: .*/Author: Randal L. Schwartz/; s/^Phone: .*\n//; s/^Date: .*/Date: $date/; print;}
因为我们需要今天的日期,所以这个程序一开始就使用了系统的date命令。另外一个比较好的做法就是在标量上下文中使用Perl自己的localtime函数(但两者的格式稍有差异):my $date = localtime;下一行则是对$^I变量赋值。
钻石操作符会读取命令行参数直指定的那些文件。程序的主循环一次会读取,更新及输出一行。钻石操作符:自动帮打开许多文件,而且如果没有指定文件,它就会从标准输入读进数据。但如果$^I中是个字符串,该字符串就会变成备份文件的扩展名。
工作过程:先假设钻石操作符正好打开了文件fred03.dat。除了像以前一样代开文件之外,它还会把文件名改成fred03.dat.bak。虽然打开的是同一个文件,但是它在磁盘上的文件名已经不同了。接着,钻石操作符会打开一个新文件并将它取名为fred03.dat。这么做并不会有任何问题,因为我们已经没有同名文件了。现在钻石操作符会把默认的输出设定为这个新打开的文件,所以输出来的所有内容都会被新进这个文件。这样while循环会从旧文件读进一步输入,做了一些改动之后把新的内容写进新文件。在普通的机器上,这样的程序可以在几秒内修改上百个文件。Perl并没有编辑任何文件,它只是创建了一个修改过的新拷贝。
有些人会把$^I的值设为~这个字符,因为emacs在处理备份文件的文件名时也是这么做。而如果把$^I设为空字符串,就会直接修改文件的内容,但不会留下任何备份。只要模式中不小心打错了一个字,就可能会把整份数据全班清空,所以如果你真的想看看备份磁带质量如何,就尽情的用空字符串吧!全部确认无误后再把备份文件删除是轻而易举的事。如果做错了,则需要把已备份的文件还原回来,Perl可以做到这件事。
9、从命令行进行在线编辑
如果在上例中的上百个文件中的拼错的Randall,改成Randal。可以用上节那样的程序来完成此事。或者,你也可以在命令行上使用下面的单行程序做到:
$ perl -p -i.bak -w -e 's/Randall/Randal/g' fred*.dat
以perl开头的命令的作用如同在文件的开头加上#!/usr/bin/perl:表示以perl程序来处理随后的脚本。
-p选项则可以让perl自动生成一小段程序,看起来类似如下的片段:
while(<>){
print;}
如果不需要这么多的功能,还可以用-n选项,这样就可以把自动执行的print去掉,所以你可以自行决定什么内容需要print。这点细微差别对大程序来说无关轻重。
-i.bak的作用就是在程序开始运行之前把$^I设为.bak。如果不想做备份,请直接写出-i,不要加扩展名。
-w的作用是打开警告。
-e用来告诉perl后面跟着的是程序代码。也就是说,s/Randall/Randal/g这个字符串会被直接当成perl程序代码。因为目前我们已经有个while循环了(来自-p选项),所以这段程序代码会被放到循环中的print前面的位置。用-e选项指定的程序中可以省略末尾的分号。如果你制定了多个-e选项,就会有多段程序代码,此时只有最后一段程序末尾的分号可以省略。
最后一个命令行参数是fred*.dat,表示@ARGV的值应该是匹配此文件名模式的所有文件名。
把以上的所有片段全都组合在一起,就好像写了下面这个程序,并且用fred*.dat这个参数调用它一样:
#!/usr/bin/perl -w $^I = ".bak"; while(<>){ s/Randall/Randal/g; print;}
这个程序与我们上一节使用的程序相比,就会发现这两者十分相似。这些命令行选项还是挺管用的。