Perl正则表达式(3) - 用正则表达式处理文本

版权声明:转载请说明出处! https://blog.csdn.net/qq_39556143/article/details/85032990

用正则表达式处理文本

3.1 替换操作

3.1.1 用s///进行替换操作 (substitution)

  • 如果把m//想象为文理处理器的字符串“查找”功能,那么s///就是查找并替换功能。
    • 如:s/Barney/Fred/; 可以实现将Barney替换为Fred的处理。
  • 与m// 和 qw//类似,s///也可以使用其他定界符
    • 如:s{barney}{fred};s#barney#%fred%;
  • 与m//类似,s///可以使用绑定操作符 =~
  • m//和s///的区别:
    • m//可以匹配任何字符串表达式;
    • s///修改的数据必须预先存放在某个变量中;
    • 这个变量一般位于该操作符的左边,也称作左值(lvalue)
  • 如果匹配失败,什么也不会发生,原来变量里的内容不受影响。
    • s///匹配替换成功返回真,否则返回假
#!/usr/bin/perl

$_ = "He is out with Barney tonight";
s/Barney/Fred/;    #$_的值变为:"He is out with Fred tonight"
s/Wilama/Fred/;    #$_的值不变,仍为:"He is out with Fred tonight"
#模式字符串 & 替换字符串可以更复杂
s/with (\w+)/against $1s team/;  #$_的值变为:"He is out against Freds teram tonight"
# 下面是一些替换操作的例子:
$_ = "green scaly dinosaur";
s/(\w+) (\w+)/$2, $1/;            #替换后为:"scaly, green dinosaur"
s/\A/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/gegantic/;                 #替换后为:"genantic (huge!)dinosaur"

3.1.2 用/g修饰符进行全局替换

也许你已经注意到,s///只替换第一个匹配的字符串,这是s///默认行为,我们可以使用/g修饰符(global)实现全局替换。

  • 除了/g之外,/i /s /x等模式修饰符同样适用于替换操作。
    下例中给出几种常用的全局替换:
$_ = "home, sweet home"
s/home/cave/g;   #$_的值变为: "cave, sweet cave"
#缩减空白
s/\s+/ /g;         #将多个空白符转换为单一空格
s/\A\s+//;         #将行首的多个空白删除;
s/\s+\z//;         #将行尾的多个空白删除;
s/\A\s+|\s+\z//g;  #将行首或行尾的空白删除;

3.1.3 s///的其他操作

(1)非破坏性替换
如果需要保留原始字符串和替换后的字符串,需要使用非破坏性替换

#!/usr/bin/perl

my $original = "Fred eat 1 rib";
(my $copy = $original) =~ s/\d+ ribs?/10 ribs/;     #先执行括号内的赋值操作,后执行替换操作,替换的时$copy中的值
#v5.14之后 perl加入了/r修饰符,保留原来变量中的值不变,而将替换结果作为返回值。
my $copy = $original =~ s/\d+ ribs?/10 ribs/r;      #绑定操作符=~优先级高于赋值操作=,先绑定后赋值

(2)大小写转换
在替换运算中,常常需要把所替换的单词全部换为大写或者小写,此时只需要用特定的反斜线定义就可以了。

转义符 举例 说明
\U s/(fred barney)/\U$1/
\L s/(FRED BarNey)/\L$1/
\u s/(fred barney)/\u$1/
\l s/(FRED BARNey)/\l$1/
\E s/(fred)(barney)/\U$1\E\u$2/ \E表示\U的结束位置
\L\u s/(FRED BARNey)/\L\u$1/
  • 大小写转换也可以使用perl中的内置函数
#!/usr/bin/perl
my $start    = "Fred";
my $upcapp   = lc($start);         #fred 全小写
my $uppered  = uc($upcapp);        #FRED 全大写
my $lowered  = lc($uppered);       #fred 全小写
my $capped   = ucfirst($lowered);  #Fred 首字母大写
my $folded   = fc($uncapp)         #fred 

(3)元字符转义
\Q会把后续字符串中出现的有元字符自动改为本意字符

#!/usr/bin/perl

if ( s/(((Fred/fred/ ) {         #编译时就会报错
    print "Repalced name\n"; 
}
if ( s/\(\(\(Fred/fred/ ) {      #编译通过,看起来凌乱不堪
    print "Repalced name\n"; 
}
if ( s/\Q(((Fred/fred/ ) {       #编译通过,简洁明了
    print "Repalced name\n"; 
}
if ( s/\Q(((\E(Fred)/fred/ ) {   #编译通过,可以使用\E结束元字符转义
    print "Repalced name\n"; 
}

3.2 正则表达式中的函数

3.2.1 split操作符

split操作符会将给定模式字符串拆分为一组列表,只要将分隔符写成模式(单个字符就是最简正则表达式),就可以用split分解数据。
用法: my @field = split /seperator/, $string;

  • split操作符用拆分模式扫描指定的字符串并返回子字符串构成的字段列表;
  • 期间只要模式在某处匹配成功,该出就是当前字段的结尾,下一字段的开头;
#!/usr/bin/perl
my @field = split /:/, "abc:ad:afg";        #@field = qw/'abc','ad','afg'/
my @field = split /:/, "abc:ad::afg";       #@field = qw/'abc','ad','','afg'/   两个分隔符连在一起,就会产生空字段
my @field = split /:/, ":::a:b:c:::";       #@field = qw/'','','','a','b','c'/  split会保留开头处的空字段,却会忽略末尾的空字段
my @field = split /:/, ":::a:b:c:::",-1;    #@field = qw/'','','','a','b','c'/  将第三个参数设置为-1,split机会保留末尾的空字段
my @field = split /\s+/, "a  b   c";        #@field = qw/'a','b','c'/           可以使用\s+匹配多个空格字符
my @field = split;                          #默认使用空白字符串\s+作为模式分解$_中的字符串, 基本等同于split /\s+/,$_;但是省略写法会忽略开头的空字符串

3.2.2 join函数

join函数不使用模式,他的功能和split操作符相反: join函数可以将一些字段拼接位一个字符串
用法: my $string = join $glue, @field;

  • join的参数定义:
    • 第一个参数 g l u e ( ) , j o i n glue(不是模式), join用 glue的值作为胶水将字段拼接起来
    • 第二个参数@field,为等待拼接的字段数组
#!/usr/bin/perl
my $x = join ":",4,6,8,10;      #$x为 "4:6:8:10"
my $x = join "foo","bar";       #只有一个带拼接字段,join函数不工作
#split和join的配合使用
my @value  = split /:/,"4:6:8:10";    #@value = qw(4,6,8,10)
my $string = join  "-",@value;        #$string 为 "4-6-8-10";

3.2.3 列表上下文中的m//

在使用split的时候,模式指定的只是分隔符:分解得到的字段未必是我们需要的数据。有时候通过模式指定想留下的部分反而比较简单。

  • 在列表上下文中使用模式匹配操作符m//的时候,返回的时所有捕获变量的列表,如果匹配失败返回空列表。
  • 在s///中用到的/g修饰符同样可以用在m//中,这样m//可以匹配字符串的多个地方
$_ = "hello there, neighbor";
my ($first,$second,$third) = /(\S+) (\S+), (\S+)/;    #类似于($first,$second,$third) = ($1,$2,$3) 这样我们就可以给变量起好听的名字了
# 由于未使用 =~ 绑定操作符,所以模式匹配默认针对$_进行

my $text  = "Fred dropped a 5 ton granite block on Mr. Slate";
my @words = ($text =~ /([a-z]+)/ig) ;
print "Result: @word\n";      #打印 Result:Fred dropped a ton granite bnlock on Mr Slate
#类似于split的逆功能:正则表达式表示不想去除部分,反而是要留下的部分

my $text  = "Fred Flistone Barney Rubble";
my %words = ($text =~ /(\w+)\s+(\w+)/g) ;
#每次匹配成功,就返回一对被捕获的值,而这一对值正好成为新哈希的key-value对

3.3 更强大的正则表达式

3.3.1 分贪婪量词

目前为止,前面介绍的量词都是贪婪(greedy)量词,他们按照左起最长原则尽可能多的匹配文本,但有时候匹配过宽。

#!/usr/bin/perl
#代码1
my $text = '<b>Fred</b> and <b>Barney</b>';
$text =~ s/<b>(.*)</b>/<b>\U$1</b>/g;                        #我们希望通过全局匹配,完成两个名字的替换,但是perl却将整个字符串进行了转换。
print "$text\n";                                             #打印: <b>FRED</B> AND <B>BARNEY</b> 
#代码2
my $text = '<b>Fred</b> and <b>Barney</b>';
my match_count = $text =~ s/<b>(.*)</b>/<b>\U$1</b>/g;       #(.*)属于贪婪匹配,从第一个<b>开始一直到最后一个</b>为止。
print "$match_count: $text\n";                               #打印: 1: <b>FRED</B> AND <B>BARNEY</b> 
#代码3
my $text = '<b>Fred</b> and <b>Barney</b>';
my match_count = $text =~ s/<b>(.*?)</b>/<b>\U$1</b>/g;      #(.*?)属于非贪婪匹配,从第一个<b>开始查找最近符合条件的</b>为止。
print "$match_count: $text\n";                               #打印: 2: <b>FRED</b> and <b>BARNEY</b> 

表3.3 使用非贪婪修饰符的正则表达式量词

元字符 意义
?? 匹配零个字符(没意义)
*? 零个或者多个,越短越好
+? 一个或者多个,越短越好
{3,}? 至少三个,越短越好
{3,5} 至少三个,最多五个,越短越好
{3}? 正好三个

3.3.2 单词边界符扩展

\b转义匹配“单词”和“非单词”字符间的交替边界。但是Perl认为的单词是字母,下划线和数字组合,其中不包括单引号。

#!/usr/bin/perl
#代码 1
my $string = "This doesn't capitalize correctly.";
$string =~ s/\b(\w)/\U$1/g;
print "$string\n";                                           #打印: This Doesn'T Capitalize Correctly.
#代码 2 (v5.22增加了基于Unicode的新单词边界符)
my $string = "This doesn't capitalize correctly.";
$string =~ s/\b{wb}(\w)/\U$1/g;                              #\b{wb}是\b的扩展,将单引号作为单词的一部分
print "$string\n";                                           #打印: This Doesn't Capitalize Correctly.
#代码 3 (v5.22增加了基于Unicode的新单词边界符)
my $string = "Mr. Flintstone doesn't capitalize correctly.";
$string =~ s/\b{sb}(\w)/\U$1/g;                              #\b{sb}是\b的扩展,可以智能分别字符.是否是句子的末尾
print "$string\n";                                           #打印: This Doesn't Capitalize Correctly.

3.3.3 跨行模式匹配

传统的正则表达式用来匹配单行文本,由于perl可以处理任意长的字符串,其模式匹配自然也可以处理多行文本,其实和处理单行文本并不太大差别。
如标量变量: $_ = “I’m much better\nthan Betty is\nat bowling,\nWilma.\n”; #包含4行的文本
使用^和$可以匹配整个字符串的开始和结束,但是加上/m修饰符之后(multiple lines),就可以匹配字符串内的每一行,这样他们代表的位置就不是整个字符串的开头和结尾了,而是每一行的开头和结尾。
例: print “Found ‘wlima’ at start of line\n” if /^wilma\b/im;

3.3.4 一次更新多个文件

通过程序自动更新文件内容时,最常见的做法就是先打开一个和原来内容一致的文件,然后在需要的位置进行更改,最后用修改的文件替换原来的文件。
如果现在有几百个格式类似的文件,其中一个叫做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
Company: RockSoft
Department: R&D
Date: June 12, 2018 6:38 pm
Version: 2.1
Size: 21k
Status:Final beta

我们需要编写以下程序:

#!/usr/bin/perl
use strict;
chomp(my $date = `date`);            #使用程序命令date获取当前时间,也可以使用perl的内置函数:my $date = localtime;(两者格式稍有不同)
$^I = ".bak";                        #$^I变量的默认值为undef,表示什么都不发生,如果将其赋值为某个字符串,钻石操作符<>就会焕发出新的力量!
                                     #(1)当$^I的值为".bak",使用<>时,<>先打开fred03.dat,并经其名称改为fred03.dat.bak,虽然是同一个文件但是磁盘上的文件明已经不同;
				     #(2)<>创建一个新的名称为fred03.dat的文件,并且把默认输出改为这个新打开的文件,所有的输出都会被写进这个文件;
				     #(3)这样while循环会从旧文件读取一行输入,做一些改动之后把新的内容写进新文件。
				     #结果:原来的文件被拷贝至fred03.dat.bak,并且将修改后的文件替换原来的文件fred03.dat(共两个文件)

while(<>){
    s/\AAuthor:.*/Author: Randal/;   #更改文件作者
    s/\APhone:.*\n//;                #将Phone那行用空字符代替,注意换行符也被替换了,所以相当于删除此行
    s/\ADate:.*/Date: $date/;        #将日期改为当前时间,可以使用变量内插
    print;                           #将处理后的文档打印出来
}

有人会把$^I的值设为~这个字符,因为emacs在处理备份文件的时候的文件名也是这么做,如果把$^I设为空字符串,就会直接修改文件的内容,但是不会留下任何备份。一旦我们在模式重大错任何一个字符,就可能将整个文件数据全部清空。

3.3.5 从命令行直接替换文件内容

我们也可以通过命令行对文件进行一些相对简单的修改。比如上例中我们不小心将Randal写成了Randall,就可以用以下命令进行多文件的修改。
perl -p -i.bak -w -e ‘s/Randall/Randal/’ fred*.dat
其中:
perl:
-> 作用如同文件中的#!/usr/bin/perl,表明使用perl处理随后的脚本;
-p:
-> 让perl自动成一段小代码 while(<>) { print; } 如果不需要这么多功能,可以使用-n把程序中的print去掉。
-i.bak:
-> 在程序开始之间将$^I变量设置为".bak" 如果不想做备份,可以直接用-i,不加扩展名
-w:
-> 开启警告功能
-e:
-> 告诉perl后面跟着的是可以执行的程序源代码,即s/Randall/Randal/会被直接当成perl的程序代码,因为前面已经有while循环了,这些代码会自动加到循环中print前面的位置。
fred*.dat
-> 表示将@ARGV的值应该是匹配此文件名模式的所有文件名。

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

#!/usr/bin/perl
$^I = ".bak";
while(<>){
    s/Randall/Randal/;
    print;
}

猜你喜欢

转载自blog.csdn.net/qq_39556143/article/details/85032990