Perl的正则表达式(三)

用s///进行替换

把存在变量中匹配模式的那部分内容替换成另一个字符串,如果匹配失败,则什么都不会发生.s///返回的是布尔值,替换成功时为真,否则为假。

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

/g全局替换

s///只会进行一次替换,s///g则会进行所有可能的的替换。我们常常会需要缩减空白,即把任何连续的空白转换成单一空格:

$_ = "Input    data\t may have extra     whitespace.";
s/\s+/ /g;

另一个常用的例子是去除掉行首和行尾的空白符:

s/^\s+|\s+$//g;

无损替换

s///会直接对原始字符串进行替换,返回成功替换的次数。如果用r修饰符,会保留原始字符串,而返回替换过后的字符串。

my $original = 'fred ate 1 rib';
my $copy = $original =~ s/\d+ ribs?/10 ribs/r;

大小写转换

在替换运算中,常常需要把替换的单词改写成全部大写或小写。

  • \U:全大写
  • \L:全小写
  • \l: 只影响后面紧跟的第一个字符,小写
  • \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 ."
s/(\w+) with (\w+)/\U$2\E with $1/i; =>"I saw FRED with barney ."
s/(fred|barney)/\u$1/gi; =>"I saw FRED with Barney ."
s/(fred|barney)/\u\L$1/ig;=>"I saw Fred with Barney ."

默认情况下,\E,\L会影响到后面全部的替换字符串,你可以用\E关闭大小写转换功能。

split操作符

对于使用制表符,冒号,空白或任意符号分隔不同字段的字符串来说,用split提取字段相当方便。只要将分隔符写成模式(通常是很简单的正则表达式):

my @fields = split /:/,"abc:def:g:h"; =>("abc","def","g","h");
my @fields = split /:/,"abc:def::g:h"; =>("abc","def","","g","h");
my @fields = split /:/,":::a:b:c:::"; =>("","","","a","b","c");

如果两个分隔符连在一起,就会产生空字段。并且split会保留开头的空字段,而会舍弃结尾处的空字段. 利用split的/\s+/模式根据空白符分隔字段也是比较常见的做法:

my $some_input = "this is a \t     test.\n";
my @args = split /\s+/,$some_input;
my @fields = split;   <=> split /\s+/,$_;

join函数

与split刚好相反,join函数将片段用指定的连接符合成一个字符串。

my $x = join ":",(4,5,6,7,8); =>"4:5:6:7:8";
my @values = split /:/,$x;
my $z = join "-",@values;

join的列表至少有两个元素,否则不起作用。join经常与split联合使用。

列表上下文中的m//

在列表上下文中庸模式匹配,如果模式匹配成功,那么返回的是所有捕获变量的列表,如果失败,返回空列表:

$_ = "hello there, neighbor!";
my($first,$second,$third) = /(\S+) (\S+), (\S+)/;

my $text = "Fred dropped a 5 ton granite block on Mr.Slate";
my @words = ($text =~ /([a-z]+)/gi);
@words => Fred dropped a ton granite block on Mr Slate

my $data = "barney rubble fred flintstone wilma flintstone";
my %last_name = ($data =~ /(\w+)\s+(\w+)/g);

非贪婪量词

到目前为止,我们看到的量词+,*,?,{4,10}都是贪婪的。所谓贪婪,就是在保证整体匹配的前提下,它们会尽量匹配长字符串,实在不行才会吐出一点。 举个例子:

/fred.+barney/,'fred and barney went bowling last night;

贪婪量词的匹配过程是这样的:首先匹配到fred,然后.+会匹配剩下所有的字符,直到最后的night,现在轮到匹配barney了,但是由于已经匹配到了结尾,没有多余的字符可供匹配了,但是由于.+少匹配一个字符也算匹配成功,所以它打算后退一步看看,于是吐出最后匹配到的字符t(虽然它很贪婪,但是为了顾全大局,并让整体模式尽可能匹配成功,它愿意回退。)。这样,正则表达式引擎会一直进行回溯,直到成功匹配,也可能最后也不能做到整体匹配,此时匹配失败。

由此可见,贪婪量词可能会经历繁琐的回溯过程,影响效率。

于是,每个贪婪量词都有一个非贪婪版本,+?,*?,??,{4,10}?,它会在整体匹配的情况下,匹配越少字符越好。

/fred.+?barney/,'fred and barney went bowling last night;

现在,匹配过程变成这样了:先匹配上fred然后往后匹配一个字符,接下匹配模式的剩余部分barney,发现没匹配上,于是.+?再多匹配一个字符,重复上述过程,直到整体匹配。

贪婪量词和非贪婪量词哪个效率高取决于要处理的数据。在上述例子中,如果fred和barney分别位于字符串的开头和结尾,那么贪婪量词更有效率;如果相隔很近,那么非贪婪量词更高效。

再看一个例子,假设你有一个HTML文本,你要去除这样的标记,并取得其内容:

I am talking about the cartoon with fred and <BOLD>Wilma</BOLD>!
s#<BOLD>(.*)</BOLD>!#$1#g;

这样做在某些情况下会出问题,因为*太贪心了,考虑下面的文本:

I am talking about the cartoon with fred and <BOLD>Wilma</BOLD>, not <BOLD>Velma</BOLD>!

这时,模式会从第一个一直匹配到最后的,把中间部分全部取出来。事实上,我们应该使用非贪婪量词,这样就可以取出每个对的内容了。

s#<BOLD>(.*)</BOLD>!#$1#g;

跨行模式匹配

传统的正则表达式都是用来匹配单行文本,事实上,单行文本和多行文本并无本质区别,只要在单行文本里加入换行符\n,就可以变成多行文本了。我们知道^,\$都是匹配字符串的绝对开头和绝对结尾,如果在模式后面加上/m修饰,就可以让它们匹配字符串内的每一行。

open FILE,$file_name
    or die "can not open $file_name : $!\n";
my $lines = join '',<FILE>;
$lines =~ s/^/$file_name : /gm;

一次更新多个文件,变量$^I

chomp( my $date = 'date');
$^I =".bak";
while(<>){
    s/^Author:.*/Author: Randal L. Schwartz/;
    s/^Phone:.*\n//;
    s/^Date:.*/Date: $date/;
    print;
}

程序里变量\$^I起了关键的作用,这个变量的默认值是undef,也不会对程序造成任何影响,但如果将其赋值成某个字符串,钻石操作符<>就会比平常有更多的魔力,该字符串会变成备份文件的扩展名。 例如现在正好打开了一个文件fred09.dat,此时perl会将文件重命名为fred09.dat.bak,然后创建一个新的空文件fred09.dat,现在钻石操作符会把默认的输出设定为这个新打开的文件。实际上,perl并没有编辑任何文件,只是创建了一个修改过的拷贝。如果把\$^I设为空字符串,则不会有备份文件。

从命令行直接编辑

perl的命令行选项设计十分巧妙,让你只用极少的按键就能建立一个完整的程序。

perl -p -i.bak -w -e 's/Randall/Randal/g' fred*.dat

-p选项可以让perl自动生成一小段程序,看起来类似于:

while(<>){
    print;
}

如果不需要太多功能,可以选用-n选项,这样可以把自动执行的print语句去掉。

-i.bak作用跟\$^I =".bak"是一样的,如果你不想备份,直接列出-i,但不加扩展名。

-e选项告诉perl后面是可供执行的程序代码。也就是说,s/Randall/Randal/g这个字符串会被直接当成perl程序代码,由于我们目前已经有个while循环,所以这段程序嗲吗会被放到print前面的位置,可以有多个-e选项,完成多段程序代码。因此上面的命令行程序其实相当于下面的代码:

1
2
3
4
5
6
#!/usr/bin/perl -w
$^I = ".bak";
while(<>){
    s/Randall/Randal/g;
    print;
}

Comments !