動手寫一個正則表達式引擎

曾經有人開玩笑:
當碰到棘手問題的時候,可以考慮使用正則表達式
當考慮正則表達式的時候,又多了一個棘手的問題

日常工作中,正則表達式是一個非常強大的工具,編寫編譯器/解釋器的時候,正則表達式是必須的工具。自己動手寫一個正則表達式,有利于使用者以正則表達式的方式思考,也是一個非常好的鍛煉編碼能力的小項目

思路

正則表達式的背后其實是圖論算法,匹配的過程就是使用確定有限狀態機DFA或者非確定有限狀態機NFA模擬識別過程,兩者是等價的。更下一層,會使用有向圖的遍歷算法。

有向圖

class Digraph:
    """
    有向圖的鄰接表表示
    """
    def __init__(self, v):
        self.v = v  # 頂點數
        self.e = 0  # 邊數
        self.adj = [set() for _ in range(v)]  # 鄰接表
    
    def add_edge(self, edge):
        s, e = edge
        self.adj[s].add(e)
        self.e += 1

    def dfs(self, sources, marked=None):
        """
        ε閉包: 深度優先搜索, 記錄可達的頂點集
        """
        marked = marked or set()
        for s in sources:
            if s not in marked:
                marked.add(s)
                self.dfs(self.adj[s], marked)
        return marked

深度優先dfs給定多個起始節點,計算這些點開始可達的頂點集

簡單的正則引擎模型

正則表達式的定義:
一·空字符是正則表達式ε
二·單個字符是正則表達式
三·包含在括號()中的另一個正則表達式
四·兩個或多個連接起來的正則表達式
五·由或運算符|分割的兩個或多個正則表達式
六·由閉包運算符標記的一個正則表達式

閉包運算符有:*,+,?,本demo中只實現了 *

正則表達式的運行分為兩個階段:

  • 第一階段:編譯正則表達式,生成NFA或者DFA,對應初始化MyRE(本處時NFA)
  • 第二階段:識別目標文本,(在NFA上模擬DFA步驟)
class MyRE:
    """
    使用非確定有限狀態機(NFA)模擬匹配過程
    """
    def __init__(self, regexp):
        self.regexp = f'(.*{regexp}.*)'
        self.g = Digraph(len(self.regexp)+1)
        
        ops = []
        for i, c in enumerate(self.regexp):
            lp = i
            if c in '(|':
                ops.append(i)
            elif c == ')':
                ori = ops.pop()
                if self.regexp[ori] == '|':
                    lp = ops.pop()
                    self.g.add_edge([lp, ori+1])
                    self.g.add_edge([ori, i])
                else:
                    lp = ori
            if i < len(self.regexp)-1 and self.regexp[i+1] == '*':
                self.g.add_edge([lp, i+1])
                self.g.add_edge([i+1, lp])
            if c in '(*)':
                self.g.add_edge([i, i+1])

    def recognizes(self, txt):
        pc = self.g.dfs([0])
        for c in txt:
            match = set()  # 識別c后能夠到達的頂點集
            for v in pc:
                if v < len(self.regexp):
                    if self.regexp[v] == c or self.regexp[v] == '.':
                        match.add(v+1)
            pc = self.g.dfs(match)  # 計算ε閉包
        return len(self.regexp) in pc  # 包含結束狀態頂點

識別的過程中,從第一個字符和開始狀態開始,先計算開始狀態可以直接到達的狀態集(ε-閉包),然后識別下一個字符,然后再計算ε-閉包,再識別下一個字符,依次遞進。識別字符結束,如果結束時的狀態集包含結束狀態,就表示這個NFA接受文本。

測試運行

# 文件名: grep.py

if __name__ == '__main__':
    import sys
    pattern = sys.argv[1]
    search_file = sys.argv[2]
    my_re = MyRE(pattern)
    with open(search_file) as fp:
        for line in fp.readlines():
            line = line.strip()
            if my_re.recognizes(line):
                print(line)

效果

(env3.6.7) ?  mydemo cat my.txt
AC
AD
AAA
ABD
ADD
BCD
ABCCBD
BABAAA
BABBAAA
(env3.6.7) ?  mydemo python grep.py "(A*B|AC)D" my.txt
ABD
ABCCBD
(env3.6.7) ?  mydemo

補充說明

本demo的實現參考Sedgewick的《算法》(第四版)第五章正則表達式。
關于正則表達式的完整詳實的說明,請參考《編譯原理》(龍書)第三章詞法分析
關于正則表達式的使用,最好的書是《精通正則表達式》,入門可以參考《正則表達式必知必會》

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容