PHP和Regex教程集合之二第1/2頁
來源:互聯網
上載者:User
Regex快速入門(二)
【導讀】在本文裡,我們主要介紹子模式(subpatterns),逆向引用(Back references)和量詞(quantifiers)
在上篇文章裡,我們介紹了Regex的模式修正符與元字元,細心的讀者也許會發現,這部分介紹的非常簡略,而且很少有實際的例子的講解。這主要是因為網上現有的Regex資料都對這部分都有詳細的介紹和眾多的例子,如果覺得對前一部分缺乏瞭解可以參看這些資料。本文希望可以儘可能多涉及一些較進階的Regex特性。
在本文裡,我們主要介紹子模式(subpatterns),逆向引用(Back references)和量詞(quantifiers),其中重點介紹對這些概念的一些擴充應用,例如子模式中的非捕獲子模式,量詞匹配時的greedy與ungreedy。
子模式(subpatterns)與逆向引用(Back references)
Regex可以包含多個字模式,子模式由圓括弧定界,可以嵌套。這也是兩個元字元“(”和“)”的作用。子模式可以有以下作用:
1. 將多選一的分支局部化。
例如,模式: cat(aract|erpillar|)匹配了 "cat","cataract" 或 "caterpillar" 之一,沒有圓括弧的話將匹配 "cataract","erpillar" 或Null 字元串。
2. 將子模式設定為捕獲子模式(例如上面這個例子)。當整個模式比對時,目標字串中匹配了子模式的部分可以通過逆向引用進行調用。左圓括弧從左至右計數(從 1 開始)以取得捕獲子模式的數。
注意,子模式是可以嵌套的,例如,如果將字串 "the red king" 來和模式 /the ((red|white) (king|queen))/進行匹配,捕獲的子串為 "red king","red" 以及 "king",並被計為 1,2 和 3 ,可以通過“1”,“2”,“3”來分別引用它們,“1”包含了“2”和“3”,它們的序號是由左括弧的順序決定的。
在一些老的linux/unux工具裡,子模式使用的圓括弧需要用反斜線轉義,向這種(subpattern),但現代的工具已經不需要了,本文中使用的例子都不進行轉義。
非捕獲子模式(non-capturing subpatterns)
用一對括弧同時完成上面提到的子模式的兩個功能有時會出現一些問題,例如,由於逆向引用的數目是有限的(通常最大不超過9),而且經常會遇到無需捕獲的子模式定義。這時,可以在開始的括弧後加上問號和冒號來表示這個子模式無需捕獲,就向下面這樣(?:red|white) (king|queen))。
如果將“the white queen”作為模式比對的目標字串,則捕獲的字串有“white queen”和“queen”,分別作為“1”和“2”,white雖然符合子模式“(?:red|white)”,但並不被捕獲。
我們前面已經介紹過用括弧與問號表示模式修正符的方法,為方便起見,如果需要在非捕獲子模式中插入模式修正符,可以把它直接放在問號和冒號之間,例如,下面兩個模式是等效的。
/(?i:saturday|sunday)/和/(??i)saturday|sunday)/。
逆向引用(Back references)
前面介紹反斜線作用時,已經提到它的一個作用就是表示逆向引用,當字元類之外的反斜線後跟一個大於0的十進位數時,它很有可能是一個逆向引用。它的含義正如它的名稱如言,它表示對它出現之前已經捕獲的子模式的引用。這個數字代表了它引用的左括弧在模式中出現的次序,我們在介紹子模式時已經看到過逆向引用的一個例子,那裡的過“1”,“2”,“3”分別表示所捕獲的第一,第二,和第三個小括弧定義的子模式的內容。
值得注意的是,當反斜線後的數字小於10時,可以確定此為一個逆向引用,這樣,這個逆向引用就可以出現在之前有相應數目的左圓括弧被捕獲前而不會出現混淆,只有整個模式能提供那麼多的捕獲子模式,就不會報錯。說起來似乎很混亂,還是讓我們來看下面這個例子。把介紹子模子時舉的例子拿來修改一下,前面講過字串 "the red king" 來和模式 /the ((red|white) (king|queen))/匹配,捕獲的子串為 "red king","red" 以及 "king",並被計為 1,2 和 3 ,現在把字串,修改為" king,the red king",模式改為/3,the ((red|white) (king|queen))/,這個模式應該也是可以匹配的。不過,並非所有的Regex工具都支援這種用法,安全的做法是在相應序號的左括弧之後使用與之相關的逆向引用。
需要注意的另一點是逆向引用的值是在目標字串中實際捕獲的符合子模式的字串片段而非該子模式本本身。例如/ (sens|respons)e and 1ibility/會匹配“sense and sensibility” 和 “response and responsibility”,但不會是 "sense and responsibility"。當被逆向引用的子模式後面有量詞從而被重複匹配了多次,逆向引用的值會以最後一次匹配的值為準。例如/([abc]){3}/匹配字串“abc”時,逆向引用“1”的值將是最後一次匹配的結果“c”。
命名子模式(named subpattern)
一些工具(例如Python)可以為逆向引用命名,從而定義出命名子模式。在Python中對Regex的使用是以函數或方法調用的格式,文法與這裡舉的例子有較大差別。有興趣的朋友可以參看一下自己使用的工具來看看是否支援命名子模式。
非捕獲子模式(non-capturing subpatterns)
用一對括弧同時完成上面提到的子模式的兩個功能有時會出現一些問題,例如,由於逆向引用的數目是有限的(通常最大不超過9),而且經常會遇到無需捕獲的子模式定義。這時,可以在開始的括弧後加上問號和冒號來表示這個子模式無需捕獲,就向下面這樣(?:red|white) (king|queen))。
如果將“the white queen”作為模式比對的目標字串,則捕獲的字串有“white queen”和“queen”,分別作為“1”和“2”,white雖然符合子模式“(?:red|white)”,但並不被捕獲。
我們前面已經介紹過用括弧與問號表示模式修正符的方法,為方便起見,如果需要在非捕獲子模式中插入模式修正符,可以把它直接放在問號和冒號之間,例如,下面兩個模式是等效的。
/(?i:saturday|sunday)/和/(?:(?i)saturday|sunday)/。
逆向引用(Back references)
前面介紹反斜線作用時,已經提到它的一個作用就是表示逆向引用,當字元類之外的反斜線後跟一個大於0的十進位數時,它很有可能是一個逆向引用。它的含義正如它的名稱如言,它表示對它出現之前已經捕獲的子模式的引用。這個數字代表了它引用的左括弧在模式中出現的次序,我們在介紹子模式時已經看到過逆向引用的一個例子,那裡的過“1”,“2”,“3”分別表示所捕獲的第一,第二,和第三個小括弧定義的子模式的內容。
值得注意的是,當反斜線後的數字小於10時,可以確定此為一個逆向引用,這樣,這個逆向引用就可以出現在之前有相應數目的左圓括弧被捕獲前而不會出現混淆,只有整個模式能提供那麼多的捕獲子模式,就不會報錯。說起來似乎很混亂,還是讓我們來看下面這個例子。把介紹子模子時舉的例子拿來修改一下,前面講過字串 "the red king" 來和模式 /the ((red|white) (king|queen))/匹配,捕獲的子串為 "red king","red" 以及 "king",並被計為 1,2 和 3 ,現在把字串,修改為" king,the red king",模式改為/3,the ((red|white) (king|queen))/,這個模式應該也是可以匹配的。不過,並非所有的Regex工具都支援這種用法,安全的做法是在相應序號的左括弧之後使用與之相關的逆向引用。
需要注意的另一點是逆向引用的值是在目標字串中實際捕獲的符合子模式的字串片段而非該子模式本本身。例如/ (sens|respons)e and 1ibility/會匹配“sense and sensibility” 和 “response and responsibility”,但不會是 "sense and responsibility"。當被逆向引用的子模式後面有量詞從而被重複匹配了多次,逆向引用的值會以最後一次匹配的值為準。例如/([abc]){3}/匹配字串“abc”時,逆向引用“1”的值將是最後一次匹配的結果“c”。
命名子模式(named subpattern)
一些工具(例如Python)可以為逆向引用命名,從而定義出命名子模式。在Python中對Regex的使用是以函數或方法調用的格式,文法與這裡舉的例子有較大差別。有興趣的朋友可以參看一下自己使用的工具來看看是否支援命名子模式。
重複(Repetition)和量詞(quantifiers)
在前面介紹逆向引用的部分裡我們已經接觸到了量詞(quantifiers)的概念,例如前面的例子/([abc]){3}/表示三個連續的字元,每個字元都必然是 “abc”這三個字元中的一個。在這個模式裡,{3}就屬於量詞。它表示一個模式需要重複匹配(repetition)的數目。
量詞可以放在下面這些項目之後:
?●單個字元(有可能是被轉義的單個字元,如xhh)
?●“.”元字元
?● 由方括弧表示的字元類
?● 逆向引用
?●由小括弧定義的子模式(除非它是個斷言,我們會在以後介紹)
最通用的量詞使用形式是用花括弧括起的兩個由逗號分隔的數字,如這樣的格式{min,max},例如,/z{2,4}/ 可以匹配 "zz", "zzz", 或者 "zzzz",花括弧中的最大值以及前面的逗號可以省略,例如/d{3,}/可以匹配三個以上的數字,數位數目沒有上限,而/d{3}/(注意,沒有逗號)則精確的匹配3個數字。當花括弧出現在不允許量詞的位置或者文法與前面提到的不符時,這裡它僅僅代表花括弧字元本身而不再具有特殊的含義。例如{,6}不是量詞,它僅僅代表這四個字元本身的含義。
為了方便,三個最常用的量詞有它們的單字元縮寫形式,它們的的含義如下表:
* 相當於 {0,}
+ 相當於 {1,}
? 相當於 {0,1}
這也是以上三個元字元做為量詞使用含義。
在使用量詞特別是沒有上限限制的量詞時,應該特別注意不要構成無限迴圈,例如/(a?)*/,在有的Regex工具裡。這會形成一個編譯錯,不過有的工具卻允許這種結構,但不能保證各種工具都可以很好的處理這種結構。
重複(Repetition)和量詞(quantifiers)
在前面介紹逆向引用的部分裡我們已經接觸到了量詞(quantifiers)的概念,例如前面的例子/([abc]){3}/表示三個連續的字元,每個字元都必然是 “abc”這三個字元中的一個。在這個模式裡,{3}就屬於量詞。它表示一個模式需要重複匹配(repetition)的數目。
量詞可以放在下面這些項目之後:
?●單個字元(有可能是被轉義的單個字元,如xhh)
?●“.”元字元
?● 由方括弧表示的字元類
?● 逆向引用
?●由小括弧定義的子模式(除非它是個斷言,我們會在以後介紹)
最通用的量詞使用形式是用花括弧括起的兩個由逗號分隔的數字,如這樣的格式{min,max},例如,/z{2,4}/ 可以匹配 "zz", "zzz", 或者 "zzzz",花括弧中的最大值以及前面的逗號可以省略,例如/d{3,}/可以匹配三個以上的數字,數位數目沒有上限,而/d{3}/(注意,沒有逗號)則精確的匹配3個數字。當花括弧出現在不允許量詞的位置或者文法與前面提到的不符時,這裡它僅僅代表花括弧字元本身而不再具有特殊的含義。例如{,6}不是量詞,它僅僅代表這四個字元本身的含義。
為了方便,三個最常用的量詞有它們的單字元縮寫形式,它們的的含義如下表:
* 相當於 {0,}
+ 相當於 {1,}
? 相當於 {0,1}
這也是以上三個元字元做為量詞使用含義。
在使用量詞特別是沒有上限限制的量詞時,應該特別注意不要構成無限迴圈,例如/(a?)*/,在有的Regex工具裡。這會形成一個編譯錯,不過有的工具卻允許這種結構,但不能保證各種工具都可以很好的處理這種結構。
量詞匹配的“greedy”與“ungreedy”
在使用帶量詞的模式時,我們常會發現對同一模式而言,同一個目標字串可以有多種匹配方式。例如/d{0,1}d/,可以匹配兩個或三個十進位數字,如果目標字串是123,當量詞取下限0裡,它匹配“12”,當量詞取上限1裡,它匹配“123”整個字元。這兩種匹配結果都是正確的,如果我們取它的子模式/(d{0,1}d)/,則匹配的結果1到底是“12”還是“123”?
實際的運行結果一般會是後者,因為預設情況下,大多數Regex工具的匹配是按“greedy”原則匹配的。“greedy”單詞的中的含義是“貪吃的, 貪婪的”的意思,它的行為也如此單詞的含義,所謂greedy匹配意指在量詞限制範圍內,只要能保持後續模式的匹配,匹配總是儘可能的重複下去,直到不匹配的情況發生為止。為便於理解,我們看下面這個簡單的例子。
/(d{1,5})d/匹配“12345”這個字串,這個模式表示在1到5個數字後面跟上一個數字,量詞範圍從1到5,當它的值在1-4時,整個模式都是匹配的,1的值可以是“1”,“12”,“123”,“1234”,而在greedy匹配的情況下,它取匹配時的量詞最大值,因此最終匹配的結果是”1234”。
在大多數情況下,這就是我們想要的結果,但情況並不總這樣。例如,我們希望用下面這個模式提取出c語言的注釋部分(在c語言中,備註陳述式放在字串/*和*/之間)。我們使用的Regex是/*.**/,但匹配的結果卻完全和需要的不同。當Regex解析到“/*”這後的“.*”時,因為“.”可以代表任一字元,這也包含了其後需要匹配的“*/”,在量詞的作用下,這個匹配將一直進行下去,超過下一個“*”/直到文本的結束,這顯然不是我們需要的結果。
為了完成如上例我們想要的那種匹配,Regex引入了ungreedy匹配方法,與greedy匹配相反,在滿足整個模式比對的前提下,它總是取最小的量詞數目結果。Ungreedy匹配用在量詞後面加上問號“?”來表示。例如在匹配C語言的注釋時,我們把Regex寫成如下形式:/*.*?*/,在量詞“*”後加上問號就可以達成想要的結果。還有前面那個例子用/(d{1,5})d/匹配“12345”這個字串,如果改寫為ungreedy模式向這樣/(d{1,5}?)d/,、1的值將為1。
上面的解釋也許有些不準確,量詞後的問號的作用實際上是反轉當前的Regex的greedy與ungreedy行為。你可以通過模式修正符“U”將Regex設成ungreedy模式然後在模式中通過量詞後的問號將之反轉為greedy。
一次性子模式(Once-only subpatterns)
關於量詞的另一個有趣的話題是一次性子模式(Once-only subpatterns)。要理解它的概念需要先瞭解一下含有量詞的Regex的匹配過程。我們這裡舉個例子。
現在,讓我們用模式/d+foo/來匹配字串“123456bar”,當然,它的結果是沒有匹配。但Regex引擎是如何工作的呢?它先分析前面的d+,這代表一個以上的數字,然後檢查目標字串的對應位置的第一個字元“1”,符合模式,然後根據量詞重複這個模式對字串進行匹配直到“123456”始終符合“d+”模式,接著它在目標字串中遇到字元“b”無法與“d+”匹配,於是查看“d+”的後續模式“foo”,與目標字串的後續部分“bar”無法匹配,這時,有趣的事情出現了,解釋引擎會對前面已經解析過的“d+”模式進行回溯,將量詞數目減少一,看剩餘部分能否匹配,此時“d+”的值改為“12345”,然後解釋引擎看目標字串剩餘的部分“6bar”能否與剩餘的模式“foo”相匹配,如果不行,就把量詞數再減一,直到達到最小的量詞限制,如果仍無法匹配,則表明目標字串無法匹配,返回無法匹配的結果。
現在,我們就可以來接觸一次性子模式了。所謂一次性子模式就是定義在Regex解析時不需要上述回溯過程的子模式。它用左圓括弧後面的問號和小於符號來表示,向這樣(?>)。如果將上面提到的例子改為一次性子模式,可以這樣書寫:
/(?>d)+foo/,這時,當解析器遇到後面不匹配的bar時,會立即返回不匹配的結果,而不會進行前面提到的回溯過程。
需要瞭解的是,一次性子模式屬於非捕獲子模式,它的匹配結果不能被逆向引用。
當一個沒有設定重複上限的子模式中包含了同樣沒有設定重複上限的模式時,使用一次性子模式是唯一可以避免讓你的程式陷入長時間等待的方法。例如你用“/(D+|<d+>)*[!?]/”這個模式去匹配一長串的a字元,向這樣“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”,在返回最終無匹配的結果前,你會等待很長的一段時間。這個模式表示一串非數字字元或者用角括弧括著的一串數字後跟隨著歎號或者問號,把這段字串分成兩個重複的部分會有很多種分法,而無論是子模式本身還是子模式之內的量詞的各可能值都要經過逐一測試,這將使最終的運算量達到一個很大的程度。這樣,你將在電腦前等待相當長的時間才會看到結果。而如果用一次性子模式來改寫剛才的模式,改成這樣/ ((?>D+)|<d+>)*[!?]/,你就可以很快得到運算的結果。
Regex快速入門(三)
在上文裡,我們介紹了Regex的子模式,逆向引用和量詞,在這篇文章裡,我們將重點介紹Regex中的斷言(Assertions)。
斷言(Assertions)
斷言(Assertions)是在目標字串的當前匹配位置進行的一種測試但這種測試並不佔用目標字串,也即不會移動模式在目標字串中的當前匹配位置。
讀起來似乎有點拗口,我們還是舉幾個簡單的例子。
兩個最常見的斷言是元字元“^”和“$”,它們檢查匹配模式是否出現在行首或行尾。
我們來看這個模式/^ddd$/,試著用它來匹配目標字串“123”。“ddd”表示三個數字字元,匹配了目標字串的三個字元,而模式中的^和$分別表示這三個字元同時出現在行首和行尾,而它們本身並不與目標字串中的任何字元相對應。
其它還有一些簡單的斷言b, B, A, Z, z,它們都以反斜線開頭,前面我們已經介紹過反斜線的這個用法。這幾個斷言的含義如下表。
斷言 含義
b 字分界線
B 非字分界線
A 目標的開頭(獨立於多行模式)
Z 目標的結尾或位於結尾的分行符號前(獨立於多行模式)
z 目標的結尾(獨立於多行模式)
G 目標中的第一個匹配位置
注意這些斷言不能出現在字元類中,如果出現了也是其它的含義,例如b在字元類中表示反斜線字元0x08。
前面介紹的這些斷言的測試都是一些基於當前位置的測試,斷言還支援更多複雜的測試條件。更複雜的斷言以子模式方式來表示,它包括前向斷言(Lookahead assertions)和後向斷言(Lookbehind assertions)。
前向斷言(Lookahead assertions)
前向斷言從目標字串的當前位置向前測試斷言條件是否成立。前向斷言又可分為前向肯定斷言和前向否定斷言,分別用(?=和{?!表示。例如模式/ w+(?=;)/用來表示一串文本字元後面會有一個分號,但是這個分號並不包括在匹配結果中。一件有趣的事看起來差不多的模式/ (?=;)w+/並不是表示一串前面不是分號的alpha字串,事實上,不論這串alpha字元的前面是否是一個分號它總是匹配的,要完成這個功能需要我們下面提到的後向斷言(Lookbehind assertions)。
後向斷言(Lookbehind assertions)
後向斷言分別用(?<=和(?<!表示肯定的後向斷言與否定後向斷言。例如,/ (?<!foo)bar/將尋找一個前面不是foo的bar字串。一般而言,後向斷言使用的子模式需要有確定的長度值,否則會產生一個編譯錯誤。
使用後向斷言與一次性子模式搭配使用可以有效文本的結束部分進行匹配,這裡來看一下例子。
考慮一下如果用/abcd$/這樣一個簡單的模式來匹配一長段以abcd結尾的文本,因為模式的匹配過程是從左向右進行的,Regex引擎將在文本中尋找每一個a字元並嘗試匹配剩餘的模式,如果在這長段文本裡僅好有不少的a字元,這樣做明顯是非常低效的,而如果把以上模式換成為樣/^.*abcd$/,這時前面的“^.*”部分將匹配整個文本,然後它發現下一個模式a無法匹配,這時會發生前面提到過的回溯過程,解析器會逐次縮短“^.*”匹配的字元長度從右向左逐次尋找剩餘的子模式,也要產生多次的嘗試過程。現在,我們用一次性子模式與後向斷言重寫所用的模式,改為/^(?>.*)(?<=abcd)/,這時,一次性子模式一次匹配了整段文本,然後用後向斷言檢查前面四個字元是否為abcd,只需要一次檢測就可以立刻確定整個模式是否匹配。在遇到需要匹配一個很長的文本時,這種方法可以非常顯著的提高處理效率。
一個模式中可以包含多個相繼的斷言,斷言也可以嵌套。另外,斷言使用的子模式也是非捕獲的,不能被逆向引用。
斷言的一個重要應用領域就是做為條件子模式的條件。那什麼是條件子模式呢?
條件子模式(Conditional subpatterns)
Regex允許在模式中根據不同的條件使用不同的匹配子模式。也就是條件子模式(Conditional subpatterns)。它的格式如下?(condition)yes-pattern)或者 (?(condition)yes-pattern|no-pattern)。如果條件滿足,採用yes-pattern,否則,採用no-pattern(如果在模式中提供了話)。
條件子模式中的條件有兩種,一種是斷言結果,另一種是看是否捕獲一個前面提供的子模式。
如果在表示條件的圓括弧裡的內容是一個數字,它表示當此數字代表的子模式被成功匹配時條件為真。看看下面這個例子,/( ( )? [^()]+ (?(1) ) )/x,(注意“x”模式修正符表示忽略字元類外的空白字元和#符號之後的內容)。
這個模式的第一部分“( ( )?”匹配了一個可選的左圖括弧“(”,第二部分“[^()]+”匹配了一個以上的非圓括弧字元,最後一部分“(?(1) ) )”是個條件子模式,表示如果捕獲到1也即那個可選的左圓括弧,第三部分應該會出現一個右圓括弧“)”。
如果在表示條件的圓括弧內是一個“R”字元,表示在這個模式或子模式被遞迴調用時條件為真,在遞迴調用的頂層,這個條件為假。關於Regex中的遞迴,我們會在後面的部分專題介紹。
如果條件不是一個數字或R字元,則它必需是一個斷言。斷言可以是肯定或否定的前身或後向斷言。讓我們看下面這個例子。
/(?(?=[^a-z]*[a-z])
d{2}-[a-z]{3}-d{2} | d{2}-d{2}-d{2} )/x
為了讓這個Regex更容易閱讀,我們特意採用了x模式修正符,這樣我們可以在用模式中加入空格對符式進行格式上的分隔並分行表示而不影響模式的解析。
第一行的條件子模式使用了一個肯定的前向斷言,表示一串可選的非小寫字母后面跟隨著一個小寫字母。換句話說,它查看目標字串是否至少包含一個小寫字母,如果是,它用“|”前的模式對目標進行匹配,看目標是否為看目標是否為兩個數字-三個小寫字母-兩個數字這種格式,否則,用“|”來匹配目標,看目標字串是否為由“-”分隔的三段二位十進位數字。
Regex中的注釋
為了讓Regex更容易閱讀,可以在其中加入備註陳述式。通常注釋由左圓括弧和井號——“(#“開始,當遇到下一個右圓括弧”)“結束。注釋是禁止嵌套的。
如果設定了“x”模式修正符,任何字元類之外(也即[]之外)的井號(#)和下一個新行標記之間的部分也被作為注釋看待。
Regex快速入門(四)
在上一篇文章裡,我們介紹了Regex中斷言相關的一些概念,在本文裡,我們會介紹Regex中遞迴的運用與利用Regex修改目標字串。
Regex中的遞迴
接觸過程式的朋友可能都遇到過成對的各種括弧吧,這些括弧常常相互嵌套,而且嵌套的層次數目無法確定。試想一下如果想提取一段程式裡用括弧括起的一段代碼,這裡面很可能包含了層次數目不定的其它括弧對,用Regex該如何完成?