1. 程式人生 > >正則表示式最短匹配

正則表示式最短匹配

正則表達

貪婪與懶惰

當正則表示式中包含能接受重複的限定符時,通常的行為是(在使整個表示式能得到匹配的前提下)匹配儘可能多的字元。考慮這個表示式:a.*b,它將會匹配最長的以a開始,以b結束的字串。如果用它來搜尋aabab的話,它會匹配整個字串aabab。這被稱為貪婪匹配。

有時,我們更需要懶惰匹配,也就是匹配儘可能少的字元。前面給出的限定符都可以被轉化為懶惰匹配模式,只要在它後面加上一個問號?。這樣.*?就意味著匹配任意數量的重複,但是在能使整個匹配成功的前提下使用最少的重複。現在看看懶惰版的例子吧:

a.*?b匹配最短的,以a開始,以b結束的字串。如果把它應用於aabab的話,它會匹配aab(第一到第三個字元)

ab(第四到第五個字元)

為什麼第一個匹配是aab(第一到第三個字元)而不是ab(第二到第三個字元)?簡單地說,因為正則表示式有另一條規則,比懶惰/貪婪規則的優先順序更高:最先開始的匹配擁有最高的優先權——The match that begins earliest wins。

表5.懶惰限定符
程式碼/語法 說明
*? 重複任意次,但儘可能少重複
+? 重複1次或更多次,但儘可能少重複
?? 重複0次或1次,但儘可能少重複
{n,m}? 重複n到m次,但儘可能少重複
{n,}? 重複n次以上,但儘可能少重複

處理選項

在C#中,你可以使用Regex(String, RegexOptions)建構函式

來設定正則表示式的處理選項。如:Regex regex = new Regex("/ba/w{6}/b", RegexOptions.IgnoreCase);

上面介紹了幾個選項如忽略大小寫,處理多行等,這些選項能用來改變處理正則表示式的方式。下面是.Net中常用的正則表示式選項:

表6.常用的處理選項
名稱 說明
IgnoreCase(忽略大小寫) 匹配時不區分大小寫。
Multiline(多行模式) 更改^$的含義,使它們分別在任意一行的行首和行尾匹配,而不僅僅在整個字串的開頭和結尾匹配。(在此模式下,$的精確含意是:匹配/n之前的位置以及字串結束前的位置.)
Singleline(單行模式)
更改.的含義,使它與每一個字元匹配(包括換行符/n)。
IgnorePatternWhitespace(忽略空白) 忽略表示式中的非轉義空白並啟用由#標記的註釋。
RightToLeft(從右向左查詢) 匹配從右向左而不是從左向右進行。
ExplicitCapture(顯式捕獲) 僅捕獲已被顯式命名的組。
ECMAScript(JavaScript相容模式) 使表示式的行為與它在JavaScript裡的行為一致。

一個經常被問到的問題是:是不是隻能同時使用多行模式和單行模式中的一種?答案是:不是。這兩個選項之間沒有任何關係,除了它們的名字比較相似(以至於讓人感到疑惑)以外。

平衡組/遞迴匹配

這裡介紹的平衡組語法是由.Net Framework支援的;其它語言/庫不一定支援這種功能,或者支援此功能但需要使用不同的語法。

有時我們需要匹配像( 100 * ( 50 + 15 ) )這樣的可巢狀的層次性結構,這時簡單地使用/(.+/)則只會匹配到最左邊的左括號和最右邊的右括號之間的內容(這裡我們討論的是貪婪模式,懶惰模式也有下面的問題)。假如原來的字串裡的左括號和右括號出現的次數不相等,比如( 5 / ( 3 + 2 ) ) ),那我們的匹配結果裡兩者的個數也不會相等。有沒有辦法在這樣的字串裡匹配到最長的,配對的括號之間的內容呢?

為了避免(/(把你的大腦徹底搞糊塗,我們還是用尖括號代替圓括號吧。現在我們的問題變成了如何把xx <aa <bbb> <bbb> aa> yy這樣的字串裡,最長的配對的尖括號內的內容捕獲出來?

這裡需要用到以下的語法構造:

  • (?'group') 把捕獲的內容命名為group,並壓入堆疊(Stack)
  • (?'-group') 從堆疊上彈出最後壓入堆疊的名為group的捕獲內容,如果堆疊本來為空,則本分組的匹配失敗
  • (?(group)yes|no) 如果堆疊上存在以名為group的捕獲內容的話,繼續匹配yes部分的表示式,否則繼續匹配no部分
  • (?!) 零寬負向先行斷言,由於沒有後綴表示式,試圖匹配總是失敗

如果你不是一個程式設計師(或者你自稱程式設計師但是不知道堆疊是什麼東西),你就這樣理解上面的三種語法吧:第一個就是在黑板上寫一個"group",第二個就是從黑板上擦掉一個"group",第三個就是看黑板上寫的還有沒有"group",如果有就繼續匹配yes部分,否則就匹配no部分。

我們需要做的是每碰到了左括號,就在壓入一個"Open",每碰到一個右括號,就彈出一個,到了最後就看看堆疊是否為空--如果不為空那就證明左括號比右括號多,那匹配就應該失敗。正則表示式引擎會進行回溯(放棄最前面或最後面的一些字元),儘量使整個表示式得到匹配。

<                         #最外層的左括號
    [^<>]*                #最外層的左括號後面的不是括號的內容
    (
        (
            (?'Open'<)    #碰到了左括號,在黑板上寫一個"Open"
            [^<>]*       #匹配左括號後面的不是括號的內容
        )+
        (
            (?'-Open'>)   #碰到了右括號,擦掉一個"Open"
            [^<>]*        #匹配右括號後面不是括號的內容
        )+
    )*
    (?(Open)(?!))         #在遇到最外層的右括號前面,判斷黑板上還有沒有沒擦掉的"Open";如果還有,則匹配失敗
>                         #最外層的右括號

平衡組的一個最常見的應用就是匹配HTML,下面這個例子可以匹配巢狀的<div>標籤<div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div>.

還有些什麼東西沒提到

我已經描述了構造正則表示式的大量元素,還有一些我沒有提到的東西。下面是未提到的元素的列表,包含語法和簡單的說明。你可以在網上找到更詳細的參考資料來學習它們--當你需要用到它們的時候。如果你安裝了MSDN Library,你也可以在裡面找到關於.net下正則表示式詳細的文件。

表7.尚未詳細討論的語法
程式碼/語法 說明
/a 報警字元(列印它的效果是電腦嘀一聲)
/b 通常是單詞分界位置,但如果在字元類裡使用代表退格
/t 製表符,Tab
/r 回車
/v 豎向製表符
/f 換頁符
/n 換行符
/e Escape
/0nn ASCII程式碼中八進位制程式碼為nn的字元
/xnn ASCII程式碼中十六進位制程式碼為nn的字元
/unnnn Unicode程式碼中十六進位制程式碼為nnnn的字元
/cN ASCII控制字元。比如/cC代表Ctrl+C
/A 字串開頭(類似^,但不受處理多行選項的影響)
/Z 字串結尾或行尾(不受處理多行選項的影響)
/z 字串結尾(類似$,但不受處理多行選項的影響)
/G 當前搜尋的開頭
/p{name} Unicode中命名為name的字元類,例如/p{IsGreek}
(?>exp) 貪婪子表示式
(?<x>-<y>exp) 平衡組
(?im-nsx:exp) 在子表示式exp中改變處理選項
(?im-nsx) 為表示式後面的部分改變處理選項
(?(exp)yes|no) 把exp當作零寬正向先行斷言,如果在這個位置能匹配,使用yes作為此組的表示式;否則使用no
(?(exp)yes) 同上,只是使用空表示式作為no
(?(name)yes|no) 如果命名為name的組捕獲到了內容,使用yes作為表示式;否則使用no
(?(name)yes) 同上,只是使用空表示式作為no