用户名: 密码: 忘记密码? 注册

Perl语言入门笔记:第十三天

作者:  时间: 2010-09-05
第九章:
用正则表达式来处理文本
之前我们知道了正则表达式如何进行匹配,现在我们也可以用它来修改文本。
用s///替换:
$_ = "He's out bowling with Barney tonight.";
s/Barney/Fred/;  #把Barney替换为Fred
print "$_\n";
如果匹配失败,则什么事都不会发生,变量也不受影响:
模式串与替换串还可以更加复杂:
s/with (\w+)/against $1's team/;
print "$_\n";   #"He's out bowling against Fred'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/fred/wilma/) {
  print "Successfully replaced fred with wilma!\n";
}

/g进行全局替换:
$_ = "home, sweet home!";
s/home/cave/g;
print "$_\n";        
#cave, sweet cave


==================================
$_ = "Input            data\t    may have extra whitespace.";
s/\s+/\s/g;            
#现在变成了"Input data may have extra whitespace."


#上面是将任何连续的空白转换成单一空格。如何删除开头和结尾的空白呢?


s/^\s+//;            
#删除开头的空白字符

s/\s+$//;            
#删除结尾的空白字符

=====================================
#!/usr/bin/perl -w

while (<>) {
chomp;
if (/\s+/) {
s/\s+/ /g;
}
print "$_\n";
}
#把一个文件里的所有行的任意空格都转成一个空格.

======================================
#!/usr/bin/perl -w

while (<>) {
chomp;
if (/\s+/) {
s/\s*/ /g;
}
print "$_\n";
}
#\s*匹配零次或一次空格.

================================

s/^\s+|\s+$//g  #去除开头和结尾的空白符

《Mastering Regular Expression》 

不同的定界符,就像m//与qw//一样,我也可以改变s///的定界符。但是替换运算会用以三个定界符,所以情况有点不同。
s#^https://#http://#;  #以井号作为定界符

如果是使用有左右之分的成对字符,就必须使用两对:一对圈引模式,一对圈引替换字符串。下面三行是一样的意思。
s{fred}{barney}
s[fred](barney);
s<fred>#barney#;

可选修饰符:
s#wilma#Wilma#gi;  #将所有的WiLmA或者WILMA等一律替换为Wilma
s{_END_.*}{}s;   #将_END_标记和其后所有的内容都截掉。

绑定操作符:
$file_name =~ s#^.*/##s;  #将$file_name中所有的Unix风格的路径全部去除

大小定转换:
$_ = "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表示全部转小写,但首字母大写:
s/(fred|barney)/\u\L$1/ig;   # $_现在成了" I saw Fred with Barney."   #注意\u\L的位置没有影响。

split操作符:
split操作符会根据分隔符拆开一个字符串。这对处理被制表符、冒号、空白或任意符号分隔的数据相当有用。用法如下:
@fields  = split /separator/, $string;
split操作符用拆分模式串”扫过“指定的字符串,并返回字段(也就是子串)列表。期间只要模式在某处匹配成功。该处就是一个字段的结尾、下一个字段的开头。

@fields = split /:/, "abc:def:g:h";  #得到 ("abc", "def", "g", "h")

如果两个分隔符连在一起,就会产生空字段:

@fields = split /:/, "abc:def::g:h";  #得到 ("abc", "def", "","g", "h")

这里有个规则,split会保留开头处的空字段,并省略结尾处的空字段。如果要保留结尾处的空字段,只要以-1作为split的第三个参数说明就能保留它们了。

利用/\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+/为模式,只是它省略开头的空字段。

join函数:
join函数不会使用模式,它功能与split恰好相反:split会将字符串分解为数个片段(子字符串),而join则会把这些片段联合成一个字符串。

my $result = join $glue, @pieces;
my $x = jion ":", 4, 6, 8, 10, 12;   # $x 为 ”4:6:8:10:12"

my @values = split /:/, $x;    #@values为(4, 6, 8, 10, 12)
my $z = join "-", @values;     #$z 为 "4-6-8-10-12"

列表上下文中的m// :
在列表上下文中使用模式匹配操作符(m//)时,如果模式匹配成功,那么返回的是所有捕获变量的列表;如果匹配失败,则会返回列表:
$_ = "Hello there, neighbor!";
my ($first, $second, $third) = /(\s+) (\s+), (\s+)/;
print "$second is my $third\n";

my $text = "Fred dropped a 5 ton granite block on Mr. Slate";
my @words = ($text =~ /([a-z]+)/ig);
print "Result: @words\n";
#打印:Result: Fred dropped a ton granite block on Mr Slate

更强大的正则表达式:
非贪婪量词:
我们前面二章看到的4个量词全都是贪婪量词。也就是说在保证整体匹配的前提下,它们会尽量匹配长字符串,实在不行才会吐出一点。如:
/fred.+barney/来匹配fred and barney went bowling last night这个字符串。
这里的+会在匹配完fred时,它人匹配换行符之外的所有字符(至少一次)。
因为加号量词(+)是个贪婪的量词,它会尽量匹配多的字符串。所以它会一路匹配到night,
而使barney没有办法进行匹配了。但+匹配完后,现在轮到模式中的barney部分,但是它已经没办法进行匹配,因为刚才已经进行到字符串的最后面。此时,.+模式就会很不情愿的吐出一个字符,反正就算少了一个字符,这部分模式还算是匹配成功。(虽然它很贪婪,不过为了顾全大局,让整体匹配都成功,就算自己没有匹配到全部的字符串也可以忍受。).+匹配的部分一路减少到了barney之前。于是整个模式也就匹配成功了。

上面为什么说这么多呢。因为这样我们可以知道回溯的动作是非常繁琐的。因为量词囫囵天下太长的字符串,所以效率不高。

 

/fred.+?barney/来匹配fred and barney went bowling last night这个字符串。
这里的?它会匹配一个以上的字符,但是越短越好。也就是最好一个字符。所以它匹配的部分是fred后面的空白符,接下来出现的barney模式就会失败。而.+?模式又很不情愿地多匹配了一个字符,然后把控制权交给之后的模式重试,但barney还是匹配失败。所以.+?只好再吞下一个字符n直到barney匹配上了。这个模式也就匹配成功了。

上面这两个这个例子就说明。从前面匹配比从后面匹配效率高了很多。
如果要处理的数据都是fred在字符串开始处,barney在字符串尾。那么选用贪婪的量词反而会比较快。
所以最终速度其实取决于正则表达式处理的数据。

非贪婪的量词并不只跟效率有关。尽管只要贪婪版本可以成功匹配的字符串,它们也同样可以匹配成功(匹配失败的情况也是一样的),但是它们匹配的字符串长度是不同的。

I'm taking about the cartoon with Fred an <BOLD>Wilma</BOLD>!
我们可以用s#<BOLD>(.*)</BOLD>#$1#g来替换掉。这里没有问题。但是我们来看下面:
I thought you said Fred and <BOLD>Velma</BOLD>, not <BOLD>Wilma</BOLD>
如果还用上面的贪婪量词来匹配的话。该模式就会第一个<BOLD>匹配直到最后一个</BOLD>,把这中间的部分全部取出来了。这就错了。
像这种情况我们就需要用非贪婪量词s#<BOLD>(.*?)</BOLD>#$1#g;

问号也有非贪婪的版本:??,虽然这还是一样会匹配到一次或零次,但会优先考虑零次的情况。

跨行的模式匹配:
传统的正则表达式都是用来匹配单行文本。由于Perl可以处理任意长度的字符串,其模式匹配也可以处理多行文本,与处理单行文本无差异。
$_ = "I'm much better\nthan Barney is\nat bowling,\nWilma.\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;

一次更新多个文件:
程序化地更新文件内容时,常见的做法就是先打开一个新文件,然后把跟旧文件相同的内容写进去,并且在需要的位置进行改写。后面会看到,这样做和直接更新文件的做法效果大致相同。

如:我们现在有几百个格式类似的文件。其中一个叫做fred03.dat,里面都是如下几行的内容:
Program name: granite
Author: Gilbert 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
Date: June 12, 2008 6:38 pm
Version: 2.1
Size: 21k
Satus: Final beta

我们要做三项改动:Author  Date  要改。phone则要删除。
要在Perl中直接修改文件内容可以使用钻石操作符(<>),些程序的新意在于特殊变量$^I的使用。

#!/usr/bin/perl -w


use strict;

chomp(my $date = `date`);
#这里也可以用chomp(my $date = localtime);

$^I = ".bak";

while (<>) {
s/^Author:.*/Author: Randal L. Schwartz/;
s/^Phone:.*\n//;
s/^Date:.*/Date: $date/;
print;
}

钻石操作符他会自动帮你打开许多文件,而且如果没有指定文件,它就会从标准输入读进数据。但如果$^I中是个字符串,该字符串就会变成备份文件的扩展名。

先假设钻石操作符正好打开了文件fred03.dat。除了像以前一样打开文件之外,它还会把文件名改成fred03.bat.bak。虽然打开的是同一个文件,但是它在磁盘上的文件名已经不同了。接着,钻石操作符会打开一个新文件并将它取名为fred03.dat。这么做并不会有任何问题。因为我们已经没有同文件了。现在钻石操作符会把默认的输出设定为这个新打开的文件,所以输出来的所有内容都会被写进这个文件。

从命令行进行在线编辑:

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

以Perl开头的命令作用如同在文件的开头写上#!/usr/bin/perl表示以perl程序来处理随后的脚本。
-p选项可以让Perl自动生成一小段程序,看起来类似如下的片段
while (<>) {
  print;
}

如果不需要这么多功能,还可以用-n选项,这样可以把自动执行的print去掉。
下一个出现的选项是-i .bak,其作用就是在程序开始运行之前把$^I设为.bak。
-w选项是打开警告的意思。
-e选项用来告诉Perl后面跟着的是程序代码。也就是说,s/Randall/Randal/g这个字符串会被直接当成Perl程序代码。因为目前我们已经有个while循环(来自-p选项)。

以上所有片段组合在一起,就好像写了下面这个程序,并且用fred*.dat这个参数调用它一样:

#!/usr/bin/perl -w

$^I = ".bak";
while (<>) {
  s/Randall/Randal/g;
print;
}

#习题:

1、
/($what){3}//(fred|barney){3}/ 的模式是一样的。
如里写成/fred|barney{3}/ 就等于/fred|barneyyy/
=============================================
2、
#!/usr/bin/perl -w

while (<>) {
if (/fred/i) {
/fred/Larry/i
}
}
================
#!/usr/bin/perl -w

my $in = $ARGV[0];
unless (defined $in) {
  die "Usage: $0 filename";
}

my $out = $in;
$out =~ s/(\.\w+)?$/.out/;

unless (open IN, "<$in") {
  die "Can't open '$in': $!";
}

unless (open OUT, ">$out") {
  die "Can't write '$out': $!";
}

while (<IN>) {
  s/fred/Larry/gi;
  print OUT $_;
}
=============================
#!/usr/bin/perl -w

while (<IN>) {
chomp;
s/fred/\n/gi;            
#将所有的FRED替换为临时占位符

s/Wilma/fred/gi;        
#将所有的WILMA替换为Fred

s/\n/Wilma/g;            
#再将所有占位符换回为Wilma

print OUT "$_\n";
}
#要进行这种互换,我们首先要找到一个“占位符”,而且必须是不会出现在数据中的。因为使用了chomp(最后输出的时候会补上一个换行符),所以我们知道换行符(\n)是绝对不会出现在字符串中的。所以换行符就可以充当占位符。NUL字符(\0)也是另一个不错的选择。

==================================
4、
#!/usr/bin/perl -w

$^I = ".bak";            
#准备备份

while (<>) {
  if (/^
#!/) {            #是 #!开头的那行吗?

  $_ .= "## Copyright (C) 20XX by Yours Truly\n";
}
print;
}
=====================
#!/usr/bin/perl -w

my %do_these;
foreach (@ARGV) {
  $do_these($_) = 1;
}
while (<>) {
  if (/^
## Copyright/) {

    delete $do_these{$ARGV};
  }
}
@ARGV = sort keys %do_these;
$^I = ".bak";        
#准备备份

while (<>) {
  if (/^
#!/) {        #是 #!开头的那行吗?

  $_ .= "## Copyright (c) 20XX by Yours Truly\n";
}
print;
}