正則表達式--遞歸匹配與非貪婪匹配

正則表達式話題
引言
本文將逐步討論一些正則表達式的使用話題。本文爲本站基礎篇之後的擴展,在閱讀本文之前,建議先閱讀正則表達式參考文檔一文。
--------------------------------------------------------------------------------
1. 表達式的遞歸匹配
有時候,我們需要用正則表達式來分析一個計算式中的括號配對情況。比如,使用表達式 "/( [^)]* /)" 或者 "/( .*? /)" 可以匹配一對小括號。但是如果括號 內還嵌有一層括號的話 ,如 "( ( ) )",則這種寫法將不能夠匹配正確,得到的結果是 "( ( )" 。類似情況的還有 HTML 中支持嵌套的標籤如 "<font> </font>" 等。本節將要討論的是,想辦法把有嵌套的的成對括號或者成對標籤匹配出來。
匹配未知層次的嵌套:
有的正則表達式引擎,專門針對這種嵌套提供了支持。並且在棧空間允許的情況下,能夠支持任意未知層次的嵌套:比如 Perl,PHP,GRETA 等。在 PHP 和 GRETA 中,表達式中使用 "(?R)" 來表示嵌套部分。
匹配嵌套了未知層次的 "小括號對" 的表達式寫法如下:"/( ([^()] | (?R))* /)"。
[Perl 和 PHP 的示例代碼]
匹配有限層次的嵌套:
對於不支持嵌套的正則表達式引擎,只能通過一定的辦法來匹配有限層次的嵌套。思路如下:
第一步,寫一個不能支持嵌套的表達式:"/( [^()]* /)","<font>((?!</?font>).)*</font>"。 這兩個表達式在匹配有嵌套的文本時,只匹配最內層。
第二步,寫一個可匹配嵌套一層的表達式:"/( ([^()] | /( [^()]* /))* /)"。這個表達式在匹配嵌套層數大於一時,只能匹配最裏面的兩層,同時,這個表達式也能匹配沒有嵌套的文本或者嵌套的最裏層。
匹配嵌套一層的 "<font>" 標籤,表達式爲:"<font>((?!</?font>).|(<font>((?!</?font>).)*</font>))*</font>"。這個表達式在匹配 "<font>" 嵌套層數大於一的文本時,只匹配最裏面的兩層。
第三步,找到匹配嵌套(n)層的表達式 與 嵌套(n-1)層的表達式之間的關係。比如,能夠匹配嵌套(n)層的表達式爲:
[標記頭] ( [匹配 [標記頭] 和 [標記尾] 之外的表達式] | [匹配 n-1 層的表達式] )* [標記尾]
回頭來看前面編寫的“可匹配嵌套一層”的表達式:
  /( ( [^()] | /(([^()])*/) )* /)
<font> ( (?!</?font>). | (<font>((?!</?font>).)*</font>) )* </font>
             
PHP 和 GRETA 的簡便之處在於,匹配嵌套(n-1)層的表達式用 (?R) 表示:
/( ( [^()] | (?R) )* /)
第四步,依此類推,可以編寫出匹配有限(n)層的表達式。這種方式寫出來的表達式,雖然看上去很長,但是這種表達式經過編譯後,匹配效率仍然是很高的。
--------------------------------------------------------------------------------
2. 非貪婪匹配的效率
可能有不少的人和我一樣,有過這樣的經歷:當我們要匹配類似 "<td>內容</td>" 或者 "[b]加粗[/b]" 這樣的文本時,我們根據正向預搜索功能寫出這樣的表達式:"<td>([^<]|<(?!/td>))*</td>" 或者 "<td>((?!</td>).)*</td>"。
當發現非貪婪匹配之時,恍然大悟,同樣功能的表達式可以寫得如此簡單:"<td>.*?</td>"。 頓時間如獲至寶,凡是按邊界匹配的地方,儘量使用簡捷的非貪婪匹配 ".*?"。特別是對於複雜的表達式來說,採用非貪婪匹配 ".*?" 寫出來的表達式的確是簡練了許多。
然而,當一個表達式中,有多個非貪婪匹配時,或者多個未知匹配次數的表達式時,這個表達式將可能存在效率上的陷阱。有時候,匹配速度慢得莫名奇妙,甚至開始懷疑正則表達式是否實用。
效率陷阱的產生:
在本站基礎文章裏,對非貪婪匹配的描述中說到:“如果少匹配就會導致整個表達式匹配失敗的時候,與貪婪模式類似,非貪婪模式會最小限度的再匹配一些,以使整個表達式匹配成功。”
具體的匹配過程是這樣的:
"非貪婪部分" 先匹配最少次數,然後嘗試匹配 "右側的表達式"。
如果右側的表達式匹配成功,則整個表達式匹配結束。如果右側表達式匹配失敗,則 "非貪婪部分" 將增加匹配一次,然後再嘗試匹配 "右側的表達式"。
如果右側的表達式又匹配失敗,則 "非貪婪部分" 將再增加匹配一次。再嘗試匹配 "右側的表達式"。
依此類推,最後得到的結果是 "非貪婪部分" 以儘可能少的匹配次數,使整個表達式匹配成功。或者最終仍然匹配失敗。
當一個表達式中有多個非貪婪匹配,以表達式 "d(/w+?)d(/w+?)z" 爲例,對於第一個括號中的 "/w+?" 來說,右邊的 "d(/w+?)z" 屬於它的 "右側的表達式",對於第二個括號中的 "/w+?" 來說,右邊的 "z" 屬於它的 "右側的表達式"。
當 "z" 匹配失敗時,第二個 "/w+?" 會 "增加匹配一次",再嘗試匹配 "z"。如果第二個 "/w+?" 無論怎樣 "增加匹配次數",直至整篇文本結束,"z" 都不能匹配,那麼表示 "d(/w+?)z" 匹配失敗,也就是說第一個 "/w+?" 的 "右側" 匹配失敗。此時,第一個 "/w+?" 會增加匹配一次,然後再進行 "d(/w+?)z" 的匹配。循環前面所講的過程,直至第一個 "/w+?" 無論怎麼 "增加匹配次數",後邊的 "d(/w+?)z" 都不能匹配時,整個表達式才宣告匹配失敗。
其實,爲了使整個表達式匹配成功,貪婪匹配也會適當的“讓出”已經匹配的字符。因此貪婪匹配也有類似的情況。當一個表達式中有較多的未知匹配次數的表達式時,爲了讓整個表達式匹配成功,各個貪婪或非貪婪的表達式都要進行嘗試減少或增加匹配次數,由此容易形成一個大循環的嘗試,造成了很長的匹配時間。本文之所以稱之爲“陷阱”,因爲這種效率問題往往不易察覺。
舉例:"d(/w+?)d(/w+?)d(/w+?)z" 匹配 "ddddddddddd..." 時,將花費較長一段時間才能判斷出匹配失敗 。
效率陷阱的避免:
避免效率陷阱的原則是:避免“多重循環”的“嘗試匹配”。並不是說非貪婪匹配就是不好的,只是在運用非貪婪匹配的時候,需要注意避免過多“循環嘗試”的問題。
情況一:對於只有一個非貪婪或者貪婪匹配的表達式來說,不存在效率陷阱。也就是說,要匹配類似 "<td> 內容 </td>" 這樣的文本,表達式 "<td>([^<]|<(?!/td>))*</td>" 和 "<td>((?!</td>).)*</td>" 和 "<td>.*?</td>" 的效率是完全相同的。
情況二:如果一個表達式中有多個未知匹配次數的表達式,應防止進行不必要的嘗試匹配。
比如,對表達式 "<script language='(.*?)'>(.*?)</script>" 來說, 如果前面部分表達式在遇到 "<script language='vbscript'>" 時匹配成功後,而後邊的 "(.*?)</script>" 卻匹配失敗,將導致第一個 ".*?" 增加匹配次數再嘗試。而對於表達式真正目的,讓第一個 ".*?" 增加匹配成“vbscript'>”是不對的,因此這種嘗試是不必要的嘗試。
因此,對依靠邊界來識別的表達式,不要讓未知匹配次數的部分跨過它的邊界。前面的表達式中,第一個 ".*?" 應該改寫成 "[^']*"。後邊那個 ".*?" 的右邊再沒有未知匹配次數的表達式,因此這個非貪婪匹配沒有效率陷阱。於是,這個匹配腳本塊的表達式,應該寫成:"<script language='([^']*)'>(.*?)</script>" 更好。
正則表達式-分組構造

分組構造使您可以捕獲子表達式組並提高具有非捕獲預測先行和回顧後發修飾符的正則表達式的效率。下表描述了正則表達式分組構造。
分組構造 說明
( ) 捕獲匹配的子字符串(或非捕獲組;有關詳細信息,請參見正則表達式選項中的 ExplicitCapture 選項)。使用 () 的捕獲根據左括號的順序從 1 開始自動編號。捕獲元素編號爲零的第一個捕獲是由整個正則表達式模式匹配的文本。
(?<name> ) 將匹配的子字符串捕獲到一個組名稱或編號名稱中。用於 name 的字符串不能包含任何標點符號,並且不能以數字開頭。可以使用單引號替代尖括號,例如 (?'name')。
(?<name1-name2> ) 平衡組定義。刪除先前定義的 name2 組的定義並在 name1 組中存儲先前定義的 name2 組和當前組之間的間隔。如果未定義 name2 組,則匹配將回溯。由於刪除 name2 的最後一個定義會顯示 name2 的先前定義,因此該構造允許將 name2 組的捕獲堆棧用作計數器以跟蹤嵌套構造(如括號)。在此構造中,name1 是可選的。可以使用單引號替代尖括號,例如 (?'name1-name2')。
(?: ) 非捕獲組。
(?imnsx-imnsx: ) 應用或禁用子表達式中指定的選項。例如,(?i-s: ) 將打開不區分大小寫並禁用單行模式。有關詳細信息,請參見正則表達式選項。
(?= ) 零寬度正預測先行斷言。僅當子表達式在此位置的右側匹配時才繼續匹配。例如,/w+(?=/d) 與後跟數字的單詞匹配,而不與該數字匹配。此構造不會回溯。
(?! ) 零寬度負預測先行斷言。僅當子表達式不在此位置的右側匹配時才繼續匹配。例如,/b(?!un)/w+/b 與不以 un 開頭的單詞匹配。
(?<= ) 零寬度正回顧後發斷言。僅當子表達式在此位置的左側匹配時才繼續匹配。例如,(?<=19)99 與跟在 19 後面的 99 的實例匹配。此構造不會回溯。
(?<! ) 零寬度負回顧後發斷言。僅當子表達式不在此位置的左側匹配時才繼續匹配。
(?> ) 非回溯子表達式(也稱爲“貪婪”子表達式)。該子表達式僅完全匹配一次,然後就不會逐段參與回溯了。(也就是說,該子表達式僅與可由該子表達式單獨匹配的字符串匹配。)
命名捕獲根據左括號的從左到右的順序按順序編號(與非命名捕獲類似),但在對所有非命名捕獲進行計數之後纔開始對命名捕獲進行編號。例如,模式 ((?<One>abc)/d+)?(?<Two>xyz)(.*) 按編號和名稱產生下列捕獲組。(編號爲 0 的第一個捕獲總是指整個模式)。
編號 名稱 模式
0 0(默認名稱) ((?<One>abc)/d+)?(?<Two>xyz)(.*)
1 1(默認名稱) ((?<One>abc)/d+)
2 2(默認名稱) (.*)
3 1 (?<One>abc)
4 2 (?<Two>xyz)
正則表達式-替換和分組
替換使用 | 字符來允許在兩個或多個替換選項之間進行選擇。例如,可以擴展章節標題正則表達式,以返回比章標題範圍更廣的匹配項。但是,這並不象您可能認爲的那樣簡單。替換匹配 | 字符兩邊的儘可能最大的表達式。您可能認爲,下面的表達式匹配出現在行首和行尾、後面跟一個或兩個數字的 Chapter 或 Section:
/^Chapter|Section [1-9][0-9]{0,1}$/
很遺憾,上面的正則表達式要麼匹配行首的單詞 Chapter,要麼匹配行尾的單詞 Section 及跟在其後的任何數字。如果輸入字符串是 Chapter 22,那麼上面的表達式只匹配單詞 Chapter。如果輸入字符串是 Section 22,那麼該表達式匹配 Section 22。
若要使正則表達式更易於控制,可以使用括號來限制替換的範圍,即,確保它只應用於兩個單詞 Chapter 和 Section。但是,括號也用於創建子表達式,並可能捕獲它們以供以後使用,這一點在有關反向引用的那一節講述。通過在上面的正則表達式的適當位置添加括號,就可以使該正則表達式匹配 Chapter 1 或 Section 3。
下面的正則表達式使用括號來組合 Chapter 和 Section,以便表達式正確地起作用:
/^(Chapter|Section) [1-9][0-9]{0,1}$/
雖然這些表達式正確發揮作用,但 Chapter| Section 兩邊的括號還會使得兩個匹配單詞中的任何一個被捕獲以供將來使用。由於在上面的表達式中只有一組括號,因此,只有一個被捕獲的“子匹配項”。可以通過使用 RegExp 對象的 $1-$9 屬性來引用此子匹配項。
在上面的示例中,您只需要使用括號來組合單詞 Chapter 和 Section 之間的選擇。若要防止匹配被保存以備將來使用,請在括號內正則表達式模式之前放置 ?:。下面的修改提供相同的能力而不保存子匹配項:
/^(?:Chapter|Section) [1-9][0-9]{0,1}$/
除 ?: 元字符外,兩個其他非捕獲元字符創建被稱爲“預測先行”匹配的某些內容。正向預測先行使用 ?= 指定,它匹配處於括號中匹配正則表達式模式的起始點的搜索字符串。反向預測先行使用 ?! 指定,它匹配處於與正則表達式模式不匹配的字符串的起始點的搜索字符串。
例如,假設您有一個文檔,該文檔包含指向 Windows 3.1、Windows 95、Windows 98 和 Windows NT 的引用。再進一步假設,您需要更新該文檔,將指向 Windows 95、Windows 98 和 Windows NT 的所有引用更改爲 Windows 2000。下面的正則表達式(這是一個正向預測先行的示例)匹配 Windows 95、Windows 98 和 Windows NT:
/Windows(?=95 |98 |NT )/
找到一處匹配後,緊接着就在匹配的文本(不包括預測先行中的字符)之後搜索下一處匹配。例如,如果上面的表達式匹配 Windows 98,將在 Windows 之後而不是在 98 之後繼續搜索。  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章