Latest News

Home » PHP程式設計教學 » 正則表達式的研究

正則表達式的研究

正則表達式是一個無比強大的工具.任何地方我們都可能用到它…
原來刻苦的啃過 正則表達式,但時間長了還是會忘…
今天又來溫習一下..看到一篇不錯的文章..所以就轉過來收藏下.

前言:
半年前我對正則表達式產生了興趣,在網上尋找過不少資料,看過不少的教學,最後在使用一個正則表達式工具RegexBuddy時發現他的教學寫的非常好,可以說是我目前見過最好的正則表達式教學。於是一直想把他翻譯過來。這個願望直到這個五一長假才得以實現,結果就有了這篇文章。關於本文的名字,使用「深入淺出」似乎已經太俗。但是通讀原文以後,覺得只有用「深入淺出」才能準確的表達出該教學給我的感受,所以也就不能免俗了。
本文是Jan Goyvaerts為RegexBuddy寫的教學的譯文,版權歸原作者所有,歡迎轉載。但是為了尊重原作者和譯者的勞動,請註明出處!謝謝!

1.什麼是正則表達式

基本說來,正則表達式是一種用來描述一定數量文字的模式。Regex代表Regular Express。本文將用<<regex>>來表示一段具體的正則表達式。

一段文字就是最基本的模式,簡單的符合相同的文字。

2.不同的正則表達式引擎

正則表達式引擎是一種可以處理正則表達式的軟體。通常,引擎是更大的應用程式的一部分。在軟體世界,不同的正則表達式並不互相相容。本教學會集中討論Perl 5 類型的引擎,因為這種引擎是應用最廣泛的引擎。同時我們也會提到一些和其他引擎的區別。許多近代的引擎都很類似,但不完全一樣。例如.NET正則庫,JDK正則包。

3.文字元號

最基本的正則表達式由單個文字元號組成。如<<a>>,它將符合字串中第一次出現的字元「a」。如對字串「Jack is a boy」。「J」後的「a」將被符合。而第二個「a」將不會被符合。

正則表達式也可以符合第二個「a」,這必須是你告訴正則表達式引擎從第一次符合的地方開始搜尋。在文字編輯器中,你可以使用「尋找下一個」。在寫程式語系中,會有一個函數可以使你從前一次符合的位置開始繼續向後搜尋。

類似的,<<cat>>會符合「About cats and dogs」中的「cat」。這等於是告訴正則表達式引擎,找到一個<<c>>,緊跟一個<<a>>,再跟一個<<t>>。

要注意,正則表達式引擎預設是大小寫敏感的。除非你告訴引擎忽略大小寫,否則<<cat>>不會符合「Cat」。

    特殊字元

對於文字字元,有12個字元被保留作特殊用途。他們是:

[ ] ^ $ . | ? * + ( )

這些特殊字元也被稱作元字元。

如果你想在正則表達式中將這些字元用作文字字元,你需要用反斜槓「」對其進行換碼 (escape)。例如你想符合「1+1=2」,正確的表達式為<<1+1=2>>.

需要注意的是,<<1+1=2>>也是有效的正則表達式。但它不會符合「1+1=2」,而會符合「123+111=234」中的「111=2」。因為「+」在這裡表示特殊含義(重複1次到多次)。

在寫程式語系中,要注意,一些特殊的字元會先被編譯器處理,然後再傳遞給正則引擎。因此正則表達式<<1+2=2>>在C++中要寫成「1+1=2」。為了符合「C: emp」,你要用正則表達式<<C: emp>>。而在C++中,正則表達式則變成了「C: emp」。

    不可顯示字元

可以使用特殊字元序列來代表某些不可顯示字元:

<< >>代表Tab(0×09)

<<
>>代表換行符(0x0D)

<<
>>代表換行符(0x0A)

要注意的是Windows中文字檔案使用「
」來結束一行而Unix使用「
」。

4.正則表達式引擎的內定工作機制

知道正則表達式引擎是如何工作的有助於你很快理解為何某個正則表達式不像你期望的那樣工作。

有兩種類型的引擎:文字導向(text-directed)的引擎和正則導向(regex-directed)的引擎。Jeffrey Friedl把他們稱作DFA和NFA引擎。本文談到的是正則導向的引擎。這是因為一些非常有用的特性,如「惰性」量詞(lazy quantifiers)和反向引用(backreferences),只能在正則導向的引擎中實現。所以毫不意外這種引擎是目前最流行的引擎。

你可以輕易分辨出所使用的引擎是文字導向還是正則導向。如果反向引用或「惰性」量詞被實現,則可以肯定你使用的引擎是正則導向的。你可以作如下測試:將正則表達式<<regex|regex not>>應用到字串「regex not」。如果符合的結果是regex,則引擎是正則導向的。如果結果是regex not,則是文字導向的。因為正則導向的引擎是「猴急」的,它會很急切的進行表功,報告它找到的第一個符合 。

    正則導向的引擎總是返回最左邊的符合

這是需要你理解的很重要的一點:即使以後有可能發現一個「更好」的符合,正則導向的引擎也總是返回最左邊的符合。

當把<<cat>>應用到「He captured a catfish for his cat」,引擎先比較<<c>>和「H」,結果失敗了。於是引擎再比較<<c>>和「e」,也失敗了。直到第四個字元,<<c>>符合了「c」。<<a>>符合了第五個字元。到第六個字元<<t>>沒能符合「p」,也失敗了。引擎再繼續從第五個字元重新檢查符合性。直到第十五個字元開始,<<cat>>符合上了「catfish」中的「cat」,正則表達式引擎急切的返回第一個符合的結果,而不會再繼續尋找是否有其他更好的符合。

5.字集

字集是由一對方括號「[]」括起來的字集合。使用字集,你可以告訴正則表達式引擎僅僅符合多個字元中的一個。如果你想符合一個「a」或一個「e」,使用<<[ae]>>。你可以使用<<gr[ae]y>>符合gray或grey。這在你不確定你要搜尋的字元是採用美國英語還是英國英語時特別有用。相反,<<gr[ae]y>>將不會符合graay或graey。字集中的字元順序並沒有什麼關係,結果都是相同的。

你可以使用連字元「-」定義一個字元範圍作為字集。<<[0-9]>>符合0到9之間的單個數字。你可以使用不止一個範圍。<<[0-9a-fA-F] >>符合單個的十六進位數字,並且大小寫不敏感。你也可以結合範圍定義與單個字元定義。<<[0-9a-fxA-FX]>>符合一個十六進位數字或字母X。再次強調一下,字元和範圍定義的先後順序對結果沒有影響。

    字集的一些應用

尋找一個可能有拼字錯誤的單詞,比如<<sep[ae]r[ae]te>> 或 <<li[cs]en[cs]e>>。

尋找程式語系的標識符,<<A-Za-z_][A-Za-z_0-9]*>>。(*表示重複0或多次)

尋找C風格的十六進位數<<0[xX][A-Fa-f0-9]+>>。(+表示重複一次或多次)

    取反字集

在左方括號「[」後面緊跟一個尖括號「^」,將會對字集取反。結果是字集將符合任何不在方括號中的字元。不像「.」,取反字集是可以符合換行換行符的。

需要記住的很重要的一點是,取反字集必須要符合一個字元。<<q[^u]>>並不意味著:符合一個q,後面沒有u跟著。它意味著:符合一個q,後面跟著一個不是u的字元。所以它不會符合「Iraq」中的q,而會符合「Iraq is a country」中的q和一個空格符。事實上,空格符是符合中的一部分,因為它是一個「不是u的字元」。

如果你只想符合一個q,條件是q後面有一個不是u的字元,我們可以用後面將講到的向前檢視來解決。

    字集中的元字元

需要注意的是,在字集中只有4個 字元具有特殊含義。它們是:「] ^ -」。「]」代表字集定義的結束;「」代表轉義;「^」代表取反;「-」代表範圍定義。其他常見的元字元在字集定義內定都是標準字元,不需要轉義。例如,要搜尋星號*或加號+,你可以用<<[+*]>>。當然,如果你對那些通常的元字元進行轉義,你的正則表達式一樣會工作得很好,但是這會降低可讀性。

在字集定義中為了將反斜槓「」作為一個文字字元而非特殊含義的字元,你需要用另一個反斜槓對它進行轉義。<<[x]>>將會符合一個反斜槓和一個X。「]^-」都可以用反斜槓進行轉義,或是將他們放在一個不可能使用到他們特殊含義的位置。我們推薦後者,因為這樣可以增加可讀性。比如對於字元「^」,將它放在除了左括號「[」後面的位置,使用的都是文字字元含義而非取反含義。如<<[x^]>>會符合一個x或^。<<[]x]>>會符合一個「]」或「x」。<<[-x]>>或<<[x-]>>都會符合一個「-」或「x」。

    字集的簡寫

因為一些字集非常常用,所以有一些簡寫模式。

<<d>>代表<<[0-9]>>;

<<w>>代表單詞字元。這個是隨正則表達式實現的不同而有些差異。絕大多數的正則表達式實現的單詞字集都包括了<<A-Za-z0-9_]>>。

<<s>>代表「白字元」。這個也是和不同的實現有關的。在絕大多數的實現中,都包括了空格符和Tab符,以及換行換行符<<
>>。

字集的縮寫形式可以用在方括號之內或之外。<<sd>>符合一個白字元後面緊跟一個數字。<<[sd]>>符合單個白字元或數字。<<[da-fA-F]>>將符合一個十六進位數字。

取反字集的簡寫

<<[S]>> = <<[^s]>>

<<[W]>> = <<[^w]>>

<<[D]>> = <<[^d]>>

    字集的重複

如果你用「?*+」操作符來重複一個字集,你將會重複整個字集。而不僅是它符合的那個字元。正則表達式<<[0-9]+>>會符合837以及222。

如果你僅僅想重複被符合的那個字元,可以用向後引用達到目的。我們以後將講到向後引用。

6.使用?*或+ 進行重複

?:告訴引擎符合前導字元0次或一次。事實上是表示前導字元是可選的。

+:告訴引擎符合前導字元1次或多次

*:告訴引擎符合前導字元0次或多次

<[A-Za-z][A-Za-z0-9]*>符合沒有屬性的HTML標籤,「<」以及「>」是文字元號。第一個字集符合一個字母,第二個字集符合一個字母或數字。

我們似乎也可以用<[A-Za-z0-9]+>。但是它會符合<1>。但是這個正則表達式在你知道你要搜尋的字串不包括類似的無效標籤時還是足夠有效的。

    限制性重複

許多現代的正則表達式實現,都容許你定義對一個字元重複多少次。詞法是:{min,max}。min和max都是非負整數。如果逗號有而max被忽略了,則max沒有限制。如果逗號和max都被忽略了,則重複min次。

因此{0,}和*一樣,{1,}和+ 的作用一樣。

你可以用<<[1-9][0-9]{3}>>符合1000~9999之間的數字(「」表示單詞邊界)。<<[1-9][0-9]{2,4}>>符合一個在100~99999之間的數字。

    注意貪婪性

假設你想用一個正則表達式符合一個HTML標籤。你知道輸入將會是一個有效的HTML檔案,因此正則表達式不需要排除那些無效的標籤。所以如果是在兩個尖括號之間的內容,就應該是一個HTML標籤。

許多正則表達式的新手會首先想到用正則表達式<< <.+> >>,他們會很驚訝的發現,對於測試字串,「This is a <EM>first</EM> test」,你可能期望會返回<EM>,然後繼續進行符合的時候,返回</EM>。

但事實是不會。正則表達式將會符合「<EM>first</EM>」。很顯然這不是我們想要的結果。原因在於「+」是貪婪的。也就是說,「+」會導致正則表達式引擎試圖盡可能的重複前導字元。只有當這種重複會引起整個正則表達式符合失敗的情況下,引擎會進行回溯。也就是說,它會放棄最後一次的「重複」,然後處理正則表達式餘下的部分。

和「+」類似,「?*」的重複也是貪婪的。

    深入正則表達式引擎內定

讓我們來看看正則引擎如何符合前面的例子。第一個記號是「<」,這是一個文字元號。第二個符號是「.」,符合了字元「E」,然後「+」一直可以符合其餘的字元,直到一行的結束。然後到了換行符,符合失敗(「.」不符合換行符)。於是引擎開始對下一個正則表達式符號進行符合。也即試圖符合「>」。到目前為止,「<.+」已經符合了「<EM>first</EM> test」。引擎會試圖將「>」與換行符進行符合,結果失敗了。於是引擎進行回溯。結果是現在「<.+」符合「<EM>first</EM> tes」。於是引擎將「>」與「t」進行符合。顯然還是會失敗。這個過程繼續,直到「<.+」符合「<EM>first</EM」,「>」與「>」符合。於是引擎找到了一個符合「<EM>first</EM>」。記住,正則導向的引擎是「急切的」,所以它會急著報告它找到的第一個符合。而不是繼續回溯,即使可能會有更好的符合,例如「<EM>」。所以我們可以看到,由於「+」的貪婪性,使得正則表達式引擎返回了一個最左邊的最長的符合。

    用懶惰性取代貪婪性

一個用於修正以上問題的可能專案是用「+」的惰性代替貪婪性。你可以在「+」後面緊跟一個問號「?」來達到這一點。「*」,「{}」和「?」表示的重複也可以用這個專案。因此在上面的例子中我們可以使用「<.+?>」。讓我們再來看看正則表達式引擎的處理過程。

再一次,正則表達式記號「<」會符合字串的第一個「<」。下一個正則記號是「.」。這次是一個懶惰的「+」來重複上一個字元。這告訴正則引擎,盡可能少的重複上一個字元。因此引擎符合「.」和字元「E」,然後用「>」符合「M」,結果失敗了。引擎會進行回溯,和上一個例子不同,因為是惰性重複,所以引擎是延伸惰性重複而不是減少,於是「<.+」現在被延伸為「<EM」。引擎繼續符合下一個記號「>」。這次得到了一個完成符合。引擎於是報告「<EM>」是一個完成的符合。整個過程大致如此。

    惰性延伸的一個替代專案

我們還有一個更好的替代專案。可以用一個貪婪重複與一個取反字集:「<[^>]+>」。之所以說這是一個更好的專案在於使用惰性重複時,引擎會在找到一個完成符合前對每一個字元進行回溯。而使用取反字集則不需要進行回溯。

最後要記住的是,本教學僅僅談到的是正則導向的引擎。文字導向的引擎是不回溯的。但是同時他們也不支援惰性重複操作。

7.使用「.」符合幾乎任意字元

在正則表達式中,「.」是最常用的符號之一。不幸的是,它也是最容易被誤用的符號之一。

「.」符合一個單個的字元而不用關心被符合的字元是什麼。唯一的例外是新行符。在本教學中談到的引擎,預設情況下都是不符合新行符的。因此在預設情況下,「.」等於是字集[^

](Window)或[^
]( Unix)的簡寫。

這個例外是因為歷史的原因。因為早期使用正則表達式的工具是基於行的。它們都是一行一行的讀入一個檔案,將正則表達式分別應用到每一行上去。在這些工具中,字串是不包括新行符的。因此「.」也就永遠不符合新行符。

現代的工具和語系能夠將正則表達式應用到很大的字串甚至整個檔案上去。本教學討論的所有正則表達式實現都提供一個選項,可以使「.」符合所有的字元,內含新行符。在RegexBuddy, EditPad Pro或PowerGREP等工具中,你可以簡單的選中「點號符合新行符」。在Perl中,「.」可以符合新行符的模式被稱作「單行模式」。很不幸,這是一個很容易混淆的名詞。因為還有所謂「多行模式」。多行模式只影響行首行尾的錨定(anchor),而單行模式只影響「.」。

其他語系和正則表達式庫也採用了Perl的術語定義。當在.NET Framework中使用正則表達式類時,你可以用類似下面的語句來啟動單行模式:Regex.Match(「string」,」regex」,RegexOptions.SingleLine)

    保守的使用點號「.」

點號可以說是最強大的元字元。它容許你偷懶:用一個點號,就能符合幾乎所有的字元。但是問題在於,它也常常會符合不該符合的字元。

我會以一個簡單的例子來說明。讓我們看看如何符合一個具有「mm/dd/yy」格式的日期,但是我們想容許使用者來選取分隔設定。很快能想到的一個專案是<<dd.dd.dd>>。看上去它能符合日期「02/12/03」。問題在於02512703也會被認為是一個有效的日期。

<<dd[-/.]dd[-/.]dd>>看上去是一個好一點的解決專案。記住點號在一個字集裡不是元字元。這個專案遠不夠完善,它會符合「99/99/99」。而<<[0-1]d[-/.][0-3]d[-/.]dd>>又更進一步。儘管他也會符合「19/39/99」。你想要你的正則表達式達到如何完美的程度取決於你想達到什麼樣的目的。如果你想校驗使用者輸入,則需要盡可能的完美。如果你只是想分析一個已知的源,並且我們知道沒有錯誤的資料,用一個比較好的正則表達式來符合你想要搜尋的字元就已經足夠。

 

8.字串開始和結束的錨定

錨定和一般的正則表達式符號不同,它不符合任何字元。相反,他們符合的是字元之前或之後的位置。「^」符合一行字串第一個字元前的位置。<<^a>>將會符合字串「abc」中的a。<<^b>>將不會符合「abc」中的任何字元。

類似的,$符合字串中最後一個字元的後面的位置。所以<<c$>>符合「abc」中的c。

    錨定的應用

在寫程式語系中校驗使用者輸入時,使用錨定是非常重要的。如果你想校驗使用者的輸入為整數,用<<^d+$>>。

使用者輸入中,常常會有多餘的前導空格或結束空格。你可以用<<^s*>>和<<s*$>>來符合前導空格或結束空格。

    使用「^」和「$」作為行的開始和結束錨定

如果你有一個包括了多行的字串。例如:「first line

second line」(其中

表示一個新行符)。常常需要對每行分別處理而不是整個字串。因此,幾乎所有的正則表達式引擎都提供一個選項,可以延伸這兩種錨定的含義。「^」可以符合字串的開始位置(在f之前),以及每一個新行符的後面位置(在

和s之間)。類似的,$會符合字串的結束位置(最後一個e之後),以及每個新行符的前面(在e與

之間)。

在.NET中,當你使用如下代碼時,將會定義錨定符合每一個新行符的前面和後面位置:Regex.Match(「string」, 「regex」, RegexOptions.Multiline)

應用:string str = Regex.Replace(Original, 「^」, 「> 「, RegexOptions.Multiline)–將會在每行的行首插入「> 」。

    絕對錨定

<<A>>只符合整個字串的開始位置,<<>>只符合整個字串的結束位置。即使你使用了「多行模式」,<<A>>和<<>>也永遠不符合新行符。

即使和$只符合字串的結束位置,仍然有一個例外的情況。如果字串以新行符結束,則和$將會符合新行符前面的位置,而不是整個字串的最後面。這個「改進」是由Perl引進的,然後被許多的正則表達式實現所遵循,內含Java,.NET等。如果應用<<^[a-z]+$>>到「joe
」,則符合結果是「joe」而不是「joe
」。

9.單詞邊界

元字元<<>>也是一種對位置進行符合的「錨」。這種符合是0長度符合。

有4種位置被認為是「單詞邊界」:

1)  在字串的第一個字元前的位置(如果字串的第一個字元是一個「單詞字元」)

2)  在字串的最後一個字元後的位置(如果字串的最後一個字元是一個「單詞字元」)

3)  在一個「單詞字元」和「非單詞字元」之間,其中「非單詞字元」緊跟在「單詞字元」之後

4)  在一個「非單詞字元」和「單詞字元」之間,其中「單詞字元」緊跟在「非單詞字元」後面

「單詞字元」是可以用「w」符合的字元,「非單詞字元」是可以用「W」符合的字元。在大多數的正則表達式實現中,「單詞字元」通常內含<<[a-zA-Z0-9_]>>。

例如:<<4>>能夠符合單個的4而不是一個更大數的一部分。這個正則表達式不會符合「44」中的4。

換種說法,幾乎可以說<<>>符合一個「字母數字序列」的開始和結束的位置。

「單詞邊界」的取反集為<<B>>,他要符合的位置是兩個「單詞字元」之間或是兩個「非單詞字元」之間的位置。

    深入正則表達式引擎內定

讓我們看看把正則表達式<<is>>應用到字串「This island is beautiful」。引擎先處理符號<<>>。因為是0長度 ,所以第一個字元T前面的位置會被考察。因為T是一個「單詞字元」,而它前面的字元是一個空字元(void),所以符合了單詞邊界。接著<<i>>和第一個字元「T」符合失敗。符合過程繼續進行,直到第五個空格符,和第四個字元「s」之間又符合了<<>>。然而空格符和<<i>>不符合。繼續向後,到了第六個字元「i」,和第五個空格字元之間符合了<<>>,然後<<is>>和第六、第七個字元都符合了。然而第八個字元和第二個「單詞邊界」不符合,所以符合又失敗了。到了第13個字元i,因為和前面一個空格符形成「單詞邊界」,同時<<is>>和「is」符合。引擎接著嘗試符合第二個<<>>。因為第15個空格符和「s」形成單詞邊界,所以符合完成。引擎「急著」返回完成符合的結果。

10.  選取符

正則表達式中「|」表示選取。你可以用選取符符合多個可能的正則表達式中的一個。

如果你想搜尋文字「cat」或「dog」,你可以用<<cat|dog>>。如果你想有更多的選取,你只要延伸清單<<cat|dog|mouse|fish>>。

選取符在正則表達式中具有最低的優先級,也就是說,它告訴引擎要麼符合選取符左邊的所有表達式,要麼符合右邊的所有表達式。你也可以用圓括號來限制選取符的作用範圍。如<<(cat|dog)>>,這樣告訴正則引擎把(cat|dog)當成一個正則表達式單位來處理。

    注意正則引擎的「急於表功」性

正則引擎是急切的,當它找到一個有效的符合時,它會停止搜尋。因此在一定條件下,選取符兩邊的表達式的順序對結果會有影響。假設你想用正則表達式搜尋一個寫程式語系的函數清單:Get,GetValue,Set或SetValue。一個明顯的解決專案是<<Get|GetValue|Set|SetValue>>。讓我們看看當搜尋SetValue時的結果。

因為<<Get>>和<<GetValue>>都失敗了,而<<Set>>符合完成。因為正則導向的引擎都是「急切」的,所以它會返回第一個完成的符合,就是「Set」,而不去繼續搜尋是否有其他更好的符合。

和我們期望的相反,正則表達式並沒有符合整個字串。有幾種可能的解決辦法。一是考慮到正則引擎的「急切」性,改變選項的順序,例如我們使用<<GetValue|Get|SetValue|Set>>,這樣我們就可以優先搜尋最長的符合。我們也可以把四個選項結合起來成兩個選項:<<Get(Value)?|Set(Value)?>>。因為問號重複符是貪婪的,所以SetValue總會在Set之前被符合。

一個更好的專案是使用單詞邊界:<<(Get|GetValue|Set|SetValue)>>或<<(Get(Value)?|Set(Value)?>>。更進一步,既然所有的選取都有相同的結尾,我們可以把正則表達式改善為<<(Get|Set)(Value)?>>。

11.  組與向後引用

把正則表達式的一部分放在圓括號內,你可以將它們形成組。然後你可以對整個組使用一些正則操作,例如重複操作符。

要注意的是,只有圓括號「()」才能用於形成組。「[]」用於定義字集。「{}」用於定義重複操作。

當用「()」定義了一個正則表達式組後,正則引擎則會把被符合的組按照順序編號,存入快取。當對被符合的組進行向後引用的時候,可以用「數字」的模式進行引用。<<1>>引用第一個符合的後向引用組,<<2>>引用第二個組,以此類推,<<
>>引用第n個組。而<< >>則引用整個被符合的正則表達式本身。我們看一個例子。

假設你想符合一個HTML標籤的開始標籤和結束標籤,以及標籤中間的文字。比如<B>This is a test</B>,我們要符合<B>和</B>以及中間的文字。我們可以用如下正則表達式:「<([A-Z][A-Z0-9]*)[^>]*>.*?</1>」

首先,「<」將會符合「<B>」的第一個字元「<」。然後[A-Z]符合B,[A-Z0-9]*將會符合0到多次字母數字,後面緊接著0到多個非「>」的字元。最後正則表達式的「>」將會符合「<B>」的「>」。接下來正則引擎將對結束標籤之前的字元進行惰性符合,直到遇到一個「</」符號。然後正則表達式中的「1」表示對前面符合的組「([A-Z][A-Z0-9]*)」進行引用,在本例中,被引用的是標籤名「B」。所以需要被符合的結尾標籤為「</B>」

你可以對相同的後向引用組進行多次引用,<<([a-c])x1x1>>將符合「axaxa」、「bxbxb」以及「cxcxc」。如果用數字形式引用的組沒有有效的符合,則引用到的內容簡單的為空。

一個後向引用不能用於它自身。<<([abc]1)>>是錯誤的。因此你不能將<< >>用於一個正則表達式符合本身,它只能用於置換操作中。

後向引用不能用於字集內定。<<(a)[1b]>>中的<<1>>並不表示後向引用。在字集內定,<<1>>可以被解釋為八進位形式的轉碼。

向後引用會降低引擎的速度,因為它需要存儲符合的組。如果你不需要向後引用,你可以告訴引擎對某個組不存儲。例如:<<Get(?:Value)>>。其中「(」後面緊跟的「?:」會告訴引擎對於組(Value),不存儲符合的值以供後向引用。

    重複操作與後向引用

當對組使用重複操作符時,快取裡後向引用內容會被不斷重整,只保留最後符合的內容。例如:<<([abc]+)=1>>將符合「cab=cab」,但是<<([abc])+=1>>卻不會。因為([abc])第一次符合「c」時,「1」代表「c」;然後([abc])會繼續符合「a」和「b」。最後「1」代表「b」,所以它會符合「cab=b」。

應用:檢查重複單詞–當編輯文字時,很容易就會輸入重複單詞,例如「the the」。使用<<(w+)s+1>>可以檢驗到這些重複單詞。要刪除第二個單詞,只要簡單的利用置換功能置換掉「1」就可以了。

    組的命名和引用

在PHP,Python中,可以用<<(?P<name>group)>>來對組進行命名。在本例中,詞法?P<name>就是對組(group)進行了命名。其中name是你對組的起的名字。你可以用(?P=name)進行引用。

.NET的命名組

.NET framework也支援命名組。不幸的是,微軟的程式員們決定發明他們自己的語法,而不是沿用Perl、Python的規則。目前為止,還沒有任何其他的正則表達式實現支援微軟發明的語法。

下面是.NET中的例子:

(?<first>group)(?』second』group)

正如你所看到的,.NET提供兩種詞法來建立命名組:一是用尖括號「<>」,或是用單引號「』』」。尖括號在字串中使用更方便,單引號在ASP代碼中更有用,因為ASP代碼中「<>」被用作HTML標籤。

要引用一個命名組,使用k<name>或k』name』.

當進行搜尋置換時,你可以用「${name}」來引用一個命名組。

12.  正則表達式的符合模式

本教學所討論的正則表達式引擎都支援三種符合模式:

<</i>>使正則表達式對大小寫不敏感,

<</s>>開啟「單行模式」,即點號「.」符合新行符

<</m>>開啟「多行模式」,即「^」和「$」符合新行符的前面和後面的位置。

    在正則表達式內定開啟或關閉模式

如果你在正則表達式內定插入修飾符(?ism),則該修飾符只對其右邊的正則表達式起作用。(?-i)是關閉大小寫不敏感。你可以很快的進行測試。<<(?i)te(?-i)st>>應該符合TEst,但是不能符合teST或TEST.

13.  原子組與防止回溯

在一些特殊情況下,因為回溯會使得引擎的效率極其低下。

讓我們看一個例子:要符合這樣的字串,字串中的每個欄位間用逗號做分隔設定,第12個欄位由P開頭。

我們容易想到這樣的正則表達式<<^(.*?,){11}P>>。這個正則表達式在標準情況下工作的很好。但是在極端情況下,如果第12個欄位不是由P開頭,則會發生災難性的回溯。如要搜尋的字串為「1,2,3,4,5,6,7,8,9,10,11,12,13」。首先,正則表達式一直完成符合直到第12個字元。這時,前面的正則表達式消耗的字串為「1,2,3,4,5,6,7,8,9,10,11,」,到了下一個字元,<<P>>並不符合「12」。所以引擎進行回溯,這時正則表達式消耗的字串為「1,2,3,4,5,6,7,8,9,10,11」。繼續下一次符合過程,下一個正則符號為點號<<.>>,可以符合下一個逗號「,」。然而<<,>>並不符合字元「12」中的「1」。符合失敗,繼續回溯。大家可以想像,這樣的回溯組合是個非常大的數量。因此可能會造成引擎崩潰。

用於阻止這樣巨大的回溯有幾種專案:

一種簡單的專案是盡可能的使符合精確。用取反字集代替點號。例如我們用如下正則表達式<<^([^,
]*,){11}P>>,這樣可以使失敗回溯的次數下降到11次。

另一種專案是使用原子組。

原子組的目的是使正則引擎失敗的更快一點。因此可以有效的阻止海量回溯。原子組的語法是<<(?>正則表達式)>>。位於(?>)之間的所有正則表達式都會被認為是一個單一的正則符號。一旦符合失敗,引擎將會回溯到原子組前面的正則表達式部分。前面的例子用原子組可以表達成<<^(?>(.*?,){11})P>>。一旦第十二個欄位符合失敗,引擎回溯到原子組前面的<<^>>。

14.  向前檢視與向後檢視

Perl 5 引入了兩個強大的正則語法:「向前檢視」和「向後檢視」。他們也被稱作「零長度斷言」。他們和錨定一樣都是零長度的(所謂零長度即指該正則表達式不消耗被符合的字串)。不同之處在於「前後檢視」會實際符合字元,只是他們會拋棄符合只返回符合結果:符合或不符合。這就是為什麼他們被稱作「斷言」。他們並不實際消耗字串中的字元,而只是斷言一個符合是否可能。

幾乎本文討論的所有正則表達式的實現都支援「向前嚮後檢視」。唯一的一個例外是Javascript只支援向前檢視。

    肯定和否定式的向前檢視

如我們前面提過的一個例子:要尋找一個q,後面沒有緊跟一個u。也就是說,要麼q後面沒有字元,要麼後面的字元不是u。採用否定式向前檢視後的一個解決專案為<<q(?!u)>>。否定式向前檢視的語法是<<(?!檢視的內容)>>。

肯定式向前檢視和否定式向前檢視很類似:<<(?=檢視的內容)>>。

如果在「檢視的內容」部分有組,也會產生一個向後引用。但是向前檢視本身並不會產生向後引用,也不會被計入向後引用的編號中。這是因為向前檢視本身是會被拋棄掉的,只保留符合與否的判斷結果。如果你想保留符合的結果作為向後引用,你可以用<<(?=(regex))>>來產生一個向後引用。

    肯定和否定式的先後檢視

向後檢視和向前檢視有相同的效果,只是方向相反

否定式向後檢視的語法是:<<(?<!檢視內容)>>

肯定式向後檢視的語法是:<<(?<=檢視內容)>>

我們可以看到,和向前檢視相比,多了一個表示方向的左尖括號。

例:<<(?<!a)b>>將會符合一個沒有「a」作前導字元的「b」。

值得注意的是:向前檢視從現用的字串位置開始對「檢視」正則表達式進行符合;向後檢視則從現用的字串位置開始先後回溯一個字元,然後再開始對「檢視」正則表達式進行符合。

    深入正則表達式引擎內定

讓我們看一個簡單例子。

把正則表達式<<q(?!u)>>應用到字串「Iraq」。正則表達式的第一個符號是<<q>>。正如我們知道的,引擎在符合<<q>>以前會掃過整個字串。當第四個字元「q」被符合後,「q」後面是空字元(void)。而下一個正則符號是向前檢視。引擎注意到已經進入了一個向前檢視正則表達式部分。下一個正則符號是<<u>>,和空字元不符合,從而導致向前檢視裡的正則表達式符合失敗。因為是一個否定式的向前檢視,意味著整個向前檢視結果是完成的。於是符合結果「q」被返回了。

我們在把相同的正則表達式應用到「quit」。<<q>>符合了「q」。下一個正則符號是向前檢視部分的<<u>>,它符合了字串中的第二個字元「i」。引擎繼續走到下個字元「i」。然而引擎這時注意到向前檢視部分已經處理完了,並且向前檢視已經完成。於是引擎拋棄被符合的字串部分,這將導致引擎回退到字元「u」。

因為向前檢視是否定式的,意味著檢視部分的完成符合導致了整個向前檢視的失敗,因此引擎不得不進行回溯。最後因為再沒有其他的「q」和<<q>>符合,所以整個符合失敗了。

為了確保你能清楚地理解向前檢視的實現,讓我們把<<q(?=u)i>>應用到「quit」。<<q>>首先符合「q」。然後向前檢視完成符合「u」,符合的部分被拋棄,只返回可以符合的判斷結果。引擎從字元「i」回退到「u」。由於向前檢視完成了,引擎繼續處理下一個正則符號<<i>>。結果發現<<i>>和「u」不符合。因此符合失敗了。由於後面沒有其他的「q」,整個正則表達式的符合失敗了。

    更進一步理解正則表達式引擎內定機制

讓我們把<<(?<=a)b>>應用到「thingamabob」。引擎開始處理向後檢視部分的正則符號和字串中的第一個字元。在這個例子中,向後檢視告訴正則表達式引擎回退一個字元,然後檢視是否有一個「a」被符合。因為在「t」前面沒有字元,所以引擎不能回退。因此向後檢視失敗了。引擎繼續走到下一個字元「h」。再一次,引擎暫時回退一個字元並檢查是否有個「a」被符合。結果發現了一個「t」。向後檢視又失敗了。

向後檢視繼續失敗,直到正則表達式到達了字串中的「m」,於是肯定式的向後檢視被符合了。因為它是零長度的,字串的現用的位置仍然是「m」。下一個正則符號是<<b>>,和「m」符合失敗。下一個字元是字串中的第二個「a」。引擎向後暫時回退一個字元,並且發現<<a>>不符合「m」。

在下一個字元是字串中的第一個「b」。引擎暫時性的向後退一個字元發現向後檢視被滿足了,同時<<b>>符合了「b」。因此整個正則表達式被符合了。作為結果,正則表達式返回字串中的第一個「b」。

    向前嚮後檢視的應用

我們來看這樣一個例子:尋找一個具有6位字元的,含有「cat」的單詞。

首先,我們可以不用向前嚮後檢視來解決問題,例如:

<< catw{3}|wcatw{2}|w{2}catw|w{3}cat>>

足夠簡單吧!但是當需求變成尋找一個具有6-12位字元,含有「cat」,「dog」或「mouse」的單詞時,這種方法就變得有些笨拙了。

我們來看看使用向前檢視的專案。在這個例子中,我們有兩個基本需求要滿足:一是我們需要一個6位的字元,二是單詞含有「cat」。

滿足第一個需求的正則表達式為<<w{6}>>。滿足第二個需求的正則表達式為<<w*catw*>>。

把兩者結合起來,我們可以得到如下的正則表達式:

<<(?=w{6})w*catw*>>

具體的符合過程留給讀者。但是要注意的一點是,向前檢視是不消耗字元的,因此當判斷單詞滿足具有6個字元的條件後,引擎會從開始判斷前的位置繼續對後面的正則表達式進行符合。

最後作些改善,可以得到下面的正則表達式:

<<(?=w{6})w{0,3}catw*>>

15.  正則表達式中的條件測試

條件測試的語法為<<(?ifthen|else)>>。「if」部分可以是向前嚮後檢視表達式。如果用向前檢視,則語法變為:<<(?(?=regex)then|else)>>,其中else部
分是可選的。

如果if部分為true,則正則引擎會試圖符合then部分,否則引擎會試圖符合else部分。

需要記住的是,向前先後檢視並不實際消耗任何字元,因此後面的then與else部分的符合時從if測試前的部分開始進行嘗試。

16.  為正則表達式加入註釋

在正則表達式中加入註釋的語法是:<<(?#comment)>>

例:為用於符合有效日期的正則表達式加入註釋:

(?#year)(19|20)dd[- /.](?#month)(0[1-9]|1[012])[- /.](?#day)(0[1-9]|[12][0-9]|3[01])
原文出處:http://imsiren.com/archives/560

About

發佈留言