讀古今文學網 > 精通正則表達式(第3版) > 表達式主導與文本主導 >

表達式主導與文本主導

Regex-Directed Versus Text-Directed

DFA和NFA反映了將正則表達式在應用算法上的根本差異。我把對應汽油機的NFA稱為「表達式主導(regex-directed)」引擎,而對應電動機的DFA稱為「文本主導(text-directed)」引擎。

NFA引擎:表達式主導

NFA Engine:Regex-Directed

我們來看用「to(nite|knight|night)」匹配文本『…tonight…』的一種辦法。正則表達式從「t」開始,每次檢查一部分(由引擎查看表達式的一部分),同時檢查「當前文本(current text)」是否匹配表達式的當前部分。如果是,則繼續表達式的下一部分,如此繼續,直到表達式的所有部分都能匹配,即整個表達式能夠匹配成功。

在「to(nite|knight|night)」的例子中,第一個元素是「t」,它將會重複嘗試,直到在目標字符串中找到『t』為止。之後,就檢查緊隨其後的字符是否能由「o」匹配,如果能,就檢查下面的元素。在本例中,「下面的元素」指「(nite|knight|night)」它的真正含義是「「nite」或者「knight」或者「night」」。引擎會依次嘗試這 3 種可能。我們(具有高級神經網絡的人類)能夠發現,如果待匹配的字符串是 tonight,第三個選擇能夠匹配。不論神經學起源(☞85)如何,表達式主導的引擎必須完全測試,才能得出結論。

嘗試「nite」的過程與之前一樣:「嘗試匹配「n」,然後是「i」,然後是「t」,最後是「e」。」如果這種嘗試失敗——就像本例,引擎會嘗試另一種可能,如此繼續下去,直到匹配成功或是報告失敗。表達式中的控制權在不同的元素之間轉換,所以我稱它為「表達式主導」。

NFA引擎在操作上的優點

實質上,在表達式主導的匹配過程中,每一個子表達式都是獨立的。這不同於反向引用,子表達式之間不存在內在聯繫,而只是整個正則表達式的各個部分。在子表達式與正則表達式的控制結構(多選分支、括號以及匹配量詞)的層級關係(layout)控制了整個匹配過程。

因為NFA引擎是正則表達式主導的,駕駛員(也就是編寫表達式的人)有充足的機會來實現他/她期望的結果(第5章和第6章將會告訴讀者,如何正確高效地實現目標)。現在看起來,這點還有些模糊,但過一段就會變清晰。

DFA引擎:文本主導

DFA Engine:Text-Directed

與表達式主導的NFA不同,DFA引擎在掃瞄字符串時,會記錄「當前有效(currently in the works)」的所有匹配可能。具體到 tonight的例子,引擎移動到 t時,它會在當前處理的匹配可能中添加一個潛在的可能:

接下來掃瞄的每個字符,都會更新當前的可能匹配序列。繼續掃瞄兩個字符以後的情況是:

有效的可能匹配變為兩個(knight被淘汰出局)。掃瞄到g時,就只剩下一個可能匹配了。當h和t匹配完成後,引擎發現匹配已經完成,報告成功。

我稱這種方式為「文本主導」,是因為它掃瞄的字符串中的每個字符都對引擎進行了控制。在本例中,某個未完成的匹配也許是任意多個(只要可行)匹配的開始。不合適的匹配可能在掃瞄後繼文字時會被去除。在某些情況下,「處理中的未終結匹配(partial match in progress)」可能就是一個完整的匹配。例如正則表達式「to(…)?」,括號內的部分並不是必須出現的,但考慮到匹配優先的性質,引擎仍然會嘗試匹配括號內的部分。匹配過程中,在嘗試括號內的部分時,完整匹配(『to』)已經保留下來,以應付括號中的內容無法匹配的情況。

如果引擎發現,文本中出現的某個字符會令所有處理中的匹配可能失效,就會返回某個之前保留的完整匹配。如果不存在這樣的完整匹配,則要報告在當前位置無法匹配。

第一想法:比較NFA與DFA

First Thoughts:NFA and DFA in Comparison

如果讀者根據上面介紹的知識比較NFA和DFA,可能會得出結論:一般情況下,文本主導的DFA引擎要快一些。正則表達式主導的NFA引擎,因為需要對同樣的文本嘗試不同的子表達式匹配,可能會浪費時間(就好像上面例子中的3個分支)。

這個結論是對的。在NFA的匹配過程中,目標文本中的某個字符可能會被正則表達式中的不同部分重複檢測(甚至有可能被同一部分反覆檢測)。即使某個字表達式能夠匹配,為了檢查表達式中剩下的部分,找到匹配,它也可能需要再一次應用(甚至可能反覆多次)。單獨的子表達式可能匹配成功,也可能失敗,但是,直到抵達正則表達式的末尾之前,我們都無法確知全局匹配成功與否(也就是說「不到最後關頭不能分勝負(It』s not over until the fat lady sings)」,但這句話又不符合本段的語境)。相反,DFA引擎則是 確定型的(deterministic)——目標文本中的每個字符只會檢查(最多)一遍。對於一個已經匹配的字符,你無法知道它是否屬於最終匹配(它可能屬於最終會失敗的匹配),但因為引擎同時記錄了所有可能的匹配,這個字符只需要檢測一次,如此而已。

正則表達式引擎所使用的兩種基本技術,都對應有正式的名字:非確定型有窮自動機(NFA)和確定型有窮自動機(DFA)。這兩個名字實在是太饒舌,所以我堅持只用 DFA 和 NFA。下文中不會出現它們的全稱了(注4)。

用戶需要面對的結果

因為NFA具有表達式主導的特性,引擎的匹配原理就非常重要。我已經說過,通過改變表達式的編寫方式,用戶可以對表達式進行多方面的控制。拿 tonight的例子來說,如果改變表達式的編寫方式,可能會節省很多工夫,比如下面這3種方式:

●「to(ni(ght|te)|knight)」

●「tonite|toknight|tonight」

●「to(k?night|nite)」

給出任意文本,這 3 個表達式都可以捕獲相同的結果,但是它們以不同的方式控制引擎。現在,我們還無法分辨這3者的優劣,不過接下來會看到。

DFA的情況相反——引擎會同時記錄所有的匹配選擇,因為這3個表達式最終能夠捕獲的文本相同,在寫法上的差異並無意義。取得一個結果可能有上百種途徑,但因為DFA能夠同時記錄它們(有點神奇,待稍後詳述),選擇哪一個表達式並無區別。對純粹的 DFA 來說,即使「abc」和「 [aa-a](b|b{1}|b)c」看來相差巨大,但其實是一樣的。

如果要描述DFA,我能想到的特徵有:

●DFA匹配很迅速。

●DFA匹配很一致。

●談論DFA匹配很惱人。

最終我會展開這3點。

因為NFA是表達式主導的,談論它是件很有意思的事情。NFA為創造性思維提供了豐富的施展空間。調校好一個表達式能帶來許多收益,調校不好則會帶來嚴重後果。這就好比發動機的熄火和點不著火,他們並不只是汽油發動機的專利。為了徹底弄明白這個問題,我們來看NFA最重要的部分:回溯(backtracking)。