Effective Perl-chapter3

今天這個主要是介紹perl語言中的正則表達(dá)式,perl的正則表達(dá)式本身就相當(dāng)于一門語言,而且這門語言甚至比perl更復(fù)雜。

了解正則表達(dá)式操作符的優(yōu)先級

“正則表達(dá)式”一詞中包含表達(dá)式,是因?yàn)闃?gòu)成 和解析正則表達(dá)式的語法近似于算術(shù)表達(dá)式,雖然二者的作用不同,但理解二者的相似性,有助于寫出更嚴(yán)謹(jǐn)?shù)恼齽t表達(dá)式。

正則表達(dá)式由原子和操作符組成,原子是構(gòu)成正則表達(dá)式的基本單位,通常是指僅匹配單個字符的匹配模式
a #匹配字母a
$ #匹配字符$
\n #匹配換行符
[a-z] #匹配任意一個小寫字母
. #匹配除\n以外的任意字符
\1 #反向引用所匹配到的第一組捕獲內(nèi)容
此外還有一些特殊的“零寬度”原子,如
\b #單詞邊界
^ #匹配子串行首位置

原子是由正則表達(dá)式操作符修飾或聯(lián)結(jié)在一起的,正則表達(dá)式操作符之間也是有優(yōu)先級別的

正則表達(dá)式的優(yōu)先級
正則表達(dá)式只有四層優(yōu)先級,圓括號和其他分組操作符擁有最高優(yōu)先級

2019-06-13 20-47-00 的屏幕截圖.png

兩個原子順序排列稱之為序列,雖然沒用標(biāo)點(diǎn)符號,但序列也是一種操作符

^ed | jo $         #匹配行首為ed或行尾為jo
^(ed | jo ) $     #匹配只有ed或只有jo的行

使用正則表達(dá)式的捕獲功能

借助正則表達(dá)式的捕獲功能,我們可以從子串中自由提取感興趣的部分

捕獲變量$1...

在使用正則表達(dá)式解析并捕獲文本時,經(jīng)常用到捕獲變量$1...等,正則表達(dá)式內(nèi)的每對括號都會捕獲括號內(nèi)匹配的文本,并將其存儲到捕獲變量中。

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

}

即使圓括號內(nèi)的表達(dá)式能在子串內(nèi)匹配多次,它也只會捕獲最后一次匹配的內(nèi)容

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

要找出捕獲變量數(shù)字和圓括號間的對應(yīng)關(guān)系,不管括號嵌套多復(fù)雜,只需從左依次數(shù)左括號的序數(shù)即可

捕獲的反向引用
正則表達(dá)式本身可以用反向引用匹配或調(diào)用之前捕獲的內(nèi)容,以原子\1,\2,\3等表示

關(guān)于反向引用有一個非常典型的例子,即單詞重復(fù)的處理

/(\w)\1/        #找出連續(xù)重復(fù)兩次的字符

捕獲并替換
捕獲及匹配變量常常用于替換操作

s/(\S+)\s+(\S+)/$2 $1/           #交換兩個單詞的位置

要是替換的目的是去除匹配的內(nèi)容,而非保留改寫,那就不要使用捕獲

刪除行首空格的兩個方法
s/^\s*(.*)/$1/;      #使用了捕獲
s/^\s+//;      #不使用捕獲,簡單

列表上下文中的匹配
在列表上下文中,匹配操作會根據(jù)所捕獲的緩存,返回與其對應(yīng)的列表,如果匹配失敗則返回空列表,這應(yīng)該最常用的特性之一了,只需一步便能掃描并同時分割子串

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

使用更精確的空白字符組

空白字符包括水平空白字符和垂直空白字符(換行符),perl正則表達(dá)式提供了各種精確匹配空白字符的方法
水平空白字符
在Perl5.10之前,預(yù)先定義好的有關(guān)空白字符處理的只有兩組。一組是\s,匹配所有的空白字符,另一組是\S,匹配所有的非空白字符,使用這種方式稍有不慎就會到導(dǎo)致意料之外的結(jié)果

my $string = <<'HERE';
this       is      a      line
this        is      another    line
and    a       final      line
HERE
假設(shè)你想把連續(xù)空白字符替換成單個空白字符
$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可以匹配除了垂直空白字符外的任意字符

行終止符
長期以來,對于行終止符的處理,一直很麻煩。因?yàn)椴坏袚Q行符,還有回車符,其組合方式也不一樣,有的文件用換行符結(jié)束當(dāng)前行,有的文件用回車符加上換行符表示換行

linux用一個\n表示換行
windows換行加回車表示換行\(zhòng)n\r
Mac用回車表示換行
windows文件在Unix/Mac下打開的話,每行結(jié)尾多出一個^M符號
為了避免這種麻煩,perl 5.10 特意引入的\R字符組,以更簡潔的方式匹配各種類型的行終止符

use 5.010;
$string =~ s/\R/\n/g;      #現(xiàn)在所有形式的行終止符都變成簡單統(tǒng)一的換行符了

非換行符
perl 5.12還引入了一種新的用來表示非換行符的字符組\N

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

用s修飾符可以使.能夠匹配換行符
if ($string =~ /(.+)/s) { ... }

如果我們只是匹配非換行符,就可以顯示使用\N匹配,不必?fù)?dān)心/s來搗亂
use 5.012;
if ($string =~ /(\N+)/) { ... }

使用命名捕獲,給匹配加標(biāo)簽

有時正則表達(dá)式中出現(xiàn)的括號太多,記憶變量編號與圓括號間的對應(yīng)關(guān)系相當(dāng)麻煩

$_ = '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后面可以將捕獲加個標(biāo)簽從而不用記憶編號和捕獲的對應(yīng)關(guān)系(?<LABEL>),捕獲的內(nèi)容存儲在%+哈希中

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

僅需分組時,用非捕獲括號

圓括號在正則表達(dá)式中,有兩種截然不同的作用: 分組和捕獲。一般來說,這兩種功能并在一起使用還是廷方便的

匹配郵件的標(biāo)題行subject:,忽略可能出現(xiàn)的回復(fù)前綴,直白寫出來會用到兩組圓括號
my ($bs,$subject) =~ /^subject:\s+(re:\s*)?(.*)/i;
其中第一個圓括號的作用是分組,但再將內(nèi)容捕獲就顯得多余
因此提供了解決方法,非捕獲括號(?:)的用法和普通括號相同,唯一的區(qū)別在于,它不會創(chuàng)建反向引用或捕獲變量

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正則表達(dá)式默認(rèn)總會返回它所能找到的“最左最長”匹配,即在字符串中找出第一個能匹配且盡可能長的字串。像*和+這樣表示重復(fù)次數(shù)的操作符,會“吃進(jìn)”盡可能多的字符

$_ = "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,較麻煩
對于任意重復(fù)操作符(*,+,{m,n}),在后面加上?就會變成非貪婪模式,即懶惰模式
my ($str) =~ /(' .*?')/;        #成功匹配test

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

有時我們會遇到僅需匹配某種條件,而非某個字符的情況。比如定位單詞邊界,字串起始、結(jié)束位置,或是子串中文本行的起始、結(jié)束位置。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

用^匹配起始位置
^用來匹配字符串的其是位置,許多人誤認(rèn)為這是“行起始”錨點(diǎn),是因?yàn)樗麄冎挥盟幚磉^單行文本

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;

用$匹配結(jié)束位置

$通常用于匹配字串結(jié)束位置,即便此處有換行符也能匹配

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

簡單字符處理應(yīng)避免正則表達(dá)式

正則表達(dá)式用起來很順手,但對于各種字符串操作,正則表達(dá)式并不總是最高效的手段,像提取字符子串、轉(zhuǎn)換字符之類的簡單任務(wù)應(yīng)該使用特定的函數(shù),比如index、rindex、substr以及tr///等等。

字串比較操作符
如果要比較兩個字串是否相同,可以用字串比較操作符,而非正則表達(dá)式

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

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

用index和rindex提取子串
index操作符可以在較長字串中定位一個較短子串的位置

#從$big_str里尋找$little_str的位置,起始下標(biāo)是0
my $pos = index $big_str, $little_str;

# rindex表示從字串右邊開始查找短的子串,但位置計(jì)算方式仍是從左到右
my $pos = rindex $big_str, $little_str;

用substr提取或修改子串
substr操作符的作用是根據(jù)給定起始位置和子串長度提取字符串中的一部分,如果沒有指定長度,則從指定位置開始一直提取到字串末尾的字符

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

#如果把substr表達(dá)式放在賦值操作符左邊,就是子串替換操作
my $world = "It's a perl world";
substr ($world,7,4) = "mad mad mad mad";

將index和substr結(jié)合起來,還可以實(shí)現(xiàn)類似于s///替換的功能,如果目的只是替換直接使用s///,不但更快,意義更明確

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

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

批量轉(zhuǎn)換單個字符
如果想把某個字符一次性全部轉(zhuǎn)換為另一個,沒必要使用正則表達(dá)式替換

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

不要忘了還有tr///操作符,雖然形式相近,但它不是正則表達(dá)式,它所做的只是字符間的轉(zhuǎn)換,默認(rèn)全局轉(zhuǎn)換

$string =~ tr/a/b/;

#如果只想對子串的某一部分作字符轉(zhuǎn)換,可以使用substr限定范圍
substr ($string ,0,10) =~ tr/a/b/;

提高正則表達(dá)式的可讀性

我們可以讓正則表達(dá)式變得更加易讀,尤其是準(zhǔn)備將代碼與他人分享,或者將來還要自己動手維護(hù)這些代碼,提升代碼可讀性就變得很重要

使用輔助空白字符和注釋
一般情況下,正則表達(dá)式中的空白字符都是有意義的。/x選項(xiàng)可以讓正則表達(dá)式解析器忽略空白字符,同時忽略注釋,因此添加空格可以清晰的理解正則表達(dá)式中每個部分的含義

/'(?:\\'| .)*?'/
/' (?:  \\'   |   .   )*?   '/x;    #簡潔易懂

避免不必要的回溯

正則表達(dá)式中的多選結(jié)構(gòu)運(yùn)算通常是比較慢的,這是由正則表達(dá)式引擎的工作方式?jīng)Q定的。當(dāng)多選結(jié)構(gòu)中的一個分支失敗時,引擎會在字串中"回溯"到之前的位置,嘗試下一個分支

while (<>) {
        print if /\b(george|jane|judy|elroy)\b/;
}
#實(shí)現(xiàn)機(jī)制:首先找到單詞邊界,嘗試匹配george,若失敗則退回單詞邊界處,嘗試匹配下一個名字,若成功,則尋找下一個單詞邊界,依次類推

#當(dāng)備選項(xiàng)彼此相近時,回溯問題尤其嚴(yán)重
'aaabbbccg' =~ /\b(aaabbbccc|aaabbbccd|aaabbbcce|aaabbbccf)\b/;

#每個備選項(xiàng)前半部分都是相同的,為了避免不必要的回溯,有一種解決方法是建立檢索樹,將所有備選項(xiàng)的相同前綴提取出來,以便把回溯次數(shù)降至最低
#從perl5.10開始,正則表達(dá)式引擎可以自動為備選文本建立檢索樹

用字符組[abc]代替多選結(jié)構(gòu)
有些情況下完全沒必要使用多選結(jié)構(gòu),應(yīng)該盡量避免

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

這個正則表達(dá)式先說到這里,后面還要深入學(xué)習(xí)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 捕獲 簽名不僅僅是語法,它們是含有一列參數(shù)對象的 first-class 對象 。同樣地,有一種含有參數(shù)集的數(shù)據(jù)...
    焉知非魚閱讀 576評論 0 0
  • 正則表達(dá)式到底是什么東西?字符是計(jì)算機(jī)軟件處理文字時最基本的單位,可能是字母,數(shù)字,標(biāo)點(diǎn)符號,空格,換行符,漢字等...
    獅子挽歌閱讀 2,169評論 0 9
  • 允許的修飾符 有些修飾符能在所有允許的地方出現(xiàn), 但并非所有的都這樣. 通常, 影響 regex 編譯的修飾符(...
    焉知非魚閱讀 1,382評論 0 1
  • http://www.jb51.net/tools/zhengze.html 正則表達(dá)式30分鐘入門教程 版本:v...
    nullleaf閱讀 619評論 0 2
  • 為什么要學(xué)正則表達(dá)式 雖然很多web開發(fā)者在忽視正則表達(dá)式后,還可以順利工作,但在javascript中還存在一些...
    Binaryify閱讀 2,355評論 0 7