曾經有人開玩笑:
當碰到棘手問題的時候,可以考慮使用正則表達式
當考慮正則表達式的時候,又多了一個棘手的問題
日常工作中,正則表達式是一個非常強大的工具,編寫編譯器/解釋器的時候,正則表達式是必須的工具。自己動手寫一個正則表達式,有利于使用者以正則表達式的方式思考,也是一個非常好的鍛煉編碼能力的小項目
思路
正則表達式的背后其實是圖論算法,匹配的過程就是使用確定有限狀態機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的《算法》(第四版)第五章正則表達式。
關于正則表達式的完整詳實的說明,請參考《編譯原理》(龍書)第三章詞法分析
關于正則表達式的使用,最好的書是《精通正則表達式》,入門可以參考《正則表達式必知必會》