实现一个 DFA 正则表达式引擎 - 1. 语法树的构建

语法树的构建这里分为三步:

1. 补全正则表达式的省略部分(主要是省略的 concat 和 or 连接符)并翻译七个集合字 ‘\w‘, ‘\W‘, ‘\s‘, ‘\S‘, ‘\d‘, ‘\D‘ 和 ‘.‘;

2. 转换为逆波兰表达式;

3. 转换为语法树;

这里以正则表达式 (a*b|ab*) 为例,逐步解释构建语法树的过程。

1. 补全正则表达式的省略部分

符合我们要求的正则表达式只有三个正交的运算符,或运算连接运算重复量词。这里将正则表达式转换为以上三种运算加上两个括号运算符。转换规则比较简单,遍历正则,在集合 ‘[‘ ‘]‘ 中的所有字符之间补全省略的 “或运算”,重复量词转换为连接运算和或运算的混合,如量词 "x{2,3}",就要转换为 (xx|xxx) 的形式;量词 "?",就要转换为 (ε|x) 的形式 (什么是 ε ? 就是什么都没有,? 代表 0 到 1 个,0 即是什么都没有)。其余连接部分补全省略的 “连接运算”,顺便把所有的转义字符和集合字符处理成 ASCII 字符互相连接的形式(比如,\s 可以处理为 (空格 | tab))。这里附上代码的一种实现。

private void normalize() {
        int index = 0;
        while (index < regex.length()) {
            char ch = regex.charAt(index++);
            switch (ch) {
                case ‘[‘: {
                    tryConcat();
                    List<Character> all = new ArrayList<>();
                    boolean isComplementarySet;
                    if (regex.charAt(index) == ‘^‘) {
                        isComplementarySet = true;
                        index++;
                    } else isComplementarySet = false;
                    for (char next = regex.charAt(index++); next != ‘]‘; next = regex.charAt(index++)) {
                        if (next == ‘\\‘ || next == ‘.‘) {
                            String token;
                            if (next == ‘\\‘) {
                                char nextNext = regex.charAt(index++);
                                token = new String(new char[]{next, nextNext});
                            } else token = String.valueOf(next);
                            List<Character> tokenSet = CommonSets.interpretToken(token);
                            all.addAll(tokenSet);
                        } else all.add(next);
                    }
                    char[] chSet = CommonSets.minimum(CommonSets.listToArray(all));
                    if (isComplementarySet) {
                        chSet = CommonSets.complementarySet(chSet);
                    }
                    nodeList.add(new LeftBracket());
                    for (int i = 0; i < chSet.length; i++) {
                        nodeList.add(new LChar(chSet[i]));
                        if (i == chSet.length - 1 || chSet[i + 1] == 0) break;
                        nodeList.add(new BOr());
                    }
                    nodeList.add(new RightBracket());
                    itemTerminated = true;
                    break;
                }
                case ‘{‘: {
                    int least;
                    int most = -1;
                    boolean deterministicLength = false;
                    StringBuilder sb = new StringBuilder();
                    for (char next = regex.charAt(index++); ; ) {
                        sb.append(next);
                        next = regex.charAt(index++);
                        if (next == ‘}‘) {
                            deterministicLength = true;
                            break;
                        } else if (next == ‘,‘) {
                            break;
                        }
                    }
                    least = Integer.parseInt(sb.toString());

                    if (!deterministicLength) {
                        char next = regex.charAt(index);
                        if (next != ‘}‘) {
                            sb = new StringBuilder();
                            for (char nextNext = regex.charAt(index++); nextNext != ‘}‘; nextNext = regex.charAt(index++)) {
                                sb.append(nextNext);
                            }
                            if (sb.length() != 0) {
                                most = Integer.parseInt(sb.toString());
                            }
                        }
                    } else most = least;

                    performMany(least, most);
                    itemTerminated = true;
                    break;
                }
                case ‘(‘: {
                    tryConcat();
                    nodeList.add(new LeftBracket());
                    itemTerminated = false;
                    break;
                }
                case ‘)‘: {
                    nodeList.add(new RightBracket());
                    itemTerminated = true;
                    break;
                }
                case ‘*‘: {
                    performMany(0, -1);
                    itemTerminated = true;
                    break;
                }
                case ‘?‘: {
                    performMany(0, 1);
                    itemTerminated = true;
                    break;
                }
                case ‘+‘: {
                    performMany(1, -1);
                    itemTerminated = true;
                    break;
                }
                case ‘|‘: {
                    nodeList.add(new BOr());
                    itemTerminated = false;
                    break;
                }
                default: {
                    tryConcat();
                    if (ch == ‘\\‘ || ch == ‘.‘) {
                        String token;
                        if (ch == ‘\\‘) {
                            char next = regex.charAt(index++);
                            token = new String(new char[]{ch, next});
                        } else token = String.valueOf(ch);
                        List<Character> tokenSet = CommonSets.interpretToken(token);
                        nodeList.add(new LeftBracket());
                        nodeList.add(new LChar(tokenSet.get(0)));
                        for (int i = 1; i < tokenSet.size(); i++) {
                            nodeList.add(new BOr());
                            nodeList.add(new LChar(tokenSet.get(i)));
                        }
                        nodeList.add(new RightBracket());
                    } else nodeList.add(new LChar(ch));

                    itemTerminated = true;
                    break;
                }
            }
        }
    }

集合中若有取并集和取补集的部分,使用一个 boolean 桶去重即可。

经过这一步,以上正则表达式已经被转换成如下形式:

[[(], a, [M], {N}, [C], b, [O], a, [C], b, [M], {N}, [)]]

其中 [] 包裹的字符均为操作符 (operator),含义为 [C] : CONCAT; [M] : MANY; [O] : OR;

2. 转换为逆波兰表达式

逆波兰表达式的转换需要先定出各操作符的优先级,然后即可使用 shunting yard 算法轻松愉快地执行转换了。这里先给出优先级:

[M] > [C] > [O]

还有一个 [(] 运算符,我们给予一个特别优先级,具体原因之后再解释,先转一张表阐述一下 shunting yard 算法:

输入 动作 输出 (逆波兰表示法) 运算符栈 提示
3 将符号加入输出队列 3    
+ 将符号压入操作符堆栈 3 +  
4 将符号加入输出队列 3 4 +  
* 将符号压入操作符堆栈 3 4 * + *号的优先级高于+号
2 将符号加入输出队列 3 4 2 * +  
/ 将堆栈中元素弹出,加入输出队列 3 4 2 * + /号和*号优先级相同
将符号压入操作符堆栈 3 4 2 * / + /号的优先级高于+号
( 将符号压入操作符堆栈 3 4 2 * ( / +  
1 将符号加入输出队列 3 4 2 * 1 ( / +  
将符号压入操作符堆栈 3 4 2 * 1 − ( / +  
5 将符号加入输出队列 3 4 2 * 1 5 − ( / +  
) 将堆栈中元素弹出,加入输出队列 3 4 2 * 1 5 − ( / + 循环直到找到(号
将堆栈元素弹出 3 4 2 * 1 5 − / + 括号匹配结束
^ 将符号压入操作符堆栈 3 4 2 * 1 5 − ^ / + ^号的优先级高于/号
2 将符号加入输出队列 3 4 2 * 1 5 − 2 ^ / +  
^ 将符号压入操作符堆栈 3 4 2 * 1 5 − 2 ^ ^ / + ^号为从右至左求值
3 将符号加入输出队列 3 4 2 * 1 5 − 2 3 ^ ^ / +  
END 将栈中所有数据加入输出队列 3 4 2 * 1 5 − 2 3 ^ ^ / +  

* 表格来自维基 http://zh.wikipedia.org/wiki/%E8%B0%83%E5%BA%A6%E5%9C%BA%E7%AE%97%E6%B3%95

可以看出,由于括号具有不由分说的最高优先级,因此左括号要直接压入运算符栈,然而左括号却不是一个普通的双目运算符,故不能因为后入栈的低优先级运算符的到来被转压至输出栈。所以左括号需要有一个特殊的优先级使其一直留在运算符栈中等待右括号的到来。

代码实现如下:

    public void visit(LeftBracket leftBracket) {
        branchStack.push(leftBracket);
    }

    public void visit(RightBracket rightBracket) {
        try {
            for (Node node = branchStack.pop(); !(node instanceof LeftBracket); node = branchStack.pop()) {
                finalStack.push(node);
            }
        } catch (EmptyStackException e) {
            throw new InvalidSyntaxException(e);
        }
    }

    public void visit(LeafNode leafNode) {
        finalStack.push(leafNode);
    }

    public void visit(BranchNode branchNode) {
        while (!branchStack.isEmpty() && branchNode.getPri() != -1 && branchNode.getPri() <= branchStack.peek().getPri()) {
            finalStack.push(branchStack.pop());
        }
        branchStack.push(branchNode);
    }

    public Stack<Node> finish() {
        while (!branchStack.isEmpty()) {
            finalStack.push(branchStack.pop());
        }
        Stack<Node> reversedStack = new Stack<>();
        while (!finalStack.isEmpty()) {
            reversedStack.push(finalStack.pop());
        }
        return reversedStack;
    }

其中 finalStack 为输出栈,branchStack 为运算符栈,这里使用了一个 visitor 模式来实现不同性质节点的不同操作。这也是一个构建语法树时常用的设计模式。

经过这一步,我们的逆波兰表达式已经构建好了:

[[O], [C], [M], {N}, b, a, [C], b, [M], {N}, a]

表达式右端存放在栈中,右端为栈顶。

3. 转换为语法树

下面就是最后一步,转换为语法树了。有了构造好的逆波兰表达式,这一步相对比较简单,直接贴上代码:

    public void visit(LeafNode leafNode) {
        stack.push(leafNode);
    }

    public void visit(BranchNode branchNode) {
        Node right = stack.pop();
        Node left = stack.pop();
        branchNode.operate(left, right);
        stack.push(branchNode);
    }

operate 方法每种运算符都有不同的实现,总之作用都是根据运算符的语义连接节点,无需贴具体实现了。

下面给出最终的语法树结构:

                |---------------[O]-------------|               

        |-------[C]-----|               |-------[C]-----|       

    |---[M]             b               a           |---[M]     

    a                                               b           

到这里,就完成了 DFA 正则引擎构建的第一步:语法树的构建。

时间: 2024-10-23 08:05:53

实现一个 DFA 正则表达式引擎 - 1. 语法树的构建的相关文章

实现一个 DFA 正则表达式引擎 - 4. DFA 的最小化

(正则引擎已完成,Github) 最小化 DFA 是引擎中另外一个略繁琐的点(第一个是构建语法树). 基本思路是,先对 DFA 进行重命名,然后引入一个拒绝态 0,定义所有状态经过非接受字符转到状态 0,0 接受所有字符转换为自身.也就是说我们先建立一个转换表,然后把第一行填写为: state a b c d e f g h ... 0 0 0 0 0 0 0 0 0 0 再之后,我们讲 DFA 的其余状态从 1 开始重命名,填入状态表.代码实现如下: // rename all states

实现一个 DFA 正则表达式引擎 - 2. NFA 的构建

语法树如何实现对于之后步骤的繁琐程度有着举足轻重的影响.因为我们已经有了一棵简单优雅的语法树,所以我们的 NFA 很容易就可以构建出来.下面来回顾一下我们拥有的节点种类: 分支节点:Concat, Or, Many 叶子节点:Closure, Char 以下是转换的核心代码: public void visit(LChar lChar) { NFAState i = stateStack.pop(); NFAState f = stateStack.pop(); i.transitionRule

实现一个 DFA 正则表达式引擎 - 3. NFA 的确定化

我们上一节已经将 NFA 构建出来了,我们的 NFAState 对象的结构实际上是这样的: NFAState { private Set<NFAState> directTable; private Map<Character, Set<NFAState>> transitionMap; private int id; } 其中 transitionMap 是该状态接受 Character 后可以转换的状态的映射表. 那么以初始状态 0 为例,我们是不是可以认为状态 0

实现一个 DFA 正则表达式引擎 - 0. 要求

决定把轮子造的飞起,试着用 JAVA 写个正则引擎. 要求: 1.  纯 DFA,无需支持 捕获组 和 断言: 2.  时间复杂度 O(n): 3. 支持 ASCII 字符集,支持基本语法:支持'\w', '\W', '\s', '\S', '\d', '\D' 和 '.'  七个集合及其他单字符转义,支持 ?, *, +, {x}, {x,}, {x,y} 六种量词,支持集合反转,支持括号. 4. 避免使用第三方工具,编译为无依赖库. (正则引擎已完成,Github)

[WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析

[WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析 标签: webkit内核JavaScriptCore 2015-03-26 23:26 2285人阅读 评论(1) 收藏 举报  分类: Webkit(34)  JavascriptCore/JIT(3)  版权声明:本文为博主原创文章,未经博主允许不得转载. 看到HorkeyChen写的文章<[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现>

[WebKit内核] JavaScriptCore深度解析--基础篇(一)字节码生成及语法树的构建

看到HorkeyChen写的文章<[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现>,写的很好,深受启发.想补充一些Horkey没有写到的细节比如字节码是如何生成的等等,为此成文. JSC对JavaScript的处理,其实与Webkit对CSS的处理许多地方是类似的,它这么几个部分: (1)词法分析->出来词语(Token): (2)语法分析->出来抽象语法树(AST:Abstract Syntax Tree): (3)遍历抽象语法

简易正则表达式引擎的实现

正则表达式基本每个程序员都会用到,实现正则表达式引擎却似乎是一个很难的任务.实际上,掌握<编译原理>前端的词法分析部分知识就能够实现一个简单的正则表达式引擎.这里推荐一下网易云课堂的课程.http://mooc.study.163.com/course/USTC-1000002001?tid=1000003000#/info 基本的正则表达式  正则表达式由字符与元字符组成,整个表达式用于描述符合某些特定特征的一类字符串,比如说表达式:abc,它表示 "abc" 这个字符串

基于ε-NFA的正则表达式引擎

正则表达式几乎每个程序员都会用到,对于这么常见的一个语言,有没有想过怎么去实现一个呢?乍想一下,也许觉得困难,实际上实现一个正则表达式的引擎并没有想像中的复杂,<编译原理>一书中有一章专门讲解了怎么基于状态机来构建基本的正则表达式引擎,讲这个初衷是为词法分析服务,不过书里的东西相对偏理论了些,实现起来还是要费些功夫的,只是它到底指明了一条路,当然,书里只针对基本的语法进行了分析讲解,对于在实际中有些非常有用的很多扩展语法,它就基本没有涉及了,这些扩展的语法中有些是比较好实现的,有些则很难. 基

自己实现一个SQL解析引擎

自己实现一个SQL解析引擎 功能:将用户输入的SQL语句序列转换为一个可执行的操作序列,并返回查询的结果集. SQL的解析引擎包括查询编译与查询优化和查询的运行,主要包括3个步骤: 查询分析: 制定逻辑查询计划(优化相关) 制定物理查询计划(优化相关) 查询分析: 将SQL语句表示成某种有用的语法树. 制定逻辑查询计划: 把语法树转换成一个关系代数表达式或者类似的结构,这个结构通常称作逻辑计划. 制定物理查询计划:把逻辑计划转换成物理查询计划,要求指定操作执行的顺序,每一步使用的算法,操作之间的