正則基礎之——貪婪與非貪婪模式

1        概述

貪婪與非貪婪模式影響的是被量詞修飾的子表達式的匹配行爲,貪婪模式在整個表達式匹配成功的前提下,儘量多的匹配,而非貪婪模式在整個表達式匹配成功的前提下,儘量少的匹配。非貪婪模式只被部分NFA引擎所支持。 html

屬於貪婪模式的量詞,也叫作匹配優先量詞,包括: 正則表達式

{m,n}」、「{m,}」、「?」、「*」和「+」。 express

在一些使用NFA引擎的語言中,在匹配優先量詞後加上「?」,即變成屬於非貪婪模式的量詞,也叫作忽略優先量詞,包括: 測試

{m,n}?」、「{m,}?」、「??」、「*?」和「+?」。 優化

從正則語法的角度來說,被匹配優先量詞修飾的子表達式使用的就是貪婪模式,如「(Expression)+」;被忽略優先量詞修飾的子表達式使用的就是非貪婪模式,如「(Expression)+?」。 spa

對於貪婪模式,各類文檔的叫法基本一致,可是對於非貪婪模式,有的叫懶惰模式或惰性模式,有的叫勉強模式,其實叫什麼無所謂,只要掌握原理和用法,可以運用自如也就是了。我的習慣使用貪婪與非貪婪的叫法,因此文中都會使用這種叫法進行介紹。 .net

2       貪婪與非貪婪模式匹配原理

對於貪婪與非貪婪模式,能夠從應用和原理兩個角度進行理解,但若是想真正掌握,仍是要從匹配原理來理解的。 設計

先從應用的角度,回答一下「什麼是貪婪與非貪婪模式?」 orm

2.1     從應用角度分析貪婪與非貪婪模式

2.1.1  什麼是貪婪與非貪婪模式

先看一個例子 htm

舉例

源字符串:aa<div>test1</div>bb<div>test2</div>cc

正則表達式一:<div>.*</div>

匹配結果一:<div>test1</div>bb<div>test2</div>

正則表達式二:<div>.*?</div>

匹配結果二:<div>test1</div>(這裏指的是一次匹配結果,因此沒包括<div>test2</div>

根據上面的例子,從匹配行爲上分析一下,什是貪婪與非貪婪模式。

正則表達式一採用的是貪婪模式,在匹配到第一個「</div>」時已經可使整個表達式匹配成功,可是因爲採用的是貪婪模式,因此仍然要向右嘗試匹配,查看是否還有更長的能夠成功匹配的子串,匹配到第二個「</div>」後,向右再沒有能夠成功匹配的子串,匹配結束,匹配結果爲「<div>test1</div>bb<div>test2</div>」。固然,實際的匹配過程並非這樣的,後面的匹配原理會詳細介紹。

僅從應用角度分析,能夠這樣認爲,貪婪模式,就是在整個表達式匹配成功的前提下,儘量多的匹配,也就是所謂的「貪婪」,通俗點講,就是看到想要的,有多少就撿多少,除非再也沒有想要的了。

正則表達式二採用的是非貪婪模式,在匹配到第一個「</div>」時使整個表達式匹配成功,因爲採用的是非貪婪模式,因此結束匹配,再也不向右嘗試,匹配結果爲「<div>test1</div>」。

僅從應用角度分析,能夠這樣認爲,非貪婪模式,就是在整個表達式匹配成功的前提下,儘量少的匹配,也就是所謂的「非貪婪」,通俗點講,就是找到一個想要的撿起來就好了,至於還有沒有沒撿的就無論了。

2.1.2  關於前提條件的說明

在上面從應用角度分析貪婪與非貪婪模式時,一直提到的一個前提條件就是「整個表達式匹配成功」,爲何要強調這個前提,咱們看下下面的例子。

正則表達式三:<div>.*</div>bb

匹配結果三:<div>test1</div>bb

修飾「.」的仍然是匹配優先量詞「*」,因此這裏仍是貪婪模式,前面的「<div>.*</div>」仍然能夠匹配到「<div>test1</div>bb<div>test2</div>」,可是因爲後面的「bb」沒法匹配成功,這時「<div>.*</div>」必須讓出已匹配的「bb<div>test2</div>」,以使整個表達式匹配成功。這時整個表達式匹配的結果爲「<div>test1</div>bb」,「<div>.*</div>」匹配的內容爲「<div>test1</div>」。能夠看到,在「整個表達式匹配成功」的前提下,貪婪模式才真正的影響着子表達式的匹配行爲,若是整個表達式匹配失敗,貪婪模式只會影響匹配過程,對匹配結果的影響無從談起。

非貪婪模式也存在一樣的問題,來看下面的例子。

正則表達式四:<div>.*?</div>cc

匹配結果四:<div>test1</div>bb<div>test2</div>cc

這裏採用的是非貪婪模式,前面的「<div>.*?</div>」仍然是匹配到「<div>test1</div>」爲止,此時後面的「cc」沒法匹配成功,要求「<div>.*?</div>」必須繼續向右嘗試匹配,直到匹配內容爲「<div>test1</div>bb<div>test2</div>」時,後面的「cc」才能匹配成功,整個表達式匹配成功,匹配的內容爲「<div>test1</div>bb<div>test2</div>cc」,其中「<div>.*?</div>」匹配的內容爲「<div>test1</div>bb<div>test2</div>」。能夠看到,在「整個表達式匹配成功」的前提下,非貪婪模式才真正的影響着子表達式的匹配行爲,若是整個表達式匹配失敗,非貪婪模式沒法影響子表達式的匹配行爲。

2.1.3  貪婪仍是非貪婪——應用的抉擇

經過應用角度的分析,已基本瞭解了貪婪與非貪婪模式的特性,那麼在實際應用中,到底是選擇貪婪模式,仍是非貪婪模式呢,這要根據需求來肯定。

對於一些簡單的需求,好比源字符爲「aa<div>test1</div>bb」,那麼取得div標籤,使用貪婪與非貪婪模式均可以取得想要的結果,使用哪種或許關係不大。

可是就2.1.1中的例子來講,實際應用中,通常一次只須要取得一個配對出現的div標籤,也就是非貪婪模式匹配到的內容,貪婪模式所匹配到的內容一般並非咱們所須要的。

那爲何還要有貪婪模式的存在呢,從應用角度很難給出滿意的解答了,這就須要從匹配原理的角度去分析貪婪與非貪婪模式。

2.2     從匹配原理角度分析貪婪與非貪婪模式

若是想真正瞭解什麼是貪婪模式,什麼是非貪婪模式,分別在什麼狀況下使用,各自的效率如何,那就不能僅僅從應用角度分析,而要充分了解貪婪與非貪婪模式的匹配原理。

2.2.1  從基本匹配原理談起

NFA引擎基本匹配原理參考:正則基礎之——NFA引擎匹配原理

這裏主要針對貪婪與非貪婪模式涉及到的匹配原理進行介紹。先看一下貪婪模式簡單的匹配過程。

源字符串:"Regex"

正則表達式:".*"

 2-1

 

2-1

注:爲了可以看清晰匹配過程,上面的空隙留得較大,實際源字符串爲「」Regex」」,下同。

來看一下匹配過程。首先由第一個「"」取得控制權,匹配位置0位的「"」,匹配成功,控制權交給「.*」。

.*」取得控制權後,因爲「*」是匹配優先量詞,在可匹配可不匹配的狀況下,優先嚐試匹配。從位置1處的「R」開始嘗試匹配,匹配成功,繼續向右匹配,匹配位置2處的「e」,匹配成功,繼續向右匹配,直到匹配到結尾的「」,匹配成功,因爲此時已匹配到字符串的結尾,因此「.*」結束匹配,將控制權交給正則表達式最後的「"」。

"」取得控制權後,因爲已經在字符串結束位置,匹配失敗,向前查找可供回溯的狀態,控制權交給「.*」,由「.*」讓出一個字符,也就是字符串結尾處的「」,再把控制權交給正則表達式最後的「"」,由「"」匹配字符串結尾處的「"」,匹配成功。

此時整個正則表達式匹配成功,其中「.*」匹配的內容爲「Regex」,匹配過程當中進行了一次回溯。

接下來看一下非貪婪模式簡單的匹配過程。

源字符串:"Regex"

正則表達式:".*?"

 

 

2-2

看一下非貪婪模式的匹配過程。首先由第一個「"」取得控制權,匹配位置0位的「"」,匹配成功,控制權交給「.*?」。

.*?」取得控制權後,因爲「*?」是忽略優先量詞,在可匹配可不匹配的狀況下,優先嚐試不匹配,因爲「*」等價於「{0,}」,因此在忽略優先的狀況下,能夠不匹配任何內容。從位置1處嘗試忽略匹配,也就是不匹配任何內容,將控制權交給正則表達式最後的「」。

"」取得控制權後,從位置1處嘗試匹配,由「"」匹配位置1處的「R」,匹配失敗,向前查找可供回溯的狀態,控制權交給「.*?」,由「.*?」吃進一個字符,匹配位置1處的「R」,再把控制權交給正則表達式最後的「"」。

"」取得控制權後,從位置2處嘗試匹配,由「"」匹配位置1處的「e」,匹配失敗,向前查找可供回溯的狀態,重複以上過程,直到由「.*?」匹配到「x」爲止,再把控制權交給正則表達式最後的「"」。

"」取得控制權後,從位置6處嘗試匹配,由「"」匹配字符串最後的「"」,匹配成功。

此時整個正則表達式匹配成功,其中「.*?」匹配的內容爲「Regex」,匹配過程當中進行了五次回溯。

2.2.2  貪婪仍是非貪婪——匹配效率的抉擇

經過匹配原理的分析,能夠看到,在匹配成功的狀況下,貪婪模式進行了更少的回溯,而回溯的過程,須要進行控制權的交接,讓出已匹配內容或匹配未匹配內容,並從新嘗試匹配,在很大程度上下降匹配效率,因此貪婪模式與非貪婪模式相比,存在匹配效率上的優點。

2.2.1中的例子,僅僅是一個簡單的應用,讀者看到這裏時,是否會存在這樣的疑問,貪婪模式就必定比非貪婪模式匹配效率高嗎?答案是否認的。

舉例

需求:取得兩個「"」中的子串,其中不能再包含「"」。

正則表達式一:".*"

正則表達式二:".*?"

狀況一:當貪婪模式匹配到更多不須要的內容時,可能存在比非貪婪模式更多的回溯。好比源字符串爲「The word"Regex" means regular expression.」。

狀況二:貪婪模式沒法知足需求。好比源字符串爲「The phrase "regular expression" is called "Regex" for short.」。

對於狀況一,正則表達式一採用的貪婪模式,「.*」會一直匹配到字符串結束位置,控制權交給最後的「」,匹配不成功後,再進行回溯,因爲多匹配的內容「means regular expression.」遠遠超過需匹配內容自己,因此採用正則表達式一時,匹配效率會比使用正則表達式二的非貪婪模式低。

對於狀況二,正則表達式一匹配到的是「"regular expression" is called "Regex"」,連需求都不知足,天然也談不上什麼匹配效率的高低了。

以上兩種狀況是廣泛存在的,那麼是否是爲了知足需求,又兼顧效率,就只能使用非貪婪模式了呢?固然不是,根據實際狀況,變動匹配優先量詞修飾的子表達式,不但能夠知足需求,還能夠提升匹配效率。

源字符串:"Regex"

給出正則表達式三:"[^"]*"

看一下正則表達式三的匹配過程。

 2-3

 

2-3

首先由第一個「"」取得控制權,匹配位置0位的「"」,匹配成功,控制權交給「[^"]*」。

[^"]*」取得控制權後,因爲「*」是匹配優先量詞,在可匹配可不匹配的狀況下,優先嚐試匹配。從位置1處的「R」開始嘗試匹配,匹配成功,繼續向右匹配,匹配位置2處的「e」,匹配成功,繼續向右匹配,直到匹配到「x」,匹配成功,再匹配結尾的「」時,匹配失敗,將控制權交給正則表達式最後的「"」。

」取得控制權後,匹配字符串結尾處的「」,匹配成功。

此時整個正則表達式匹配成功,其中「[^"]*」匹配的內容爲「Regex」,匹配過程當中沒有進行回溯。

將量詞修飾的子表達式由範圍較大的「.」,換成了排除型字符組「[^"]」,使用的還是貪婪模式,很完美的解決了需求和效率問題。固然,因爲這一匹配過程沒有進行回溯,因此也不須要記錄回溯狀態,這樣就可使用固化分組,對正則作進一步的優化。

給出正則表達式四:"(?>[^"]*)"

固化分組並非全部語言都支持的,如.NET支持,而Java就不支持,可是在Java中卻可使用更簡單的佔有優先量詞來代替:"[^"]*+"

3       貪婪仍是非貪婪模式——再談匹配效率

通常來講,貪婪與非貪婪模式,若是量詞修飾的子表達式相同,好比「.*」和「.*?」,它們的應用場景一般是不一樣的,因此效率上通常不具備可比性。

而對於改變量詞修飾的子表達式,以知足需求時,好比把「.*」改成「[^"]*」,因爲修飾的子表達式已不一樣,也不具備直接的可對比性。可是在相同的子表達式,又均可以知足需求的狀況下,好比「[^"]*」和「[^"]*?」,貪婪模式的匹配效率一般要高些。

同時還有一個事實就是,非貪婪模式能夠實現的,經過優化量詞修飾的子表達式的貪婪模式均可以實現,而貪婪模式能夠實現的一些優化效果,卻未必是非貪婪模式能夠實現的。

貪婪模式還有一點優點,就是在匹配失敗時,貪婪模式能夠更快速的報告失敗,從而提高匹配效率。下面將全面考察貪婪與非貪婪模式的匹配效率。

3.1     效率提高——演進過程

在瞭解了貪婪與非貪婪模式的匹配基本原理以後,咱們再來從新看一下正則效率提高的演進過程。

需求:取得兩個「"」中的子串,其中不能再包含「"」。

源字符串:The phrase "regular expression" is called "Regex" for short.

正則表達式一:".*"

正則表達式一匹配的內容爲「"regular expression" is called "Regex"」,不符合要求。

提出正則表達式二:".*?"

首先「"」取得控制權,由位置0位開始嘗試匹配,直到位置11處匹配成功,控制權交給「.*?」,匹配過程同2.2.1中非貪婪模式的匹配過程。「.*?」匹配的內容爲「Regex」,匹配過程當中進行了四次回溯。

如何消除回溯帶來的匹配效率的損失,就是使用更小範圍的子表達式,採用貪婪模式,提出正則表達式三:"[^"]*"

首先「"」取得控制權,由位置0位開始嘗試匹配,直到位置11處匹配成功,控制權交給「[^"]*」,匹配過程同2.2.2節中非貪婪模式的匹配過程。「[^"]*」匹配的內容爲「Regex」,匹配過程當中沒有進行回溯。

3.2     效率提高——更快的報告失敗

以上討論的是匹配成功的演進過程,而對於一個正則表達式,在匹配失敗的狀況下,若是可以以最快的速度報告匹配失敗,也會提高匹配效率,這或許是咱們設計正則過程當中最容易忽略的。而在源字符串數據量很是大,或正則表達式比較複雜的狀況下,是否可以快速報告匹配失敗,將對匹配效率產生直接的影響。

下面將構建匹配失敗的正則表達式,對匹配過程進行分析。

如下匹配過程分析中,源字符串統一爲:The phrase "regular expression" is called "Regex" for short.

3.2.1  非貪婪模式匹配失敗過程分析

 

 3-1

3-1

構建匹配失敗的非貪婪模式的正則表達式:".*?"@

因爲最後的「@」的存在,這個正則表達式最後必定是匹配失敗的,那麼看一下匹配過程。

首先由「"」取得控制權,由位置0處開始嘗試匹配,匹配失敗,直到圖中標示的A處匹配成功,控制權交給「.*?」。

.*?」取得控制權後,由A後面的位置開始嘗試匹配,因爲是非貪婪模式,首先忽略匹配,將控制權交給「"」,同時記錄一下回溯狀態。「"」取得控制權後,由A後面的位置開始嘗試匹配,匹配字符「r」失敗,查找可供回溯的狀態,將控制權交給「.*?」,由「.*?」匹配字符「r」。重複以上過程,直到「.*?」匹配了B處前面的字符「n」,「"」匹配了B處的字符「」,將控制權交給「@」。由「@」匹配接下來的空格「 」,匹配失敗,查找可供回溯的狀態,控制權交給「.*?」,由「.*?」匹配空格。繼續重複以上匹配過程,直到由「.*?」匹配到字符串結束位置,將控制權交給「"」。因爲已是字符串結束位置,匹配失敗,報告整個表達式在位置11處匹配失敗,一輪匹配嘗試結束。

正則引擎傳動裝置使正則向前傳動,進入下一輪嘗試。後續匹配過程與第一輪嘗試匹配過程基本相似,能夠參考圖3-1

從匹配過程當中能夠看到,非貪婪模式的匹配失敗過程,幾乎每一步都伴隨着回溯過程,對匹配效率的影響是很大的。

3.2.2  貪婪模式匹配失敗過程分析——大範圍子表達式

 3-2

 

3-2

PS:以上分析過程圖示參考了《精通正則表達式》一書相關章節圖示。

構建匹配失敗的貪婪模式的正則表達式:".*"@

其中量詞修飾的子表達式爲匹配範圍較大的「.」,因爲最後的「@」的存在,這個正則表達式最後也是必定匹配失敗的,看一下匹配過程。

首先由「"」取得控制權,由位置0處開始嘗試匹配,匹配失敗,直到圖中標示的A處匹配成功,控制權交給「.*」。

.*」取得控制權後,由A後面的位置開始嘗試匹配,因爲是貪婪模式,優化嘗試匹配,一直匹配到字符串的結束位置,將控制權交給「"」。「"」取得控制權後,因爲已是字符串的結束位置,匹配失敗,查找可供回溯的狀態,將控制權交給「.*」,由「.*」讓出已匹配字符「.」。重複以上過程,直到後面「"」匹配了C處後面的字符「」,將控制權交給「@」。由「@」匹配接下來D處的空格「 」,匹配失敗,查找可供回溯的狀態,控制權交給「.*」,由「.*」讓出已匹配文本。繼續重複以上匹配過程,直到由「.*」讓出全部已匹配的文本到I處,將控制權交給「"」。「"」匹配失敗,因爲已經沒有可供回溯的狀態,報告整個表達式在位置11處匹配失敗,一輪匹配嘗試結束。

正則引擎傳動裝置使正則向前傳動,進入下一輪嘗試。後續匹配過程與第一輪嘗試匹配過程基本相似,能夠參考圖3-2

從匹配過程當中能夠看到,大範圍子表達式貪婪模式的匹配失敗過程,從整體上看,與非貪婪模式沒有什麼區別,最終進行的回溯次數與非貪婪模式基本一致,對匹配效率的影響仍然很大。

3.2.3  貪婪模式匹配失敗過程分析——改進的子表達式

 3-3

 

3-3

構建匹配失敗的貪婪模式的正則表達式:"[^"]*"@

其中量詞修飾的子表達式,改成匹配範圍較小的排除型字符組「[^"]」,因爲最後的「@」的存在,這個正則表達式最後也是必定匹配失敗的,看一下匹配過程。

首先由「"」取得控制權,由位置0處開始嘗試匹配,匹配失敗,直到圖中標示的A處匹配成功,控制權交給「[^"]*」。

[^"]*」取得控制權後,由A後面的位置開始嘗試匹配,因爲是貪婪模式,優先嚐試匹配,一直匹配到B處,將控制權交給「"」。「"」匹配接下來的的字符「"」,匹配成功,將控制權交給「@」。由「@」匹配接下來的空格「 」,匹配失敗,查找可供回溯的狀態,控制權交給「[^"]*」,由「[^"]*」讓出已匹配文本。繼續重複以上匹配過程,直到由「[^"]*」讓出全部已匹配的文本到C處,將控制權交給「"」。「"」匹配失敗,因爲已經沒有可供回溯的狀態,報告整個表達式在位置11處匹配失敗,一輪匹配嘗試結束。

正則引擎傳動裝置使正則向前傳動,進入下一輪嘗試。後續匹配過程與第一輪嘗試匹配過程基本相似,能夠參考圖3-3

從匹配過程當中能夠看到,使用了排除型字符組的貪婪模式的匹配失敗過程,從整體上看,大量減小了每輪迴溯的次數,能夠有效的提高匹配效率。

3.2.4  貪婪模式匹配失敗過程分析——固化分組

經過3.2.3節的分析能夠知道,因爲「[^"]*」使用了排除型字符組,那麼圖3-3中,在AB之間被匹配到的字符,就必定不會是字符「"」,因此BC之間回溯過程就是多餘的,也就是說在這之間的可供回溯的狀態徹底能夠不記錄。.NET中可使用固化分組,Java中可使用佔有優先量詞來實現這一效果。

 3-4

 

3-4

首先由「"」取得控制權,由位置0處開始嘗試匹配,匹配失敗,直到圖中標示的A處匹配成功,控制權交給「(?>[^"]*)」。

(?>[^"]*)」取得控制權後,由A後面的位置開始嘗試匹配,因爲是貪婪模式,優先嚐試匹配,一直匹配到B處,將控制權交給「"」,在這一匹配過程當中,不記錄任何可供回溯的狀態。「"」匹配接下來的字符「」,匹配成功,將控制權交給「@」。由「@」匹配接下來的空格「 」,匹配失敗,查找可供回溯的狀態,因爲已經沒有可供回溯的狀態,報告整個表達式在位置11處匹配失敗,一輪匹配嘗試結束。

正則引擎傳動裝置使正則向前傳動,進入下一輪嘗試。後續匹配過程與第一輪嘗試匹配過程基本相似,能夠參考圖3-4

從匹配過程當中能夠看到,使用了固化分組的貪婪模式的匹配失敗過程,沒有涉及到回溯,能夠最大限度的提高匹配效率。

3.3     非貪婪模式向貪婪模式的轉換

使用匹配範圍較大的子表達式時,貪婪模式與非貪婪模式匹配到的內容會有所不一樣,可是經過優化子表達式,非貪婪模式能夠實現的匹配,貪婪模式均可以實現。

好比在實際應用中,匹配img標籤的內容。

舉例

需求:取得img標籤中的圖片地址,src=後固定爲「

源字符串:<img class="test" src="/img/logo.gif" title="測試" />

正則表達式一:<img\b.*?src="(.*?)".*?>

匹配結果中,捕獲組1的內容即爲圖片地址。能夠看到,這個例子中使用的都是非貪婪模式,而根據上面章節的分析,後面兩個非貪婪模式均可以使用排除型字符組,將非貪婪模式轉換爲貪婪模式。

正則表達式二:<img\b.*?src="([^"]*)"[^>]*>

:「src=""」和標籤結束標記符「>」之間的屬性中,也可能出現字符「>」,但那是極端狀況,這裏不予討論。

後兩處非貪婪模式,能夠經過排除型字符組轉換爲貪婪模式,提升匹配效率,而「src=」前的非貪婪模式,因爲要排除的是一個字符序列「src=」,而不是單獨的某一個或幾個字符,因此不能使用排除型字符組。固然也不是沒有辦法,可使用順序環視來達到這一效果。

正則表達式三:<img\b(?:(?!src=).)*src="([^"]*)"[^>]*>

(?!src=).」表示這樣一個字符,從它開始,右側不能是字符序列「src=」,而「(?:(?!src=).)*」就表示符合上面規則的字符,有0個或無限多個。這樣就達到排除字符序列的目的,實現的效果同排除型字符組同樣,只不過排除型字符組排除的是一個或多個字符,而這種環視結構排除的是一個或多個有序的字符序列。

可是以順序環視的方式排除字符序列,因爲在匹配每個字符時,都要進行較多的判斷,因此相對於非貪婪模式,是提高效率仍是下降效率,要根據實際狀況進行分析。對於簡單的正則表達式,或是簡單的源字符串,通常來講是非貪婪模式效率高些,而對於數量較大源字符串,或是複雜的正則表達式,通常來講是貪婪模式效率高些。

好比上面取得img標籤中的圖片地址需求,基本上用正則表達二就能夠了;對於複雜的應用,如平衡組中,就須要使用結合環視的貪婪模式了。

以匹配嵌套div標籤的平衡組爲例:

Regex reg = new Regex(@"(?isx)                      #匹配模式,忽略大小寫,「.」匹配任意字符

                      <div[^>]*>                      #開始標記<div...>

                          (?>                         #分組構造,用來限定量詞*修飾範圍

                              <div[^>]*>  (?<Open>)   #命名捕獲組,遇到開始標記,入棧,Open計數加1

                          |                           #分支結構

                              </div>  (?<-Open>)      #狹義平衡組,遇到結束標記,出棧,Open計數減1

                          |                           #分支結構

                              (?:(?!</?div\b).)*      #右側不爲開始或結束標記的任意字符

                          )*                          #以上子串出現0次或任意屢次

                          (?(Open)(?!))               #判斷是否還有'OPEN',有則說明不配對,什麼都不匹配

                      </div>                          #結束標記</div>

                      ");

(?:(?!</?div\b).)*」這裏使用的就是結合環視的貪婪模式,雖然每匹一個字符都要作不少判斷,但這種判斷是基於字符的,速度很快,而若是這裏使用非貪婪模式,那麼每次要作的就是分支結構「|」的判斷了,而分支結構是很是影響匹配效率的,其代價遠遠高於對肯定字符的判斷。而另一個緣由,就是貪婪模式能夠結合固化分組來提高效率,而對非貪婪模式使用固化分組倒是沒有意義的。

4       貪婪與非貪婪——最後的回顧

4.1     一個例子的匹配原理回顧

再回過頭來看一下2.1.1節例子中正則,前面從應用角度進行了分析,但討論過匹配原理後會發現,匹配過程並非那麼簡單的,下面從匹配原理角度分析的匹配過程。

 4-1

 

4-1

首先由「<」取得控制權,由位置0位開始嘗試匹配,匹配字符「a」,匹配失敗,第一輪匹配結束。第二輪匹配從位置1開始嘗試匹配,一樣匹配失敗。第三輪從位置3開始嘗試匹配,匹配字符「<」,匹配成功,控制權交給「d」。

d」嘗試匹配字符「d」,匹配成功,控制權交給「i」。重複以上過程,直到由「>」匹配到字符「>」,控制權交給「.*」。

.*」屬於貪婪模式,將從B處後的字符「t」開始,一直匹配到E處,也就是字符串結束位置,將控制權交給「<」。

<」從字符串結束位置嘗試匹配,匹配失敗,向前查找可供回溯的狀態,把控制權交給「.*」,由「.*」讓出一個字符「c」,把控制權再交給「<」,嘗試匹配,匹配失敗,向前查找可供回溯的狀態。一直重複以上過程,直到「.*」讓出已匹配的字符「<」,實際上也就是讓出了已匹配的子串「</div>cc」爲止,「<」才匹配字符「<」成功,控制權交給「/」。

接下來由「/」、「d」、「i」、「v」分別匹配對應的字符成功,此時整個正則表達式匹配完畢。

4.2     貪婪與非貪婪——量詞的細節

4.2.1  區間量詞的非貪婪模式

前面提到的非貪婪模式,一直都是使用的「*?」,而沒有涉及到其它的區間量詞,對於「*?」和「+?」這樣的非貪婪模式,大多數接觸過正則表達式的人均可以理解,可是對於區間量詞的非貪婪模式,好比「{m,n}?」,要麼是沒見過,要麼是不理解,主要是這種應用場景很是少,因此被忽略了。

首先須要明確的一點,就是量詞「{m,n}」是匹配優先量詞,雖然它有了上限,可是在達到上限以前,可以匹配,仍是要儘量多的匹配的。而「{m,n}?」就是對應的忽略優先量詞了,在可匹配可不匹配的狀況下,儘量少的匹配。

接下來舉一個例子說明這種非貪婪模式的應用。

舉例(參考 限制字符長度與最小匹配):

需求:如何限制在長度爲100的字符串中,從頭匹配到最早出現的abc

csdn.{1,100}abc 這樣寫是最大匹配(1-100個字符串中,我須要最小的)

好比csdnfddabckjdsfjabc,匹配結果應爲:csdnfddabc

正則表達式:csdn.{1,100}?abc

或許對這個例子還有人不是很理解,可是想一想,其實「*」就等價於「{0,}」,「+」就等價於「{1,}」,「*?」也就是「{0,}?」,抽象出來也就是「{m,}?」,即上限爲無窮大。若是上限爲一個固定值,那就是「{m,n}?」,這樣應該也就能夠理解了。

{m}」沒有放在匹配優先量詞中,一樣的,「{m}?」雖然被部分語言所支持,可是也沒有放在忽略優先量詞中,主要是由於這兩種量詞,實現的效果是同樣的,只有被修飾的子表達式匹配m次才能匹配成功,且沒有可供回溯的狀態,因此也不存在是匹配優先仍是忽略優先的問題,也就不在本文的討論範圍內。事實上即便討論也沒有意義的,只要知道它們的匹配行爲也就是了。

4.2.2  忽略優先量詞的匹配下限

對於匹配優先量詞的匹配下限很好理解,「?」等價於「{0,1}」,它修飾的子表達式,最少匹配0次,最多匹配1次;「*」等價於「{0,}」,它修飾的子表達式,最少匹配0次,最多匹配無窮屢次;「+」等價於「{1,}」,它修飾的子表達式,最少匹配1次,最多匹配無窮屢次。

對於忽略優先量詞的下限,也是容易忽略的。

??」也是忽略優先量詞,被修飾的子表達式使用的也是非貪婪模式,「??」修飾的子表達式,最少匹配0次,最多匹配1次。在匹配過程當中,遵循非貪婪模式匹配原則,先不匹配,即匹配0次,記錄回溯狀態,只有不得不匹配時,纔去嘗試匹配。

*?」修飾的子表達式,最少匹配0次,最多匹配無窮屢次;「+?」修飾的子表達式,最少匹配1次,最多匹配無窮屢次,「+?」雖然使用的是非貪婪模式,在匹配過程當中,首先要匹配一個字符,以後纔是忽略匹配的,這一點也須要注意。

4.3     貪婪與非貪婪模式小結

Ø  從語法角度看貪婪與非貪婪

被匹配優先量詞修飾的子表達式,使用的是貪婪模式;被忽略優先量詞修飾的子表達式,使用的是非貪婪模式。

匹配優先量詞包括:「{m,n}」、「{m,}」、「?」、「*」和「+」。

忽略優先量詞包括:「{m,n}?」、「{m,}?」、「??」、「*?」和「+?」。

Ø  從應用角度看貪婪與非貪婪

貪婪與非貪婪模式影響的是被量詞修飾的子表達式的匹配行爲,貪婪模式在整個表達式匹配成功的前提下,儘量多的匹配;而非貪婪模式在整個表達式匹配成功的前提下,儘量少的匹配。非貪婪模式只被部分NFA引擎所支持。

Ø  從匹配原理角度看貪婪與非貪婪

能達到一樣匹配結果的貪婪與非貪婪模式,一般是貪婪模式的匹配效率較高。

全部的非貪婪模式,均可以經過修改量詞修飾的子表達式,轉換爲貪婪模式。

貪婪模式能夠與固化分組結合,提高匹配效率,而非貪婪模式卻不能夠。

相關文章
相關標籤/搜索