1. 程式人生 > >實現一個正則表示式引擎in Python(二)

實現一個正則表示式引擎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的最小