美文网首页
用 Java 实现一个正则表达式引擎

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

作者: ab029ac3022b | 来源:发表于2018-10-29 00:05 被阅读123次

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

就三步:

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

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

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

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

节点和图的数据结构

class Node {
    int id; //节点的 id, 生成新节点时自动生成, 自增长
    Map<Character, List<Node>> 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 两个字母首尾相连这样表示

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

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

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)* *号加一条从尾到首的空转移

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

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


好了,这就一个NFA(非确定有限状态机)了, 你肯定也想到了,匹配字符串时就是将字符一个一个读入,然后根据读入的字符在 1 2 3 4 5 ....这些状态之间转换,然后判断是否到了结束状态就可以得出是否匹配成功,但是NFA和字符串进行匹配效率太低,原因有二:
  • 空边无用,需要消除,空边是构造NFA时起辅助作用的,匹配时就不再需要了
  • 转移状态非确定,需要合并相同边,考虑这样一种情况:

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

第二步(根据NFA得出DFA )

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


a(a|b|c)*cba

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

class StateTable {
    State[][] stateTable; //状态表
    Map<Character, Integer> mapx; //转换条件到数组横坐标的映射
// 当你要实现一个像 \w 这样的通配字符时,可以用整形储存转换条件,然后再加一个映射关系
    Map<State, Integer> mapy; //状态到数组纵坐标的映射
}
class State {
    List<Integer> 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<Node> 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<State> 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 (确定有限状态机)


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实现网络爬虫二之Java正则表达式

    title: 用Java实现网络爬虫二之Java正则表达式tags: Java 网络爬虫 Spider Crawl...

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

    实现一个正则表达式需要几步? 就三步: 分析正则表达式并构建出NFA 根据NFA得出DFA 根据DFA匹配字符串当...

  • 8.2 正则表达式的匹配过程

    正则表达式所使用的理论模型就是有穷自动机,其具体实现称为正则引擎(regex engine)。用正则表达式处理字符...

  • jQuery不过如此

    首先要理解,JavaScript的语法是需要引擎来实现的.引擎是用其他语言写的(一般是c或者java).因为Jav...

  • 正则表达式规则

    正则引擎的具体实现有很多,都能支持正则表达式的解析。虽然正则表达式的原理是一样的,但是不同的正则引擎支持的正则表达...

  • 正则表达式的匹配原理是什么

    正则表达式是如何实现查找匹配的? 1,正则表达式的使用2,正则表达式匹配搜索算法3,正则表达式引擎:DFA和NFA...

  • 正则表达式的高级用法——正|反向预查

    我之前一直自诩精通正则表达式(毕竟我实现过正则引擎),但关于正则表达式有几个特殊用法我一直都不熟,一来是用的少,二...

  • 使用 Facebook 开源动画库 POP 实现真实衰减与弹簧动

    POP动画引擎简介 POP动画引擎中Layer与CALayer的联系与区别 用POP引擎实现衰减动画 用POP引擎...

  • freemarker

    FreeMarker是一个用Java语言编写的模板引擎,它基于模板来生成文本输出。它不仅可以用作表现层的实现技术,...

  • 模板字符串

    模版引擎 自己实现一个类似于EJS的模版引擎核心原理是:1.正则表达式代替字符串2.使用with包裹作用域3.为了...

网友评论

      本文标题:用 Java 实现一个正则表达式引擎

      本文链接:https://www.haomeiwen.com/subject/qgedtqtx.html