1. 程式人生 > >正則表示式-NFA引擎匹配原理

正則表示式-NFA引擎匹配原理

為什麼要了解引擎匹配原理

一個個音符雜亂無章的組合在一起,彈奏出的或許就是噪音,同樣的音符經過作曲家的手,就可以譜出非常動聽的樂曲,一個演奏者同樣可以照著樂譜奏出動聽的樂曲,但他/她或許不知道該如何去改變音符的組合,使得樂曲更動聽。

作為正則的使用者也一樣,不懂正則引擎原理的情況下,同樣可以寫出滿足需求的正則,但是不知道原理,卻很難寫出高效且沒有隱患的正則。所以對於經常使用正則,或是有興趣深入學習正則的人,還是有必要了解一下正則引擎的匹配原理的。

正則表示式引擎

正則引擎大體上可分為不同的兩類:DFA和NFA,而NFA又基本上可以分為傳統型NFA和POSIX NFA。

  • DFA Deterministic finite automaton 確定型有窮自動機
  • NFA Non-deterministic finite automaton 非確定型有窮自動機
  • Traditional NFA
  • POSIX NFA

DFA引擎因為不需要回溯,所以匹配快速,但不支援捕獲組,所以也就不支援反向引用和$number這種引用方式,目前使用DFA引擎的語言和工具主要有awk、egrep 和 lex。

POSIX NFA主要指符合POSIX標準的NFA引擎,它的特點主要是提供longest-leftmost匹配,也就是在找到最左側最長匹配之前,它將繼續回溯。同DFA一樣,非貪婪模式或者說忽略優先量詞對於POSIX NFA同樣是沒有意義的。

大多數語言和工具使用的是傳統型的NFA引擎,它有一些DFA不支援的特性:

  • 捕獲組、反向引用和$number引用方式;
  • 環視(Lookaround,(?<=…)、(?<!…)、(?=…)、(?!…)),或者有的有文章叫做預搜尋;
  • 忽略優化量詞(??、*?、+?、{m,n}?、{m,}?),或者有的文章叫做非貪婪模式;
  • 佔有優先量詞(?+、*+、++、{m,n}+、{m,}+,目前僅Java和PCRE支援),固化分組(?>…)。

引擎間的區別不是本文的重點,僅做簡要的介紹,有興趣的可參考相關文獻。

預備知識

字串組成

對於字串“abc”而言,包括三個字元和四個位置。

佔有字元和零寬度

正則表示式匹配過程中,如果子表示式匹配到的是字元內容,而非位置,並被儲存到最終的匹配結果中,那麼就認為這個子表示式是佔有字元的;如果子表示式匹配的僅僅是位置,或者匹配的內容並不儲存到最終的匹配結果中,那麼就認為這個子表示式是零寬度的。

佔有字元是互斥的,零寬度是非互斥的。也就是一個字元,同一時間只能由一個子表示式匹配,而一個位置,卻可以同時由多個零寬度的子表示式匹配。

控制權和傳動
正則的匹配過程,通常情況下都是由一個子表示式(可能為一個普通字元、元字元或元字元序列組成)取得控制權,從字串的某一位置開始嘗試匹配,一個子表示式開始嘗試匹配的位置,是從前一子表達匹配成功的結束位置開始的。如正則表示式:

(子表示式一)(子表示式二)

假設(子表示式一)為零寬度表示式,由於它匹配開始和結束的位置是同一個,如位置0,那麼(子表示式二)是從位置0開始嘗試匹配的。

假設(子表示式一)為佔有字元的表示式,由於它匹配開始和結束的位置不是同一個,如匹配成功開始於位置0,結束於位置2,那麼(子表示式二)是從位置2開始嘗試匹配的。

而對於整個表示式來說,通常是由字串位置0開始嘗試匹配的。如果在位置0開始的嘗試,匹配到字串某一位置時整個表示式匹配失敗,那麼引擎會使正則向前傳動,整個表示式從位置1開始重新嘗試匹配,依此類推,直到報告匹配成功或嘗試到最後一個位置後報告匹配失敗。

正則表示式簡單匹本過程

基礎匹配過程

源字串:abc

正則表示式:abc

匹配過程:

首先由字元“a”取得控制權,從位置0開始匹配,由“a”來匹配“a”,匹配成功,控制權交給字元“b”;由於“a”已被“a”匹配,所以“b”從位置1開始嘗試匹配,由“b”來匹配“b”,匹配成功,控制權交給“c”;由“c”來匹配“c”,匹配成功。

此時正則表示式匹配完成,報告匹配成功。匹配結果為“abc”,開始位置為0,結束位置為3。

含有匹配優先量詞的匹配過程——匹配成功(一)

源字串:abc

正則表示式:ab?c

量詞“?”屬於匹配優先量詞,在可匹配可不匹配時,會先選擇嘗試匹配,只有這種選擇會使整個表示式無法匹配成功時,才會嘗試讓出匹配到的內容。這裡的量詞“?”是用來修飾字符“b”的,所以“b?”是一個整體。

匹配過程:

首先由字元“a”取得控制權,從位置0開始匹配,由“a”來匹配“a”,匹配成功,控制權交給字元“b?”;由於“?”是匹配優先量詞,所以會先嚐試進行匹配,由“b?”來匹配“b”,匹配成功,控制權交給“c”,同時記錄一個備選狀態;由“c”來匹配“c”,匹配成功。記錄的備選狀態丟棄。

此時正則表示式匹配完成,報告匹配成功。匹配結果為“abc”,開始位置為0,結束位置為3。

 含有匹配優先量詞的匹配過程——匹配成功(二)

源字串:ac

正則表示式:ab?c

匹配過程:

首先由字元“a”取得控制權,從位置0開始匹配,由“a”來匹配“a”,匹配成功,控制權交給字元“b?”;先嚐試進行匹配,由“b?”來匹配“c”,同時記錄一個備選狀態,匹配失敗,此時進行回溯,找到備選狀態,“b?”忽略匹配,讓出控制權,把控制權交給“c”;由“c”來匹配“c”,匹配成功。

此時正則表示式匹配完成,報告匹配成功。匹配結果為“ac”,開始位置為0,結束位置為2。其中“b?”不匹配任何內容。

含有匹配優先量詞的匹配過程——匹配失敗

源字串:abd

正則表示式:ab?c

匹配過程:

首先由字元“a”取得控制權,從位置0開始匹配,由“a”來匹配“a”,匹配成功,控制權交給字元“b?”;先嚐試進行匹配,由“b?”來匹配“b”,同時記錄一個備選狀態,匹配成功,控制權交給“c”;由“c”來匹配“d”,匹配失敗,此時進行回溯,找到記錄的備選狀態,“b?”忽略匹配,即“b?”不匹配“b”,讓出控制權,把控制權交給“c”;由“c”來匹配“b”,匹配失敗。此時第一輪匹配嘗試失敗。

正則引擎使正則向前傳動,由位置1開始嘗試匹配,由“a”來匹配“b”,匹配失敗,沒有備選狀態,第二輪匹配嘗試失敗。

繼續向前傳動,直到在位置3嘗試匹配失敗,匹配結束。此時報告整個表示式匹配失敗。

含有忽略優先量詞的匹配過程——匹配成功

源字串:abc

正則表示式:ab??c

量詞“??”屬於忽略優先量詞,在可匹配可不匹配時,會先選擇不匹配,只有這種選擇會使整個表示式無法匹配成功時,才會嘗試進行匹配。這裡的量詞“??”是用來修飾字符“b”的,所以“b??”是一個整體。

匹配過程:

首先由字元“a”取得控制權,從位置0開始匹配,由“a”來匹配“a”,匹配成功,控制權交給字元“b??”;先嚐試忽略匹配,即“b??”不進行匹配,同時記錄一個備選狀態,控制權交給“c”;由“c”來匹配“b”,匹配失敗,此時進行回溯,找到記錄的備選狀態,“b??”嘗試匹配,即“b??”來匹配“b”,匹配成功,把控制權交給“c”;由“c”來匹配“c”,匹配成功。

此時正則表示式匹配完成,報告匹配成功。匹配結果為“abc”,開始位置為0,結束位置為3。其中“b??”匹配字元“b”。

零寬度匹配過程

源字串:a12

正則表示式:^(?=[a-z])[a-z0-9]+$

元字元“^”和“$”匹配的只是位置,順序環視“(?=[a-z])”只進行匹配,並不佔有字元,也不將匹配的內容儲存到最終的匹配結果,所以都是零寬度的。

這個正則的意義就是匹配由字母或數字組成的,第一個字元是字母的字串。

匹配過程:

首先由元字元“^”取得控制權,從位置0開始匹配,“^”匹配的就是開始位置“位置0”,匹配成功,控制權交給順序環視“(?=[a-z])”;

“(?=[a-z])”要求它所在位置右側必須是字母才能匹配成功,零寬度的子表示式之間是不互斥的,即同一個位置可以同時由多個零寬度子表示式匹配,所以它也是從位置0嘗試進行匹配,位置0的右側是字元“a”,符合要求,匹配成功,控制權交給“[a-z0-9]+”;

因為“(?=[a-z])”只進行匹配,並不將匹配到的內容儲存到最後結果,並且“(?=[a-z])”匹配成功的位置是位置0,所以“[a-z0-9]+”也是從位置0開始嘗試匹配的,“[a-z0-9]+”首先嚐試匹配“a”,匹配成功,繼續嘗試匹配,可以成功匹配接下來的“1”和“2”,此時已經匹配到位置3,位置3的右側已沒有字元,這時會把控制權交給“$”;

元字元“$”從位置3開始嘗試匹配,它匹配的是結束位置,也就是“位置3”,匹配成功。

此時正則表示式匹配完成,報告匹配成功。匹配結果為“a12”,開始位置為0,結束位置為3。其中“^”匹配位置0,“(?=[a-z])”匹配位置0,“[a-z0-9]+”匹配字串“a12”,“$”匹配位置3。