[出處:http://www.regexlab.com/zh/regtopic.htm]javascript
本文將逐步討論一些正則表達式的使用話題。本文爲本站基礎篇以後的擴展,在閱讀本文以前,建議先閱讀正則表達式參考文檔一文。html
有時候,咱們須要用正則表達式來分析一個計算式中的括號配對狀況。好比,使用表達式 "\( [^)]* \)" 或者 "\( .*? \)" 能夠匹配一對小括號。可是若是括號 內還嵌有一層括號的話 ,如 "( ( ) )",則這種寫法將不可以匹配正確,獲得的結果是 "( ( )" 。相似狀況的還有 HTML 中支持嵌套的標籤如 "<font> </font>" 等。本節將要討論的是,想辦法把有嵌套的的成對括號或者成對標籤匹配出來。java
匹配未知層次的嵌套:正則表達式
有的正則表達式引擎,專門針對這種嵌套提供了支持。而且在棧空間容許的狀況下,可以支持任意未知層次的嵌套:好比 Perl,PHP,GRETA 等。在 PHP 和 GRETA 中,表達式中使用 "(?R)" 來表示嵌套部分。spa
匹配嵌套了未知層次的 "小括號對" 的表達式寫法以下:"\( ([^()] | (?R))* \)"。htm
[Perl 和 PHP 的示例代碼]blog
匹配有限層次的嵌套:遞歸
對於不支持嵌套的正則表達式引擎,只能經過必定的辦法來匹配有限層次的嵌套。思路以下:ip
第一步,寫一個不能支持嵌套的表達式:"\( [^()]* \)","<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)層的表達式。這種方式寫出來的表達式,雖然看上去很長,可是這種表達式通過編譯後,匹配效率仍然是很高的。
可能有很多的人和我同樣,有過這樣的經歷:當咱們要匹配相似 "<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>" 更好。