實現一個正則表示式引擎in Python(二)
專案地址:Regex in Python
在看一下之前正則的語法的 BNF 正規化
group ::= ("(" expr ")")* expr ::= factor_conn ("|" factor_conn)* factor_conn ::= factor | factor factor* factor ::= (term | term ("*" | "+" | "?"))* term ::= char | "[" char "-" char "]" | .
上一篇構造了 term 的簡單 NFA
構造複雜的 NFA
factor
根據上面的factor ::= (term | term ("*" | "+" | "?"))*
,先進行 term 的 NFA 的生成,然後根據詞法分析器來判斷要進行哪個 factor 的 NFA 的構造
def factor(pair_out): term(pair_out) if lexer.match(Token.CLOSURE): nfa_star_closure(pair_out) elif lexer.match(Token.PLUS_CLOSE): nfa_plus_closure(pair_out) elif lexer.match(Token.OPTIONAL): nfa_option_closure(pair_out)
nfa_star_closure
*操作就是對之前的 term 再生成兩個節點進行連線
def nfa_star_closure(pair_out): if not lexer.match(Token.CLOSURE): return False start = Nfa() end = Nfa() start.next_1 = pair_out.start_node start.next_2 = end pair_out.end_node.next_1 = pair_out.start_node pair_out.end_node.next_2 = end pair_out.start_node = start pair_out.end_node = end lexer.advance() return True
nfa_plus_closure
+和*的唯一區別就是必須至少匹配一個字元,所以不能從節點 2 直接跳轉到節點 4
def nfa_plus_closure(pair_out):
if not lexer.match(Token.PLUS_CLOSE):
return False
start = Nfa()
end = Nfa()
start.next_1 = pair_out.start_node
pair_out.end_node.next_1 = pair_out.start_node
pair_out.end_node.next_2 = end
pair_out.start_node = start
pair_out.end_node = end
lexer.advance()
return True
nfa_option_closure
?對應的則是隻能輸入 0 個或 1 個的匹配字元,所以相對於*就不能再次從節點 1 跳轉會節點 0
def nfa_option_closure(pair_out):
if not lexer.match(Token.OPTIONAL):
return False
start = Nfa()
end = Nfa()
start.next_1 = pair_out.start_node
start.next_2 = end
pair_out.end_node.next_1 = end
pair_out.start_node = start
pair_out.end_node = end
lexer.advance()
return True
factor_conn
factor_conn ::= factor | factor factor*
對於 factor_conn 就是一個或者多個 factor 相連線,也就是說如果有多個 factor,只要將它們的頭尾節點相連線
def factor_conn(pair_out):
if is_conn(lexer.current_token):
factor(pair_out)
while is_conn(lexer.current_token):
pair = NfaPair()
factor(pair)
pair_out.end_node.next_1 = pair.start_node
pair_out.end_node = pair.end_node
return True
expr
expr ::= factor_conn ("|" factor_conn)*
對於 expr 就是一個 factor_conn 或者多個 factor_conn 用|相連線
構建|的 NFA 就是生成兩個新節點,新生成的頭節點有兩條邊分別連線到 factor_conn 的頭節點,對於兩個 factor_conn 的尾節點分別生成一條邊連線到新生成的尾節點
def expr(pair_out):
factor_conn(pair_out)
pair = NfaPair()
while lexer.match(Token.OR):
lexer.advance()
factor_conn(pair)
start = Nfa()
start.next_1 = pair.start_node
start.next_2 = pair_out.start_node
pair_out.start_node = start
end = Nfa()
pair.end_node.next_1 = end
pair_out.end_node.next_2 = end
pair_out.end_node = end
return True
group
group 其實就是在 expr 上加了兩個括號,完全可以去掉
def group(pair_out):
if lexer.match(Token.OPEN_PAREN):
lexer.advance()
expr(pair_out)
if lexer.match(Token.CLOSE_PAREN):
lexer.advance()
elif lexer.match(Token.EOS):
return False
else:
expr(pair_out)
while True:
pair = NfaPair()
if lexer.match(Token.OPEN_PAREN):
lexer.advance()
expr(pair)
pair_out.end_node.next_1 = pair.start_node
pair_out.end_node = pair.end_node
if lexer.match(Token.CLOSE_PAREN):
lexer.advance()
elif lexer.match(Token.EOS):
return False
else:
expr(pair)
pair_out.end_node.next_1 = pair.start_node
pair_out.end_node = pair.end_node
構造 NFA 總結
可以看到對於整個 NFA 的構造,其實就是從最頂部開始向下遞迴,整個過程大概是:
expr -> factor_conn -> factor -> term
當遞迴過程回到factor_conn會根據
factor_conn ::= factor | factor factor*
判斷可不可以繼續構造下一個factor如果不可以就返回到expr,expr則根據
expr ::= factor_conn ("|" factor_conn)*
判斷能不能繼續構造下一個factor_conn重複上面的過程
匹配輸入字串
現在已經完成了NFA的構造,接下來就是通過這個NFA來對輸入的字串進行分析
一個例子
以剛剛的圖作為演示,假設0-1節點的邊是字符集0-9,4-5節點的邊是字符集a-z,其它都是空
所以這個圖表示的正則表示式[0-9]*[a-z]+
假設對於分析字串123a
closure
從開始節點8進行分析,我們要做的第一個操作就是算出在節點8時不需要任何輸入就可以到達的節點,這個操作稱為closure,得到closure集合
move
之後我們就需要根據NFA和當前的輸入字元來進行節點間的跳轉,得到的自然也是一個集合
closure操作
我們利用一個棧來實現closure操作
- 把傳入集合裡的所有節點壓入棧中
- 然後對這個棧的所有節點進行判斷是否有可以直接跳轉的節點
- 如果有的話直接壓入棧中
- 直到棧為空則結束操作
def closure(input_set):
if len(input_set) <= 0:
return None
nfa_stack = []
for i in input_set:
nfa_stack.append(i)
while len(nfa_stack) > 0:
nfa = nfa_stack.pop()
next1 = nfa.next_1
next2 = nfa.next_2
if next1 is not None and nfa.edge == EPSILON:
if next1 not in input_set:
input_set.append(next1)
nfa_stack.append(next1)
if next2 is not None and nfa.edge == EPSILON:
if next2 not in input_set:
input_set.append(next2)
nfa_stack.append(next2)
return input_set
move操作
- move操作就是遍歷當前的狀態節點集合,如果符合的edge的條件的話
- 就加入到下一個狀態集合中
def move(input_set, ch):
out_set = []
for nfa in input_set:
if nfa.edge == ch or (nfa.edge == CCL and ch in nfa.input_set):
out_set.append(nfa.next_1)
return out_set
match
現在最後一步就是根據上面的兩個操作進行字串的分析了
- 首先先計算出開始節點的closure集合
- 開始遍歷輸入的字串,從剛剛的closure集合開始做move操作
- 然後判斷當前的集合是不是可以作為接收狀態,只要當前集合有某個狀態節點沒有連線到其它節點,它就是一個可接收的狀態節點,能被當前NFA接收還需要一個條件就是當前字元已經全匹配完了
def match(input_string, nfa_machine):
start_node = nfa_machine
current_nfa_set = [start_node]
next_nfa_set = closure(current_nfa_set)
for i, ch in enumerate(input_string):
current_nfa_set = move(next_nfa_set, ch)
next_nfa_set = closure(current_nfa_set)
if next_nfa_set is None:
return False
if has_accepted_state(next_nfa_set) and i == len(input_string) - 1:
return True
return False
小結
這篇主要講了複雜一點的NFA節點的構建方法,和對利用構造的NFA來對輸入自負床進行分析。到目前為止,其實一個完整的正則表示式引擎已經完成了,但是如果想更近一步的話,還需要將NFA轉換成DFA,再進行DFA的最小