用 Java 实现一个正则表达式引擎

实现一个正则表达式需要几步?

就三步:

  • 分析正则表达式并构建出NFA
  • 根据NFA得出DFA
  • 根据DFA匹配字符串
    当然,这只是最基本的,但是可以了解到正则表达式的实现原理,这篇文章实现三个最基本的正则操作:
  • 连接 abc 匹配 abc
  • 或 ab|cd 匹配 ab或cd
  • 重复 a* 匹配 任意多个a

(功能较完备的正则引擎:https://github.com/Qzhangqi/Regex)

第一步 (分析正则表达式并构建出NFA )

例子:ab(c|d)*
把正则中的每个字母都表示成两个节点用一条边相连,如表示 a

用 Java 实现一个正则表达式引擎_第1张图片

节点和图的数据结构

class Node {
    int id; //节点的 id, 生成新节点时自动生成, 自增长
    Map> nextNodes; //Character 表示边上的字符(转换条件)
    boolean isEnd   = false; //是否是结束节点
}

class Graph {
    Node start; //图的开始节点
    Node end;  //图的结束节点
}

当读入一个a时这样处理

Node start = new Node();
Node end = new Node();
start.addNextNode('a', end);
graph = new Graph(start, end);

ab 两个字母首尾相连这样表示

用 Java 实现一个正则表达式引擎_第2张图片

Graph seriesGraph(Graph graph1, Graph graph2) {
     graph1.end.addNextNode(' ', graph2.start);
     graph1.end = graph2.end;
     return graph1;
}

(c|d) 括号内的就整体处理和算术一样 | 号这样表示

用 Java 实现一个正则表达式引擎_第3张图片

void parallelGraph(Graph graph1, Graph graph2) {
        Node start = new Node();
        Node end   = new Node();
        start.addNextNode(' ', graph1.start);
        start.addNextNode(' ', graph2.start);
        graph1.end.addNextNode(' ', end);
        graph2.end.addNextNode(' ', end);
        graph1.start = start;
        graph1.end = end;
    }

(c|d)* *号加一条从尾到首的空转移

用 Java 实现一个正则表达式引擎_第4张图片

void repeatGraph(Graph graph) {
        graph.end.addNextNode(' ', graph.start);
}

再把ab和(c|d)*连起来,最后结果是这样的

用 Java 实现一个正则表达式引擎_第5张图片

好了,这就一个NFA(非确定有限状态机)了, 你肯定也想到了,匹配字符串时就是将字符一个一个读入,然后根据读入的字符在 1 2 3 4 5 ....这些状态之间转换,然后判断是否到了结束状态就可以得出是否匹配成功,但是NFA和字符串进行匹配效率太低,原因有二:

  • 空边无用,需要消除,空边是构造NFA时起辅助作用的,匹配时就不再需要了
  • 转移状态非确定,需要合并相同边,考虑这样一种情况:
用 Java 实现一个正则表达式引擎_第6张图片

这在读入 a 时是转换到 2 状态,还是 3 状态,如果随机进入一个状态,到后面不匹配还需要回溯,影响性能。

第二步(根据NFA得出DFA )

例子是一个这样的正则表达式:a(a|b|c)*cba,使用上一步的办法构造出来的NFA是这样的


用 Java 实现一个正则表达式引擎_第7张图片
a(a|b|c)*cba

当把它转换成一个DFA时,我们决定不用 Node 这样的数据结构来储存这张图了,而是用一张表

class StateTable {
    State[][] stateTable; //状态表
    Map mapx; //转换条件到数组横坐标的映射
// 当你要实现一个像 \w 这样的通配字符时,可以用整形储存转换条件,然后再加一个映射关系
    Map mapy; //状态到数组纵坐标的映射
}
class State {
    List id; //状态的 id, 状态将有多个 id
    boolean isEnd; //是否是结束态
}

拿上图中的前四个点举个例

a b c
{0} {9} null null null
{9} null null null {7}
{7} null null null {1,3,5}
{1} {2} null null null

开始转换了

先把表的列确定下来,在读入正则时根据不同的字符填充 mapx

a b c

然后对起始点 bfs 但是这里的 bfs 有点不一样,只有空边的下一个节点才会添加入 bfs 队列,而非空边的下一节点直接加入 stateTable 状态表,这样就消除了空边
ps:这里给出的代码都是伪代码,是帮助理解的,和真正程序里的代码是有区别的,比如没有记录一个 State 是否是结束状态,在实现时要注意

    //startNode      起始点
    void start(Node startNode) {
        State state = new State();
        state.nodes.add(startNode);
        addline(state);
    }
    //添加一行
    void addline(State state) {
        for (Node node : state.nodes) {
            bfs(node, state);
        }
    }

    void bfs(Node snode, State state) {
        ArrayList bfsNodes = new ArrayList<>(); // bfs队列
        bfsNodes.add(snode);
        while (!bfsNodes.isEmpty()) {
            Node node = bfsNodes.remove(0);
            for (int i : node.nextNodes.keySet()) {
                for (Node node0 : node.nextNodes.get(i)) {
                    //上两行不用管,知道这里 node0 是 snode 的下一个节点
                    // i 是 snode 到 node0 的转换条件
                    if (!node0.look) {
                        node0.look = true;
                        if ((char)i != ' ')
                            //                    列号    行号       状态id
                            stateTable.addState((char)i, state, node0.getId());
                        else
                            bfsNodes.add(node0);
                    }
                }
            }
        }
    }

处理完头节点,这个表就这样了

a b c
{0} {9} null null

把刚刚添加的这一行中所有的 State 添加进一个队列 (添加过的不重复添加),从队列中一个一个取出 State 进行处理,直到队列空

void start(Node startNode) {
        State state = new State();
        state.nodes.add(startNode);
        ArrayList states = new ArrayList<>(); //状态队列
        states.add(state);
        while (!states.isEmpty()) {
            State state1 = states.pollFirst();
            addline(state1);
            //这个函数会根据 state1(行号 mapy中映射成 y 坐标)
            //去查 stateTable 将这一行中的所有状态添加进 states
            //添加时还会和 stateTable 中的所有 行号 比较,有了的就不添加了
            stateTable.add(state1, states);
        }
    }

完成后 stateTable 就是这个样子, 这就是 DFA (确定有限状态机)


用 Java 实现一个正则表达式引擎_第8张图片
a(a|b|c)*cba

第三步(根据DFA匹配字符串)

这步比较简单了,直接看代码

    public boolean match(String matched) {
        //头一行的行号
        State state = stateTable.getFirstLine();
        for (int i = 0; i < matched.length ; i++) {
            char ch = matched.charAt(i);
            //根据 行号 列号 获得状态
            state = stateTable.get(state, ch);
        }
        return state.isEnd;
    }

你可能感兴趣的:(用 Java 实现一个正则表达式引擎)