正则表达式之三:用正则表达式处理文本

前面学习的是模式匹配的规则,现在要学习的是如何利用模式匹配来处理文本。干活是时候到了!

 

1s///替换

将制定变量合乎模式的那个部分替换为另一个部分内容

$_="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";

 

3split操作符

它会根据分隔符拆开一个字符串,这对于处理被制表符、冒号、空白或任意符号分隔的数据相当有用。只要能将分隔符写成模式(通常是很简单的正则表达式),就可以使用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里使用费捕捉圆括号(?:)的写法,就可以安全地进行分组。

 

4join函数

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;}

这个程序与我们上一节使用的程序相比,就会发现这两者十分相似。这些命令行选项还是挺管用的。

你可能感兴趣的:(正则表达式)