正則匹配原理之——逆序環視深刻

前幾天在CSDN論壇遇到這樣一個問題:正則表達式

var str="8912341253789"; spa

須要將這個字符串中的重複的數字給去掉,也就是結果89123457.net

首先須要說明的是,這種需求並不適合用正則來實現,至少,正則不是最好的實現方式。blog

這個問題自己不是本文討論的重點,本文所要討論的,主要是由這一問題的解決方案而引出的另外一個正則匹配原理問題。ci

先看一下針對這一問題自己給出的解決方案。字符串

string str = "8912341253789";get

Regex reg = new Regex(@"((\d)\d*?)\2");博客

while (str != (str = reg.Replace(str, "$1"))) { }string

richTextBox2.Text = str;ast

/*--------輸出--------

89123457

*/

基於此有朋友提出另外一個疑問,爲何使用下面的正則沒有效果

(?<=(?<value>\d).*?)\k<value>

由此也引出本文所要討論的逆序環視更深刻的一些細節,涉及到逆序環視的匹配原理和匹配過程。前面的兩篇博客中雖然也有介紹,但還不夠深刻,參考 正則基礎之——環視 正則應用之——逆序環視探索 。本文將以逆序環視和反向引用結合這種複雜應用場景,對逆序環視進行深刻探討。

先把問題簡化和抽象一下,上面的正則中用到了命名捕獲組和命名捕捉組的反向引用,這在必定程度上增長了問題的複雜度,寫成普通捕獲組,而且用「\d」代替範圍過大的「.」,以下

(?<=(\d)\d*?)\1

須要匹配的字符串,抽象一下,取兩種典型字符串以下。

源字符串一:878

源字符串二:9878

與上面正則表達式相似,正則表達式相應的也有四種形式

正則表達式一:(?<=(\d)\d*)\1

正則表達式二:(?<=(\d)\d*?)\1

正則表達式三:(?<=(\d))\d*\1

正則表達式四:(?<=(\d))\d*?\1

先看一下匹配結果:

string[] source = new string[] {"878", "9878" };

List<Regex> regs = new List<Regex>();

regs.Add(new Regex(@"(?<=(\d)\d*)\1"));

regs.Add(new Regex(@"(?<=(\d)\d*?)\1"));

regs.Add(new Regex(@"(?<=(\d))\d*\1"));

regs.Add(new Regex(@"(?<=(\d))\d*?\1"));

foreach (string s in source)

{

     foreach (Regex r in regs)

     {

          richTextBox2.Text += "源字符串: " + s.PadRight(8, ' ');

          richTextBox2.Text += "正則表達式: " + r.ToString().PadRight(18, ' ');

          richTextBox2.Text += "匹配結果: " + r.Match(s).Value + "\n------------------------\n";

     }

     richTextBox2.Text += "------------------------\n";

}

/*--------輸出--------

源字符串: 878     正則表達式: (?<=(\d)\d*)\1    匹配結果: 8

------------------------

源字符串: 878     正則表達式: (?<=(\d)\d*?)\1   匹配結果:

------------------------

源字符串: 878     正則表達式: (?<=(\d))\d*\1    匹配結果: 78

------------------------

源字符串: 878     正則表達式: (?<=(\d))\d*?\1   匹配結果: 78

------------------------

------------------------

源字符串: 9878    正則表達式: (?<=(\d)\d*)\1    匹配結果:

------------------------

源字符串: 9878    正則表達式: (?<=(\d)\d*?)\1   匹配結果:

------------------------

源字符串: 9878    正則表達式: (?<=(\d))\d*\1    匹配結果: 78

------------------------

源字符串: 9878    正則表達式: (?<=(\d))\d*?\1   匹配結果: 78

------------------------

------------------------

*/

這個結果也許會出乎不少人的意料以外,剛開始接觸這個問題時,我也同樣感到迷惑,放了兩天後,才靈機一觸,想通了問題的關鍵所在,下面將展開討論。

在此以前,可能還須要作兩點說明:

一、  下面討論的話題已經與本文開始提到的問題沒有多大關聯了,最初的問題主要是爲了引出本文的話題,問題自己不在討論範圍以內,而本文也主要是純理論的探討。

二、  本文適合有必定正則基礎的讀者。若是您對上面幾個正則的匹配結果和匹配過程感到費解,不要緊,下面就將爲您解惑;可是若是您對上面幾個正則中元字符和語法表明的意義都不清楚的話,仍是先從基礎看起吧。

2       逆序環視匹配原理深刻

正則表達式一:(?<=(\d)\d*)\1

正則表達式二:(?<=(\d)\d*?)\1

正則表達式三:(?<=(\d))\d*\1

正則表達式四:(?<=(\d))\d*?\1

上面的幾個正則表達式,能夠最終抽象爲「(?<=SubExp1)SubExp2」這樣的表達式,在作逆序環視原理分析時,根據「SubExp1」的特色,能夠概括爲三類:

一、  逆序環視中的子表達式「SubExp1」長度固定,正則表達式三和四屬於這一類,固然,這一類裏是包括「?」這一量詞的,但也僅限於這一個量詞。

二、  逆序環視中的子表達式「SubExp1」長度不固定,其中包含忽略優先量詞,如「*?」、「+?」、「{m,}?」等,也就是一般所說的非貪婪模式,正則表達式二屬於這一類。

三、  逆序環視中的子表達式「SubExp1」長度不固定,其中包含匹配優先量詞,「*」、「+」、「{m,}」等,也就是一般所說的貪婪模式,正則表達式一屬於這一類。

下面針對這三類正則表達式進行匹配過程的分析。

2.1     固定長度子表達式匹配過程分析

2.1.1  源字符串一 + 正則表達式三匹配過程

源字符串一:878

正則表達式三:(?<=(\d))\d*\1

首先在位置處開始嘗試匹配,由「(?<=(\d))」取得控制權,長度固定,只有一位,由位置處向左查找一位,失敗,「(?<=(\d))」匹配失敗,致使第一輪匹配嘗試失敗。

正則引擎傳動裝置向前傳動,由位置1處嘗試匹配,控制權交給「(?<=(\d))」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「8」成功,此時「(?<=(\d))」匹配成功,匹配結果爲位置1,捕獲組1匹配到的內容就是「8」,控制權交給「\d*」。因爲「\d*」爲貪婪模式,會優先嚐試匹配位置1後面的「7」和「8」,匹配成功,記錄回溯狀態,控制權交給「\1」。因爲前面捕獲組1捕獲到的內容是「8」,因此「\1」要匹配到「8」才能匹配成功,而此時已到達字符串結尾處,匹配失敗,「\d*」回溯,讓出最後的字符「8」,再將控制權交給「\1」, 由「\1」匹配最後的「8」成功,此時整個表達式匹配成功。因爲「(?<=(\d))」只匹配位置,不佔有字符,因此整個表達式匹配到的結果爲「78」,其中「\d*」匹配到的是「7」,「\1」匹配到的是「8」。

2.1.2  源字符串二 + 正則表達式三匹配過程

源字符串二:9878

正則表達式三:(?<=(\d))\d*\1

這一組合的匹配過程,與2.1.1節的匹配過程基本相似,只不過多了一輪匹配嘗試而已,這裏再也不贅述。

2.1.3  源字符串一 + 正則表達式四匹配過程

源字符串一:878

正則表達式四:(?<=(\d))\d*?\1

首先在位置處開始嘗試匹配,由「(?<=(\d))」取得控制權,長度固定,只有一位,由位置處向左查找一位,失敗,「(?<=(\d))」匹配失敗,致使第一輪匹配嘗試失敗。

正則引擎傳動裝置向前傳動,由位置1處嘗試匹配,控制權交給「(?<=(\d))」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「8」成功,此時「(?<=(\d))」匹配成功,匹配結是果爲位置1,捕獲組1匹配到的內容就是「8」,控制權交給「\d*?」。因爲「\d*?」爲非貪婪模式,會優先嚐試忽略匹配,記錄回溯狀態,控制權交給「\1」。因爲前面捕獲組1捕獲到的內容是「8」,因此「\1」要匹配到「8」才能匹配成功,而此時位置1後面的字符是「7」,匹配失敗,「\d*?」回溯,嘗試匹配位置1後面的字符「7」,再將控制權交給「\1」, 由「\1」匹配最後的「8」成功,此時整個表達式匹配成功。因爲「(?<=(\d))」只匹配位置,不佔有字符,因此整個表達式匹配到的結果爲「78」,其中「\d*?」匹配到的是「7」,「\1」匹配到的是最後的「8」。

這與2.1.1節組合的匹配過程基本一致,只不過就是「\d*」和「\d*?」匹配與回溯過程有所區別而已。

2.1.4  源字符串二 + 正則表達式四匹配過程

源字符串二:9878

正則表達式四:(?<=(\d))\d*?\1

這一組合的匹配過程,與2.1.3節的匹配過程基本相似,這裏再也不贅述。

2.2     非貪婪模式子表達式匹配過程分析

2.2.1  源字符串一 + 正則表達式二匹配過程

源字符串一:878

正則表達式二:(?<=(\d)\d*?)\1

首先在位置處開始嘗試匹配,由「(?<=(\d)\d*?)」取得控制權,長度不固定,至少一位,由位置處向左查找一位,失敗,「(?<=(\d)\d*?)」匹配失敗,致使第一輪匹配嘗試失敗。

正則引擎傳動裝置向前傳動,由位置1處嘗試匹配,控制權交給「(?<=(\d)\d*?)」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「8」成功,將控制權交給「\d*?」,因爲「\d*?」爲非貪婪模式,會優先嚐試忽略匹配,即不匹配任何內容,並記錄回溯狀態,此時「(\d)\d*?」匹配成功,那麼「(?<=(\d)\d*?)」也就匹配成功,匹配結果爲位置1,因爲此處的子表達式「(\d)\d*?」爲非貪婪模式,取得一個成功匹配項後,即交出控制權,同時丟棄全部回溯狀態。因爲前面捕獲組1捕獲到的內容是「8」,因此「\1」要匹配到「8」才能匹配成功,而此時位置1後面的字符是「7」,此時已無可供回溯的狀態,整個表達式在位置1處匹配失敗。

正則引擎傳動裝置向前傳動,由位置2處嘗試匹配,控制權交給「(?<=(\d)\d*?)」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「7」成功,將控制權交給「\d*?」,因爲「\d*?」爲非貪婪模式,會優先嚐試忽略匹配,即不匹配任何內容,並記錄回溯狀態,此時「(\d)\d*?」匹配成功,那麼「(?<=(\d)\d*?)」也就匹配成功,匹配結果爲位置2,因爲此處的子表達式「(\d)\d*?」爲非貪婪模式,取得一個成功匹配項後,即交出控制權,同時丟棄全部回溯狀態。因爲前面捕獲組1捕獲到的內容是「7」,因此「\1」要匹配到「7」才能匹配成功,而此時位置2後面的字符是「7」,此時已無可供回溯的狀態,整個表達式在位置2處匹配失敗。

位置3處的匹配過程也一樣道理,最後「\1」因無字符可匹配,致使整個表達式匹配失敗。

此時已嘗試了字符串全部位置,均匹配失敗,因此整個表達式匹配失敗,未取得任何有效匹配結果。

2.2.2  源字符串二 + 正則表達式二匹配過程

源字符串一:9878

正則表達式二:(?<=(\d)\d*?)\1

這一組合的匹配過程,與2.2.1節的匹配過程基本相似,這裏再也不贅述。

2.3     貪婪模式子表達式匹配過程分析

2.3.1  源字符串一 + 正則表達式一匹配過程

源字符串一:878

正則表達式二:(?<=(\d)\d*)\1

首先在位置處開始嘗試匹配,由「(?<=(\d)\d*)」取得控制權,長度不固定,至少一位,由位置處向左查找一位,失敗,「(?<=(\d)\d*)」匹配失敗,致使第一輪匹配嘗試失敗。

正則引擎傳動裝置向前傳動,由位置1處嘗試匹配,控制權交給「(?<=(\d)\d*)」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「8」成功,將控制權交給「\d*」,因爲「\d*」爲貪婪模式,會優先嚐試匹配,並記錄回溯狀態,但此時已沒有可用於匹配的字符,因此匹配失敗,回溯,不匹配任何內容,丟棄回溯狀態,此時「(\d)\d*」匹配成功,匹配內容爲「8」,那麼「(?<=(\d)\d*)」也就匹配成功,匹配結果是位置1,因爲此處的子表達式爲貪婪模式,「(\d)\d*」取得一個成功匹配項後,須要查找是否還有更長匹配,找到最長匹配後,纔會交出控制權。再向左查找,已沒有字符,「8」已經是最長匹配,此時交出控制權,同時丟棄全部回溯狀態。因爲前面捕獲組1捕獲到的內容是「8」,因此「\1」要匹配到「8」才能匹配成功,而此時位置1後面的字符是「7」,此時已無可供回溯的狀態,整個表達式在位置1處匹配失敗。

正則引擎傳動裝置向前傳動,由位置2處嘗試匹配,控制權交給「(?<=(\d)\d*)」,向左查找一位,接着將控制權交給「(\d)」,更進一步的將控制權交給「\d」。「\d」取得控制權後,向右嘗試匹配,匹配「7」成功,將控制權交給「\d*」,因爲「\d*」爲貪婪模式,會優先嚐試匹配,並記錄回溯狀態,但此時已沒有可用於匹配的字符,因此匹配失敗,回溯,不匹配任何內容,丟棄回溯狀態,此時「(\d)\d*」匹配成功,匹配內容爲「7」,那麼「(?<=(\d)\d*)」也就匹配成功,匹配結果是位置2,因爲此處的子表達式爲貪婪模式,「(\d)\d*」取得一個成功匹配項後,須要查找是否還有更長匹配,找到最長匹配後,纔會交出控制權。再向左查找,由位置處向右嘗試匹配,「\d」取得控制權後,匹配位置處的「8」成功,將控制權交給「\d*」,因爲「\d*」爲貪婪模式,會優先嚐試匹配,並記錄回溯狀態,匹配位置1處的「7」成功,此時「(\d)\d*」匹配成功,那麼「(\d)\d*」又找到了一個成功匹配項,匹配內容爲「87」,其中捕獲組1匹配到的是「8」。再向左查找,已沒有字符,「87」已經是最長匹配,此時交出控制權,同時丟棄全部回溯狀態。因爲前面捕獲組1捕獲到的內容是「8」,因此「\1」匹配位置2處的「8」匹配成功,此時整個有達式匹配成功。

演示例程中用的是Match,只取一次匹配項,事實上若是用的是Matches,正則表達式是須要嘗試全部位置的,對於這一組合,一樣道理,在位置3處,因爲「\1」沒有字符可供匹配,因此匹配必定是失敗的。

至此,這一組合的匹配完成,有一個成功匹配項,匹配結果爲「8」,匹配開始位置爲位置2,也就是匹配到的內容爲第二個「8」。

2.3.2  源字符串二 + 正則表達式一匹配過程

源字符串二:9878

正則表達式二:(?<=(\d)\d*)\1

首先在位置處開始嘗試匹配,由「(?<=(\d)\d*)」取得控制權,長度不固定,至少一位,由位置處向左查找一位,失敗,「(?<=(\d)\d*)」匹配失敗,致使第一輪匹配嘗試失敗。

正則引擎傳動裝置向前傳動,由位置1處嘗試匹配,這一輪的匹配過程與2.3.1節的組合在位置1處的匹配過程相似,只不過「(\d)\d*」匹配到的是「9」,捕獲組1匹配到的也是「9」,所以「\1」匹配失敗,致使整個表達式在位置1處匹配失敗。

正則引擎傳動裝置向前傳動,由位置2處嘗試匹配,這一輪的匹配過程與2.3.1節的組合在位置2處的匹配過程相似。首先「(\d)\d*」找到一個成功匹配項,匹配到的內容是「8」,捕捉組1匹配到的內容也是「8」,此時再向左嘗試匹配,又找到一個成功匹配項,匹配到的內容是「98」,捕捉組1匹配到的內容也是「9」,再向左查找時,已無字符,因此「98」就是最長匹配項,「(?<=(\d)\d*)」匹配成功,匹配結果是位置2。因爲此時捕獲組1匹配的內容是「9」,因此「\1」在位置2處匹配失敗,致使整個表達式在位置2處匹配失敗。

正則引擎傳動裝置向前傳動,由位置3處嘗試匹配,這一輪的匹配過程與上一輪在位置2處的匹配過程相似。首先「(\d)\d*」找到一個成功匹配項「7」,繼續向左嘗試,又找到一個成功匹配項「87」,再向左嘗試,又找到一個成功匹配項「987」,此時已爲最長匹配,交出控制權,並丟棄全部回溯狀態。此時捕獲組1匹配的內容是「9 因此「\1」在位置3處匹配失敗,致使整個表達式在位置3處匹配失敗。

位置4處最終因爲「\1」沒有字符可供匹配,因此匹配必定是失敗的。

至此在源字符串全部位置的匹配嘗試都已完成,整個表達式匹配失敗,未找到成功匹配項。

2.4     小結

以上匹配過程分析,看似繁複,其實把握如下幾點就能夠了。

一、  逆序環視中子表達式爲固定長度時,要麼匹配成功,要麼匹配失敗,沒什麼好說的。

二、  逆序環視中子表達式爲非貪婪模式時,只要找到一個匹配成功項,即交出控制權,並丟棄全部可供回溯的狀態。

三、  逆序環視中子表達式爲貪婪模式時,只有找到最長匹配成功項時,纔會即交出控制權,並丟棄全部可供回溯的狀態。

也就是說,對於正則表達式「(?<=SubExp1)SubExp2」,一旦「(?<=SubExp1)」交出控制權,那麼它所匹配的位置就已固定,「SubExp1」所匹配的內容也已固定,而且沒有可供回溯的狀態了。

3       逆序環視匹配原理總結

再來總結一下正則表達式「(?<=SubExp1)SubExp2」的匹配過程吧。逆序環視的匹配原理圖以下圖所示。

 

 

3-1

3-1 逆序環視匹配原理圖

正則表達式「(?<=SubExp1)SubExp2」的匹配過程,可分爲主匹配流程和子匹配流程兩個流程,主匹配流程以下圖所示。

 

 

3-2 主匹配流程圖

主匹配流程:

一、  由位置處向右嘗試匹配,在找到知足「(?<=SubExp1)」最小長度要求的位置前,匹配必定是失敗的,直到找到這樣一個的位置xx知足「(?<=SubExp1)」最小長度要求;

二、  從位置x處向左查找知足「SubExp1」最小長度要求的位置y

三、  由「SubExp1」從位置y開始向右嘗試匹配,此時進入一個獨立的子匹配過程;

四、  若是「SubExp1」在位置y處子匹配還須要下一輪子匹配,則再向左查找一個y,也就是y-1從新進入獨立的子匹配過程,如此循環,直到再也不須要下一輪子匹配,子匹配成功則進入步驟5,最終匹配失敗則報告整個表達式匹配失敗;

五、  (?<=SubExp1)」成功匹配後,控制權交給後面的子表達式「SubExp2」,繼續嘗試匹配,直到整個表達式匹配成功或失敗,報告在位置x處整個表達式匹配成功或失敗;

六、  若有必要,繼續查找下一位置x’,並開始新一輪嘗試匹配。

子匹配流程以下圖所示。

 

3-3 子匹配流程圖

子匹配過程:

一、  進入子匹配後,源字符串即已肯定,也就是位置y和位置x之間的子字符串,而此時的正則表達式則變成了「^SubExp1$」,由於在這一輪子匹配當中,一旦匹配成功,則匹配開始位置必定是y,匹配結束位置必定是x

二、  子表達式長度固定時,要麼匹配成功,要麼匹配失敗,返回匹配結果,而且不須要下一輪子匹配;

三、  子表達式長度不固定時,區分是非貪婪模式仍是貪婪模式;

四、  若是是非貪婪模式,匹配失敗,報告失敗,而且要求進行下一輪子匹配;匹配成功,丟棄全部回溯狀態,報告成功,而且再也不須要嘗試下一輪子匹配;

五、  若是是貪婪模式,匹配失敗,報告失敗,而且要求進行下一輪子匹配;匹配成功,丟棄全部回溯狀態,報告成功,記錄本次匹配成功內容,而且要求嘗試下一輪子匹配,直到取得最長匹配爲止;

在特定的一輪匹配中,x的位置是固定的,而逆序環視中的子表達式「SubExp1」,在報告最終的匹配結果前,匹配開始的位置是不可預知的,須要通過一輪以上的子匹配才能肯定,但匹配結束的位置必定是位置x

固然,這只是針對特定的一輪匹配而言的,當這輪匹配失敗,正則引擎傳動裝置會向前傳動,使x=x+1,再進入下一輪匹配嘗試,直到整個表達式報告匹配成功或失敗爲止。

至此逆序環視的匹配原理已基本上分析完了,固然,還有更復雜的,如「SubExp1」中既包含貪婪模式子表達式,又包含非貪婪模式子表達式,但不管怎樣複雜,都是要遵循以上匹配原理的,因此只要理解了以上匹

相關文章
相關標籤/搜索