今天這個主要是介紹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)先級
兩個原子順序排列稱之為序列,雖然沒用標(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í)!