正則表達式引擎的構建——基於編譯原理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連接符,在向運算符棧中添加一個新的運算符時,需要比較棧頂運算符和將要添加的運算符之間的優先級,以決定是否先進行棧頂運算符的運算。

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




發佈了49 篇原創文章 · 獲贊 71 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章