1. 程式人生 > >正則表示式引擎的構建——基於編譯原理DFA(龍書第三章)——2 構造抽象語法樹

正則表示式引擎的構建——基於編譯原理DFA(龍書第三章)——2 構造抽象語法樹

簡要介紹

    構造抽象語法樹是構造基於DFA的正則表示式引擎的第一步。目前在我實現的這個正則表示式的雛形中,正則表示式的運算子有3種,表示選擇的|運算子,表示星號運算的*運算子,表示連線的運算子cat(在實際正則表示式中被省去)。

例如對於正則表示式a*b|c,在a*和b之間省略了連線運算子cat。其中|、cat運算子是雙目運算子,*運算子是單目運算子。


下圖來自編譯原理一書:


對(a|b)*abb構造語法樹,需要注意的是,此圖中在原正則表示式的末尾添加了一個#號表示接受狀態。在我自己的程式碼中沒有新增最後一個#號,而是用 eType_END 型別的詞法單元表示正則表示式的末尾和DFA的接受狀態。


構造正則表示式的抽象語法樹的過程和構造算術表示式的抽象語法樹的過程類似,都一樣會存在運算子優先順序和括號處理的問題。有差異的地方是正則表示式中存在單目運算子*,而普通的算術表示式中都是雙目運算子。


構造正則表示式語法樹的過程基於詞法分析,這裡的詞法分析就比較簡單了,因為一個字元就對應一個詞法單元,需要注意的地方是:

1 在兩個非運算子、右括號左括號對之間需要新增cat連線運算子。

2 在尾部需要加入一個 eType_END 型別的詞法單元表示正則表示式的末尾和DFA的接受狀態。


語法樹展示

根據正則表示式得到的語法樹如下所示:


支援轉義字元(右斜槓\)的模式串:


在我寫的詞法分析中支援萬用字元點號(.),支援轉義字元(右斜槓\加特殊字元)。另外這個語法分析樹的列印方式大家也可以從我的程式碼中找到實現方法^_^。

在以上各個語法樹中,列印輸出時遮蔽了尾部的eType_END節點。


構建語法樹主要需要的物件和資料結構

構建語法樹主要需要的物件和資料結構如下:


整個語法樹的構建過程中需要一個詞法分析器Lex,詞法分析器從左到右逐個字元地掃描正則表示式,根據遇到的字元返回正確的Token給語法樹構建器,對於不合法的正則表示式給出報錯資訊(例如轉義字元\後面跟的不是特殊字元)。

語法樹構建器拿到詞法分析器返回的詞法Token後,開始進行自下而上的建樹過程,在不考慮括號的情況下,正確的正則表示式的第一個詞法Token應該是一個非運算子,它被包裝為語法樹節點結構然後被壓入語法樹構建器的語法樹節點棧中。

之後第二個詞法Token可能是一個運算子也可能是一個非運算子,如果是非運算子,則需要新增一個表示連線的cat運算子到運算子棧中,並將得到的運算元Token包裝為語法樹節點壓入語法樹節點棧中。每次向運算子棧中壓入新的運算子new之前,都需要檢視當前運算子棧頂的運算子old,和new誰的優先順序更高,如果old的優先順序較高,則先處理old運算子(會用掉語法樹節點棧中的節點,運算得到的節點再壓回語法樹節點棧),old被處理完後,old出棧,接下來的棧頂元素成為old,再次和new進行比較,重複這個過程,直到old的運算子優先順序低於new,再將new運算子壓棧。如果遇到了左括號,則先將左括號壓入運算子棧中,在遇到右括號時需要將運算子棧中的節點從棧頂開始處理,直到處理到最靠近棧頂的左括號為止。

當正則表示式處理完後,最後再處理運算子棧中剩餘的運算子。

正確的結果應該是運算子棧為空,語法樹節點棧中有一個節點,這個節點就是整個語法樹的根節點。


結合例項介紹構建語法樹過程

接下來舉一個例項,對正則表示式(a|b)*a|bcd 構造語法樹。過程如下:
1 詞法分析器從左向右掃描表示式,先得到左括號,將左括號包裝成節點,壓入運算子棧中;
2 詞法分析器獲得的下一個節點為字元a,壓入語法樹節點棧中;
3 詞法分析器繼續獲取詞法Token,得到運算子|,壓入運算子棧中;
4 下一個字元是b,將b包裝成節點壓入語法樹節點棧中;
5 繼續獲取字元,得到右括號,此時語法樹構建器開始根據語法樹節點棧和運算子棧進行運算合併已有節點,直到在語法樹節點棧中遇到左括號為止。開始處理時語法樹節點棧和運算子棧中內容如下:
運算子棧:(|
語法樹節點棧:ab
運算子棧的棧頂運算子出棧,得到|運算子,這是一個雙目運算子,所以從語法樹節點棧中出棧2個節點b和a,|運算子和節點a節點b,得到新的節點(記為M),M再壓入語法樹節點棧,此時在運算子棧頂已經是左括號,將其出棧,節點合併結束。
兩個棧的內容如下:
運算子棧:空
語法樹節點棧:M
6 接下來是*號運算子,因為*號是優先順序最高的運算子,所以可以直接處理,無需進行運算子優先順序的比較,*號會消耗語法樹節點棧中一個節點(也就是M),*號運算子和M節點運算得到新的節點N,重新壓入節點棧中。

7 接下來詞法分析器得到字元a,但是在節點N和字元a之間需要插入一個連線cat運算子,我們把cat運算子用‘+’來表示,‘+’壓入運算子棧,a壓入節點棧。
8 詞法分析器得到的下一個Token是運算子|,在向運算子棧中壓入‘|’運算子之前,我們需要檢查運算子棧的棧頂運算子和當前想要壓棧的運算子的優先順序,如果棧頂運算子的優先順序大於等於將要壓棧的運算子,則需要先處理棧頂的運算子(這裡是一個迴圈的過程,也就是說處理完棧頂的運算子之後,還要繼續比較棧頂的運算子和將要壓棧的運算子之間的優先順序,以決定接下來該執行什麼步驟)。在這裡棧頂的運算子‘+’的優先順序比運算子‘|’的優先順序高,所以先進行棧頂運算子的運算,‘+’連線運算子將節點N和a組成為新的節點(記為P)並重新壓入節點棧中。然後運算子棧為空,此時把前面所說的“將要壓入運算子棧的‘|’運算子”壓入運算子棧。
9 下一個字元是b,此時不需要插入連線運算子,只需要將字元b包裝為節點壓入節點棧。
10 下一個字元是c,此時同樣需要插入一個連線運算子,在向運算子棧中壓入‘+’運算子之前,我們需要檢查運算子棧的棧頂運算子和當前想要壓棧的運算子的優先順序。在這裡‘+’的優先順序高於棧頂的‘|’,所以直接將運算子‘+’壓入運算子棧中,並將字元c包裝為節點壓入節點棧。
11 下一個字元是d,此時同樣需要插入一個連線運算子,在向運算子棧中壓入‘+’運算子之前,我們需要檢查運算子棧的棧頂運算子和當前想要壓棧的運算子的優先順序。在這裡兩個運算子相同,所以先處理運算子棧棧頂的運算子,‘+’運算子和節點棧中的b,c字元組成新的節點Q壓入節點棧,然後運算子棧頂的運算子為‘|’,‘+’的優先順序高於‘|’,所以不在處理運算子棧的棧頂運算子。將‘+’壓入運算子棧,將字元d包裝為節點壓入節點棧。
12 此時詞法分析器報告已經到達正則表示式的結尾,所以開始處理運算子棧中剩餘的運算子,從棧頂開始依次處理,首先遇到的是‘+’連線符,從節點棧中取出節點Q和字元d生成新的節點R壓回節點棧。
13 繼續處理運算子棧,棧頂運算子為‘|’,從節點棧中取出節點P和節點R生成新的節點S壓回節點棧。
14 此時運算子棧清空,節點棧中只有一個節點S,S就是最終生成的語法樹的根節點。(至此大功告成、功德圓滿^_^呼呼)
可以看出,我們遇到一個非運算子時,需要檢查是否需要新增cat連線符,在向運算子棧中新增一個新的運算子時,需要比較棧頂運算子和將要新增的運算子之間的優先順序,以決定是否先進行棧頂運算子的運算。

我們將上面每一個步驟中的運算子棧和節點棧以圖形的方式直觀地展現出來: