生物信息编程Perl

Effective Perl-chapter3

2019-06-17  本文已影响1人  chensole

今天这个主要是介绍perl语言中的正则表达式,perl的正则表达式本身就相当于一门语言,而且这门语言甚至比perl更复杂。

了解正则表达式操作符的优先级

“正则表达式”一词中包含表达式,是因为构成 和解析正则表达式的语法近似于算术表达式,虽然二者的作用不同,但理解二者的相似性,有助于写出更严谨的正则表达式。

正则表达式由原子和操作符组成,原子是构成正则表达式的基本单位,通常是指仅匹配单个字符的匹配模式
a #匹配字母a
$ #匹配字符$
\n #匹配换行符
[a-z] #匹配任意一个小写字母
. #匹配除\n以外的任意字符
\1 #反向引用所匹配到的第一组捕获内容
此外还有一些特殊的“零宽度”原子,如
\b #单词边界
^ #匹配子串行首位置

原子是由正则表达式操作符修饰或联结在一起的,正则表达式操作符之间也是有优先级别的

正则表达式的优先级
正则表达式只有四层优先级,圆括号和其他分组操作符拥有最高优先级

2019-06-13 20-47-00 的屏幕截图.png
两个原子顺序排列称之为序列,虽然没用标点符号,但序列也是一种操作符
^ed | jo $         #匹配行首为ed或行尾为jo
^(ed | jo ) $     #匹配只有ed或只有jo的行

使用正则表达式的捕获功能

借助正则表达式的捕获功能,我们可以从子串中自由提取感兴趣的部分

捕获变量$1...

在使用正则表达式解析并捕获文本时,经常用到捕获变量$1...等,正则表达式内的每对括号都会捕获括号内匹配的文本,并将其存储到捕获变量中。

$_ = 'http://www.perl.org/index.html';
if (m#^http://([^/]+)(.*)#) {
        print "host = $1\n";      # www.perl.org
        print "path = $2\n";      # /index.html

}

即使圆括号内的表达式能在子串内匹配多次,它也只会捕获最后一次匹配的内容

$_ = 'ftp://ftp.uu.net/pub/systems';
if (m#ftp://([^/]+)(.*)#) {
        print "host = $1\n";      # ftp.uu.net
        print "path = $2\n";      # /systems
}

要找出捕获变量数字和圆括号间的对应关系,不管括号嵌套多复杂,只需从左依次数左括号的序数即可

捕获的反向引用
正则表达式本身可以用反向引用匹配或调用之前捕获的内容,以原子\1,\2,\3等表示

关于反向引用有一个非常典型的例子,即单词重复的处理

/(\w)\1/        #找出连续重复两次的字符

捕获并替换
捕获及匹配变量常常用于替换操作

s/(\S+)\s+(\S+)/$2 $1/           #交换两个单词的位置

要是替换的目的是去除匹配的内容,而非保留改写,那就不要使用捕获

删除行首空格的两个方法
s/^\s*(.*)/$1/;      #使用了捕获
s/^\s+//;      #不使用捕获,简单

列表上下文中的匹配
在列表上下文中,匹配操作会根据所捕获的缓存,返回与其对应的列表,如果匹配失败则返回空列表,这应该最常用的特性之一了,只需一步便能扫描并同时分割子串

my ($name ,$value) = /^([^:]*):\s*(.*)/;
my $subject = (/^subject:\s+(re:\s*)?(.*)/i)[1];

使用更精确的空白字符组

空白字符包括水平空白字符和垂直空白字符(换行符),perl正则表达式提供了各种精确匹配空白字符的方法
水平空白字符
在Perl5.10之前,预先定义好的有关空白字符处理的只有两组。一组是\s,匹配所有的空白字符,另一组是\S,匹配所有的非空白字符,使用这种方式稍有不慎就会到导致意料之外的结果

my $string = <<'HERE';
this       is      a      line
this        is      another    line
and    a       final      line
HERE
假设你想把连续空白字符替换成单个空白字符
$string =~ s/\s+/ /g;      # 由于换行符也属于空白字符,所以会将行换符也换成空格,变成单行文本
# this is a line this is another line and a final line

从perl 5.10过后,我们就可以用\h字符组匹配任意水平空白字符
use 5.010;
$string =~ s/\h+/ /g;

同样\H会匹配除了水平空白字符以外的所有字符

垂直空白字符
垂直空白字符分为回车符、换行符、换页符、垂直制表符等等

#查看某段文本是否为多行文本
use 5.010;
if ($string =~ /\v/) {
        say 'found a multiline string';
}

#把多行文本按行切分
my @line = split /\v/,$string;

同样\V可以匹配除了垂直空白字符外的任意字符

行终止符
长期以来,对于行终止符的处理,一直很麻烦。因为不但有换行符,还有回车符,其组合方式也不一样,有的文件用换行符结束当前行,有的文件用回车符加上换行符表示换行

linux用一个\n表示换行
windows换行加回车表示换行\n\r
Mac用回车表示换行
windows文件在Unix/Mac下打开的话,每行结尾多出一个^M符号
为了避免这种麻烦,perl 5.10 特意引入的\R字符组,以更简洁的方式匹配各种类型的行终止符

use 5.010;
$string =~ s/\R/\n/g;      #现在所有形式的行终止符都变成简单统一的换行符了

非换行符
perl 5.12还引入了一种新的用来表示非换行符的字符组\N

之前我们可以用.匹配任意非换行符的字符
if ($string =~ /(.+)/) { ... }

用s修饰符可以使.能够匹配换行符
if ($string =~ /(.+)/s) { ... }

如果我们只是匹配非换行符,就可以显示使用\N匹配,不必担心/s来捣乱
use 5.012;
if ($string =~ /(\N+)/) { ... }

使用命名捕获,给匹配加标签

有时正则表达式中出现的括号太多,记忆变量编号与圆括号间的对应关系相当麻烦

$_ = 'buster and mimi';
if (/(\S+) and (\S+)/) {
        my ($first,$second) = ($1,$2);
        ...;
}
程序完成后若需要变更,可能会加上更多括号,但难免忘记修改捕获编号

$_ = 'buster or mimi';
if (/(\S+) (and | or) (\S+)/) {
        my ($first,$second) = ($1,$2);     #错误
        ...;
}
在perl 5.10后面可以将捕获加个标签从而不用记忆编号和捕获的对应关系(?<LABEL>),捕获的内容存储在%+哈希中

$_ = 'buster or mimi';
if (/(?<first>\S+) (and | or) (?<second>\S+)/) {
        my ($first,$second) = ($+{first},$+{second});     #错误
        ...;
}

同样适用于反向引用\k<LABEL>
$_ = 'buster or buster';
if (/(?<first>\S+) (and | or) \k<first>/) {
        say "i found the same name twice";
}

仅需分组时,用非捕获括号

圆括号在正则表达式中,有两种截然不同的作用: 分组和捕获。一般来说,这两种功能并在一起使用还是廷方便的

匹配邮件的标题行subject:,忽略可能出现的回复前缀,直白写出来会用到两组圆括号
my ($bs,$subject) =~ /^subject:\s+(re:\s*)?(.*)/i;
其中第一个圆括号的作用是分组,但再将内容捕获就显得多余
因此提供了解决方法,非捕获括号(?:)的用法和普通括号相同,唯一的区别在于,它不会创建反向引用或捕获变量

my ($subject) =~ /^subject:\s+(?:re:\s*)?(.*)/i;      #非捕获括号很简洁

在split中使用非捕获括号,以便禁用分隔符保留模式,一般情况下,在split中捕获的分隔符,也会返回到输出的列表中
my $string = "1:2;3:4";
my @items = split /(:|;)/,$string;      #此时@item中有(1  :  2  ;  3  :  4)

而借助非捕获括号,可以解决分隔符保留的问题
my @items = split /(?::|;)/,$string;  

能懒则懒,不要贪婪

一般情况下,perl正则表达式默认总会返回它所能找到的“最左最长”匹配,即在字符串中找出第一个能匹配且尽可能长的字串。像*和+这样表示重复次数的操作符,会“吃进”尽可能多的字符

$_ = "greeting, planet earth\n";
/\w+/;      #匹配greeting
/\w*/;      #匹配greeting
/n[et]*/;    #匹配greeting中的n
/n[et]+/;    #匹配planet中的net
/G.*t/;          #匹配greeting, planet eart

一般我们总是希望以贪婪模式匹配,不过也有例外
$_ = "this 'test' isn't successful?";
my ($str) =~ /(' .* ')/;      #匹配test' isn

my ($str) =~ /('[^']+')/;      #成功匹配test,较麻烦
对于任意重复操作符(*,+,{m,n}),在后面加上?就会变成非贪婪模式,即懒惰模式
my ($str) =~ /(' .*?')/;        #成功匹配test

用零宽原子匹配字符中的特定位置

有时我们会遇到仅需匹配某种条件,而非某个字符的情况。比如定位单词边界,字串起始、结束位置,或是子串中文本行的起始、结束位置。perl提供了锚位操作符,以匹配上述位置,它们只是条件判断,不会匹配任何字符

用\b判断单词边界

my $string = "happy apple\n";
print "found\n" if $strint =~ /apple/;      #found

my $string = "happyapple\n";
print "found\n" if $strint =~ /\bapple/;  #不打印found

用^匹配起始位置
^用来匹配字符串的其是位置,许多人误认为这是“行起始”锚点,是因为他们只用它处理过单行文本

my $string = <<'HERE';
this       is      a      line
this        is      another    line
and    a       final      line
HERE
my (@matches) = $string =~ /^(\w+)/g;      #只匹配this
如果要匹配每行的第一个单词,可以使用/m修饰打开多行模式。/m会改变^的行为,使它不但匹配字串开始,还能匹配换行符后的位置
my (@matches) = $string =~ /^(\w+)/mg;

用$匹配结束位置

$通常用于匹配字串结束位置,即便此处有换行符也能匹配

if ("some text\n" =~ /text$/) {
        print "Matched 'text'\n";
}
若加上/m修饰符,可以匹配多行字串内换行符前的位置
print "fred\nquit\ndoor\n" =~ /(..)$/mg;      #返回editor

简单字符处理应避免正则表达式

正则表达式用起来很顺手,但对于各种字符串操作,正则表达式并不总是最高效的手段,像提取字符子串、转换字符之类的简单任务应该使用特定的函数,比如index、rindex、substr以及tr///等等。

字串比较操作符
如果要比较两个字串是否相同,可以用字串比较操作符,而非正则表达式

# the fast way
if ($answer eq 'yes') { ... }

#the slow way
if ($answer =~ /^yes$/) { ... }

用index和rindex提取子串
index操作符可以在较长字串中定位一个较短子串的位置

#从$big_str里寻找$little_str的位置,起始下标是0
my $pos = index $big_str, $little_str;

# rindex表示从字串右边开始查找短的子串,但位置计算方式仍是从左到右
my $pos = rindex $big_str, $little_str;

用substr提取或修改子串
substr操作符的作用是根据给定起始位置和子串长度提取字符串中的一部分,如果没有指定长度,则从指定位置开始一直提取到字串末尾的字符

my $perl = substr "It's a perl world",7,4;   #提取子串perl
my $perl = substr "It's a perl world",7;      #提取perl world

#如果把substr表达式放在赋值操作符左边,就是子串替换操作
my $world = "It's a perl world";
substr ($world,7,4) = "mad mad mad mad";

将index和substr结合起来,还可以实现类似于s///替换的功能,如果目的只是替换直接使用s///,不但更快,意义更明确

substr (index ($world,perl),4) = "mad mad mad mad";

#代码更清晰,速度也更快
$world =~ s/perl/mad mad mad mad/;

批量转换单个字符
如果想把某个字符一次性全部转换为另一个,没必要使用正则表达式替换

$string =~ s/a/b/g;

不要忘了还有tr///操作符,虽然形式相近,但它不是正则表达式,它所做的只是字符间的转换,默认全局转换

$string =~ tr/a/b/;

#如果只想对子串的某一部分作字符转换,可以使用substr限定范围
substr ($string ,0,10) =~ tr/a/b/;

提高正则表达式的可读性

我们可以让正则表达式变得更加易读,尤其是准备将代码与他人分享,或者将来还要自己动手维护这些代码,提升代码可读性就变得很重要

使用辅助空白字符和注释
一般情况下,正则表达式中的空白字符都是有意义的。/x选项可以让正则表达式解析器忽略空白字符,同时忽略注释,因此添加空格可以清晰的理解正则表达式中每个部分的含义

/'(?:\\'| .)*?'/
/' (?:  \\'   |   .   )*?   '/x;    #简洁易懂

避免不必要的回溯

正则表达式中的多选结构运算通常是比较慢的,这是由正则表达式引擎的工作方式决定的。当多选结构中的一个分支失败时,引擎会在字串中"回溯"到之前的位置,尝试下一个分支

while (<>) {
        print if /\b(george|jane|judy|elroy)\b/;
}
#实现机制:首先找到单词边界,尝试匹配george,若失败则退回单词边界处,尝试匹配下一个名字,若成功,则寻找下一个单词边界,依次类推

#当备选项彼此相近时,回溯问题尤其严重
'aaabbbccg' =~ /\b(aaabbbccc|aaabbbccd|aaabbbcce|aaabbbccf)\b/;

#每个备选项前半部分都是相同的,为了避免不必要的回溯,有一种解决方法是建立检索树,将所有备选项的相同前缀提取出来,以便把回溯次数降至最低
#从perl5.10开始,正则表达式引擎可以自动为备选文本建立检索树

用字符组[abc]代替多选结构
有些情况下完全没必要使用多选结构,应该尽量避免

while (<>) {
        push @var,m'(($|@|%|&)\w+)'g;
}
#由于匹配的全是单个字符,因此用字符组表示可选就足够了
while (<>) {
        push @var,m'([$@%&]\w+)'g;
}

这个正则表达式先说到这里,后面还要深入学习!

上一篇下一篇

猜你喜欢

热点阅读