正則表達式中的量詞

自我感覺量詞是正則表達式裏最不容易理解的地方,所以特別爲它做了個總結。

爲了容易理解,會簡單地結合正則表達式引擎的工作方式來講。正則表達式引擎分爲文本導向型(Text-directed Engines)和正則表達式導向型(Regex-directed Engines)兩種。因爲基本上採用的是正則表達式導向型的引擎,所以下文關於引擎工作方式的部分都是基於正則表達式導向型引擎的。

一、沒有量詞時正則表達式引擎的工作方式
在沒有量詞之前,正則表達式的一個符號塊只能匹配文本中的一個符號,如[abc]匹配字符a或b或c。此時,正則表達式的匹配流程非常的簡單。

正則表達式引擎按從左到右的順序讀取正則表達式中的字符塊和文本中的字符,並檢查字符塊和字符是否匹配。根據匹配的結果和匹配符號的位置,後續的操作分爲四種。

匹配成功,且匹配的是正則表達式的第一個符號塊。說明文本中以該字符開始的一段字符串可能會是我們需要的字符串,所以引擎接着向右讀取正則表達式中的字符塊和文本中的字符進行匹配。爲了說明的方便,我們把這個字符記爲A。
匹配成功,且匹配的是正則表達式的最後一個符號塊。說明文本中從A開始到目前讀取位置的這一段字符是我們需要的字符串。於是,引擎將這段文本輸出,然後接着尋找下一個匹配的字符串,它繼續向右讀取文本中的字符,但是從頭開始讀取正則表達式中的字符塊,將它們進行匹配。
匹配成功,且匹配的是正則表達式中間的符號塊。說明文本中從A開始到目前爲止的這一段字符還是匹配的,如果之後的字符也匹配的話就找到所需的字符串。所以引擎接着向右讀取正則表達式中的字符塊和文本中的字符進行匹配。
匹配失敗,無論匹配的是正則表達式中的哪個符號塊。說明在從文本中從A開始的各種字符串中,並不存在我們所需的字符串。因此,引擎重新從A的後一個位置開始讀取字符,並從頭開始讀取正則表達式中的字符塊,進行匹配。
引擎不斷重複這樣的操作,直到讀取到文本的終結符。

比如,我們用正則表達式<[ou]l>去匹配文本<ol>This is ol</ol>。引擎先讀取正則表達式的第一個字符塊<和文本的第一個字符<。因爲它們成功匹配,所以引擎繼續讀取正則表達式的第二個字符塊[ou]和文本的第二個字符o,也成功匹配了,就繼續……,直到匹配到>,正則表達式和字符串<ol>完全匹配了,於是找到了第一個我們所需字符串。之後,引擎繼續讀取文本中的字符T和正則表達式中的第一個字符塊<,匹配失敗,引擎讀取文本中的下一個字符h,還是失敗,直到讀取到第15個字符<,匹配成功。然後引擎讀取正則表達式中的[ou]字符塊和文本中的/字符塊,匹配失敗,引擎重新從文本的第15個<之後開始讀取字符,從正則表達式的開頭讀取字符塊……直到引擎讀到了終結符,查找結束,找到了一個字符串,開始於文本的第1位,結束於文本的第4位。

注意:在這種實現方式下,正則表達式會優先匹配文本最左邊的字符串,而且匹配過的內容不會重複出現。如用正則表達式a.{4,4}去匹配My dear grandparent,得到的匹配結果是ar gr和andpa。其中沒有arent,因爲arent中的首字母a已經在andpa中被匹配了。這消除了匹配順序導致的歧義和是否能重複匹配導致的歧義。

二、量詞帶來的不確定性
但是,引入了量詞之後,事情就變得複雜了起來。量詞可以讓被修飾的字符重複若干次,如a*表示任意個a組成的字符串。量詞在正則表達式中起着很大的作用,但使用中總是出現意想不到的結果。

問題的起因是,被修飾字符的重複次數往往是不確定的。文本中以同一個字符開頭,可能會有好多種長度不同的字符串形式與正則表達式匹配,這就引起了歧義。如用<.*>去匹配<p>first</p> yeah,以第一個<打頭,可以有兩種長度不同的匹配,<p>和<p>first</p>,到底採用哪一種匹配呢?總不能靠運氣吧。於是,在量詞的基礎上又給他分了三類。

量詞有三類,貪婪型量詞(Greedy quantifiers),勉強型量詞(Reluctant quantifiers)和佔有型量詞(Possessive quantifiers)。

三、貪婪型量詞和勉強型量詞
由於引擎按從左往右的順序讀取,它並不能提前預知後面的字符串是什麼,也就不知道到底讓被修飾字符重複多少次能獲得一個匹配的字符串。雖然你一眼看去能看出來,上一個例子中的.*讓.重複1次匹配文本中的p和重複10次匹配文本中的p>first</p就能獲得一個匹配的字符串。但是計算機並不能,他只能一個一個可能性地試過去,他可能從重複次數爲0開始嘗試,0次不行就1次,1次不行就兩次……,也有可能從最大的重複次數開始試,最大的不行就最大減一次……。這樣直到獲得第一個匹配,那麼匹配成功,引擎就把它當作了匹配結果,或者最大到最小的所有的可能都試過了都不行,那麼當前這段字符串和正則表達式的匹配就失敗了。

有些同學可能對最大重複次數有些迷茫。它就是不考慮後面的符號塊能不能匹配的情況下,被修飾字符塊能連續匹配多少個文本中的字符。以上一個例子來說,當引擎將正則表達式的<和文本的第一個字符<匹配起來後,讀取到正則表達式的.*後,不考慮它之後的>,.*可以匹配文本中的p>first</p> yeah,這就是在嘗試匹配以文本中第一個字符<開頭的字符串時,它的最大重複次數。

從最大重複次數開始逐步減小重複次數的量詞是貪婪型量詞,而從最小重複次數開始逐步增大重複次數的量詞是勉強型量詞。

量詞默認是貪婪的,貪婪型量詞會使被修飾字符重複儘可能多的次數。用以上例子來說明引擎處理貪婪型量詞的方式,首先引擎讀取了正則表達式的首字符塊<和文本的首字符<,並且匹配成功。之後引擎會讀取正則表達式的.*,.可以匹配所有的字符,而*使.重複出現,而*又是貪婪的,所以引擎會不停地重複用.去匹配文本中的字符,直到讀到文本的終結符,.和終結符匹配失敗,這個時候.的重複次數達到最大了,而引擎讀取了之後的>,發現已經匹配不了了,這個時候引擎知道重複最大次數是不行的,於是讓.少重複了一次,.*少匹配了文本中最後的h,引擎拿着這個h和正則表達式的>去匹配,匹配還是不成功,所以這個重複此時還是不行,再減一……直到重複次數爲10爲止,.*吐出來的>和正則表達式的>成功匹配,而此時正則表達式中的字符塊被全部匹配了,結果就產生了。

在貪婪型量詞的後面加一個?就成了勉強型量詞,勉強型量詞會使被修飾字符重複儘可能少的次數。對以上例子來說,正則表達式爲<.*?>,匹配方式如下。首先引擎讀取了正則表達式的首字符塊<和文本的首字符<,並且匹配成功。之後引擎讀取正則表達式的.*?,*?是勉強的,引擎首先會讓.重複最小的次數,由於*代表任意次,所以最小次數爲0。之後,引擎會讀取.*?之後的>,嘗試着讓它去匹配文本中的下一個字符p,匹配失敗,這個時候引擎知道重複最小次數是匹配不了了,於是讓.多重複了一次,讓.*?匹配了p,之後再用>去匹配文本的下一個字符>,匹配成功,此時此時正則表達式中的字符塊被全部匹配了,結果就產生了。

四、佔有型量詞
在貪婪型量詞的後面加一個+就成了佔有型量詞,佔有型量詞讓被修飾字符重複最大次數。乍一看和貪婪型量詞沒啥區別啊,其實少了三個字,儘可能。還是用上面的例子來說,此時正則表達式爲.*+,引擎只會嘗試一種可能,前面的過程和貪婪型相同。當引擎發現.的重複次數達到最大了,引擎讀取了之後的>,發現匹配不了,這時引擎知道重複最大次數是不行的,然後整個匹配就失敗了,引擎不會去嘗試其他的重複次數。

佔有型量詞可以提高匹配的效率,因爲它不會遍歷所有可能性去匹配一段字符串,它只匹配重複次數最大的那種可能,行就行,不行就不行。如果我們需要的結果只有在最大重複次數時纔會出現,那其餘的嘗試都是不必要的

比如,我們要得到文本<p>one</p> two中每對尖括號包裹的內容,那我們可能會用<.*?>,但我們也可以用表達式<[^>]*+>。後者效率更高,因爲它不用嘗試其他的可能性。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章