Antlr4 快速入门 - 编写语法解析器

news2024/11/14 15:01:12

Antlr全称(ANother Tool for Language Recognition),Antlr4是一款强大的语法分析器生成工具,推特,Haddop,Oracle等各大知名公司在用到了Antlr来构建自己的语言处理类项目。

一门语言的正式描述称为语法(grammar),Antlr可以为语言生成一个词法分析器,并且自动建立语法分析树,也能自动生成树的遍历器,然后我们就可以访问树的节点,执行自定义业务逻辑代码。

本文主要是介绍Antlr4的使用,因此不过多的介绍其特性和各种使用方式,以简单的Demo来了解下它的功能。

基本概念

词法分析

词法分析也称为分词,此阶段从左向右扫描源文件,将其字符流分割成一个个的词(token) 。所谓 token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词。

  • 语法中一般有多种字符组合起来的规则,有意义的字符组合一般为(Token)
  • 将文本转换为Token的程序称为词法分析器(Lexier),过程称为词法分析(Lexical analysis) 

语法分析

  • 词法分析完成后,字符流就被转换为 token 流了,接下来根据语言的语法规则来解析这个token 流,被称为语法分析。
  • 语法分析的过程就是不断的将语法规则应用于源程序,将源程序解析成一颗抽象语法树(parser tree),该树记录了语法分析器识别语句结构的过程;

入门Antlr

以上词法分析和语法分析的过程,在实际使用antlr的过程都不需要关心,只需要进行定义语法规则,以及处理最后的语法分析树即可。

环境配置

有两种方式可以快速的跑起来Demo,命令行或者IDE,不过由于命令行当前默认需要JDK 11,可能大部分人不支持这个版本,还有一些额外的配置。

antlr 在 4.10仍然支持JDK 1.8,在4.10.1之后需要JDK11。

我们直接使用Idea自带的插件来完成Demo;

安装antlr4的插件; 

Idea插件使用

新建一个Expr.g4文件,记得文件名要和里面定义的语法规则名称保持一致。 

grammar Expr;
prog:	expr EOF ;
expr:	expr ('*'|'/') expr
    |	expr ('+'|'-') expr
    |	INT
    |	'(' expr ')'
    ;
NEWLINE : [\r\n]+ -> skip;
INT     : [0-9]+ ;
复制代码

在文件目标规则上右键测试,Antlr4帮我们生成了最终的分析树: 

 

Java API使用

1. 引入Pom

    <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4</artifactId>
            <version>4.10.1</version>
        </dependency>
复制代码

2. 插件生成Java代码

把生成的文件拷贝到自己的包下:

其中文件的含义:

  • ExprParser 包含语法分析器的定义,专门用来识别我们的语言。
  • ExprLexer 词法分析器的定义,将输入字符分解为词汇符号;
  • ExprLexer.tokens antlr4会将我们定义的词法符号指定一个数字类型,然后将对应的关系存储在这个文件中,一般来说我们用不到,高阶选手可能会用到。
  • ExprListener antlr4在遍历语法树的时候,遍历器会触发一系列的事件,通知我们的监听器;ExprListener是监听器的接口定义 ExprBaseListener是监听器的空实现。
  • ExprVisitor 如果我们想要自己显示的自定义遍历语法树,可以使用Visitor来遍历树,ExprBaseVistor是默认的空实现。

3. 使用生成代码

package me.aihe.bizim.expr;

import me.aihe.bizim.expr.ExprParser.ProgContext;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

public class ExprDemo {

    public static void main(String[] args) {
        // 构建字符流
        CodePointCharStream charStream = CharStreams.fromString("1+2+3*4");

        // 从字符流分析词法, 解析为token
        ExprLexer lexer = new ExprLexer(charStream);

        // 从token进行分析
        ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );

        // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
        ProgContext prog = parser.prog();
        ParseTreeWalker walker = new ParseTreeWalker();
        walker.walk( new ExprBaseListener(), prog );

        // 使用visitor,生成自定义的对象
        Object accept = prog.accept(new ExprBaseVisitor<>());


        // 打印生成的语法树
        System.out.println( prog.toStringTree(parser));

    }

}


复制代码

4. 编写自定义的业务逻辑

通过antlr4的API,我们可以识别定义语言的语法树,但是如何做自定义的处理呢,基于上述的demo,我们实现自己的visior用于构建一个计数器的实现。

在真正实现之前,我们需要将语法重新区分一下不同的分支,方便逻辑处理:

grammar Expr;
prog:	expr EOF ;
expr:	expr ('*'|'/') expr  #MultiOrDiv
    |	expr ('+'|'-') expr  #AddOrSub
    |	INT     #Lieteral
    |	'(' expr ')'   #Single
    ;
NEWLINE : [\r\n]+ -> skip;
INT     : [0-9]+ ;
复制代码
package me.aihe.bizim.expr;

import java.util.Objects;

import me.aihe.bizim.expr.ExprParser.AddOrSubContext;
import me.aihe.bizim.expr.ExprParser.ExprContext;
import me.aihe.bizim.expr.ExprParser.LieteralContext;
import me.aihe.bizim.expr.ExprParser.MultiOrDivContext;
import me.aihe.bizim.expr.ExprParser.ProgContext;
import me.aihe.bizim.expr.ExprParser.SingleContext;

public class EvalExprVisitor extends ExprBaseVisitor<Integer>{

    /**
     * 入口处调用
     * @param ctx the parse tree
     * @return
     */
    @Override
    public Integer visitProg(ProgContext ctx) {
        ExprContext expr = ctx.expr();
        return visit(expr);
    }

    /**
     * expr:	expr ('*'|'/') expr
     *     |	expr ('+'|'-') expr
     *     |	INT
     *     |	'(' expr ')'
     * @param ctx the parse tree
     * @return
     */

    /**
     * expr ('*'|'/') expr
     */
    @Override
    public Integer visitMultiOrDiv(MultiOrDivContext ctx) {
        Integer op1 = visit(ctx.expr(0));
        Integer op2 = visit(ctx.expr(1));

        String operator = ctx.getChild(1).getText();
        if (Objects.equals(operator, "*")){
            return op1 * op2;
        }

        if (Objects.equals(operator, "/")){
            return op1 / op2;
        }

        return 0;
    }

    /**
     * expr ('+'|'-') expr
     */
    @Override
    public Integer visitAddOrSub(AddOrSubContext ctx) {
        Integer op1 = visit(ctx.expr(0));
        Integer op2 = visit(ctx.expr(1));

        String operator = ctx.getChild(1).getText();
        if (Objects.equals(operator, "+")){
            return op1 + op2;
        }

        if (Objects.equals(operator, "-")){
            return op1 - op2;
        }

        return 0;
    }

    /**
     *
     * @param ctx the parse tree
     * @return
     */
    @Override
    public Integer visitSingle(SingleContext ctx) {
        return visit(ctx);
    }

    /**
     * INT
     * @return
     */
    @Override
    public Integer visitLieteral(LieteralContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
}

复制代码

5. 验证程序

符合预期,这样我们自定义的计数器解析程序就处理好了;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import me.aihe.bizim.expr.ExprParser.ProgContext;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.springframework.util.Assert;

public class ExprDemo {

    public static void main(String[] args) {

        List<String> testSet = Arrays.asList(
            "1+2",
            "1+2+3*4",
            "3/3",
            "10/2",
            "5*5+10+5*5"
        );

        List<Integer> res = Arrays.asList(
            3,15,1,5,60
        );

        for (int i = 0; i < testSet.size(); i++) {
            // 构建字符流
            CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));

            // 从字符流分析词法, 解析为token
            ExprLexer lexer = new ExprLexer(charStream);

            // 从token进行分析
            ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );

            // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
            ProgContext prog = parser.prog();


            // 使用visitor,生成自定义的对象
            Integer integer = prog.accept(new EvalExprVisitor());
            System.out.println(integer);
            Assert.isTrue(Objects.equals(integer, res.get(i)),"");
        }
    }

}

复制代码

6. 生成调用图

借用一个辅助工具dot语言,用来生成各种自定义图形的,基于上述的语法树监听器,我们构建自己的执行程序,然后查看真实的调用过程。

package me.aihe.bizim.expr;

import java.util.List;
import java.util.Set;
import java.util.Stack;

import me.aihe.bizim.expr.ExprParser.ProgContext;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.misc.MultiMap;
import org.antlr.v4.runtime.misc.OrderedHashSet;

public class EvalExprListener extends ExprBaseListener {

    private Graph graph = new Graph();
    Stack<String> current = new Stack<>();

    String prefix = "exp_";



    @Override
    public void enterEveryRule(ParserRuleContext ctx) {
        if (ctx instanceof ProgContext){
            return;
        }

        // 生成唯一的节点标识
        String n = prefix + System.identityHashCode(ctx);
        String node = n + String.format("[label=<%s>]",ctx.getText());
        graph.nodes.add(node);

        if (!current.empty()){
            graph.edge( current.peek(), n );
        }

        current.push(n);
    }

    @Override
    public void exitEveryRule(ParserRuleContext ctx) {
        if (ctx instanceof ProgContext){
            return;
        }
        current.pop();
    }

    public String toDot(){
        return graph.toDot();
    }


    static class Graph {
        Set<String> nodes = new OrderedHashSet<>();
        MultiMap<String, String> edges = new MultiMap<>();


        public void edge(String source, String target) {
            if (source == null || target == null){
                return;
            }
            edges.map(source, target);
        }

        public String toDot() {
            StringBuffer sb = new StringBuffer();
            sb.append("digraph G { \n");
            sb.append("node[shape=plaintext,style=filled];graph[splines=ortho];\n");

            // 声明节点
            for (String node : nodes) {
                sb.append(node).append(";");
                sb.append("\n");
            }

            sb.append("\n");

            for (String source : edges.keySet()) {
                List<String> targets = edges.get(source);
                for (String target : targets) {
                    sb.append(" ")
                        .append(source)
                        .append(" -> ")
                        .append(target).append("\n");
                }
            }

            sb.append("}\n");
            return sb.toString();
        }

    }
}

复制代码

使用监听器:

CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));

            // 从字符流分析词法, 解析为token
            ExprLexer lexer = new ExprLexer(charStream);

            // 从token进行分析
            ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );

            // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
            ProgContext prog = parser.prog();


            // 使用visitor,生成自定义的对象
            Integer integer = prog.accept(new EvalExprVisitor());
            System.out.println(integer);
            Assert.isTrue(Objects.equals(integer, res.get(i)),"");

            ParseTreeWalker walker = new ParseTreeWalker();

            EvalExprListener listener = new EvalExprListener();
            walker.walk(listener, prog );
            System.out.println(listener.toDot());
复制代码

5*5+10+5*5 生成结果:

digraph G { 
node[shape=plaintext,style=filled];graph[splines=ortho];
exp_2085857771[label=<5*5+10+5*5>];
exp_248609774[label=<5*5+10>];
exp_708049632[label=<5*5>];
exp_1887400018[label=<5>];
exp_285377351[label=<5>];
exp_344560770[label=<10>];
exp_559450121[label=<5*5>];
exp_716083600[label=<5>];
exp_791885625[label=<5>];

 exp_2085857771 -> exp_248609774
 exp_2085857771 -> exp_559450121
 exp_248609774 -> exp_708049632
 exp_248609774 -> exp_344560770
 exp_708049632 -> exp_1887400018
 exp_708049632 -> exp_285377351
 exp_559450121 -> exp_716083600
 exp_559450121 -> exp_791885625
}
复制代码

在dot语言解析网站上贴下生成的代码: dreampuf.github.io/GraphvizOnl…

7. Java代码生成Dot图

或者直接用Java代码生成对应的SVG图也是可以的:仍然使用上述监听器。

下载plantuml:plantuml.com/zh/download graphviz配置:plantuml.com/zh/graphviz…

  • 首先在机器上安装graphviz;
  • 验证plantuml支持点图,java -jar plantuml.jar -testdot
  • 集成点图的pom,然后输出图片。

Java引入依赖:

    <dependency>
            <groupId>net.sourceforge.plantuml</groupId>
            <artifactId>plantuml</artifactId>
            <version>1.2022.13</version>
        </dependency>
复制代码

使用Java生成对应的点图:


public class Demo {
    public static void main(String[] args) throws IOException {
        FileFormat gif = FileFormat.valueOf("SVG");
        StringBuffer sb = new StringBuffer();
        // 注意 digraph g {的后面不要有空格
        sb.append("@startuml\n"
            + "digraph g {\n"
            + "node[shape=plaintext,style=filled];graph[splines=ortho];\n"
            + "exp_2085857771[label=<5*5+10+5*5>];\n"
            + "exp_248609774[label=<5*5+10>];\n"
            + "exp_708049632[label=<5*5>];\n"
            + "exp_1887400018[label=<5>];\n"
            + "exp_285377351[label=<5>];\n"
            + "exp_344560770[label=<10>];\n"
            + "exp_559450121[label=<5*5>];\n"
            + "exp_716083600[label=<5>];\n"
            + "exp_791885625[label=<5>];\n"
            + "\n"
            + " exp_2085857771 -> exp_248609774\n"
            + " exp_2085857771 -> exp_559450121\n"
            + " exp_248609774 -> exp_708049632\n"
            + " exp_248609774 -> exp_344560770\n"
            + " exp_708049632 -> exp_1887400018\n"
            + " exp_708049632 -> exp_285377351\n"
            + " exp_559450121 -> exp_716083600\n"
            + " exp_559450121 -> exp_791885625\n"
            + "}\n"
            + "@enduml");

        String str = sb.toString();
        System.out.println(str);
        SourceStringReader dot = new SourceStringReader(str);
        FileOutputStream os = new FileOutputStream(new File("result.svg"));
        dot.outputImage(os, new FileFormatOption(gif));
        os.flush();

    }
}
复制代码

8. 添加错误监听器

// 可以在识别到错误的时候,抛出异常终止进行
parser.addErrorListener( new BaseErrorListener());
复制代码

自定义语法

如果说想要自定义一些语言类应用,可以参考antlr4的lab网站,内置了当前主流的语言语法结构。

lab.antlr.org/

比如URL语法,以及JSON,CSV,DOT、SQL等语言的解析器;

总结

1、Antlr4是一种语法分析器的 生成器,帮我们屏蔽了词法分析和语法分析的过程,直接生成出对应的语法分析树。 基于语法分析树,我们可以构建自己的语言类应用;

2、Idea工具可以直接安装Antlr4的插件,可以帮我们检验自定义的语法特性,并且可以生成对应的Java基础类。

3、通过生成的Java基础类,配合Antlr4的API,可以快速完成一个语言类的应用。

参考:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/68138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机考研报名人数下降一半!211北京科技大学计算机报考人数公布!

北京科技大学是一所211大学&#xff0c;计算机学科评估B&#xff0c;计算机实力在211大学中还算不错。前段时间&#xff0c;北京科技大学公布了23考研的报考人数&#xff0c;而且详细到了各个专业的人数&#xff1a;北京科技大学2023年硕士研究生各招生专业准考人数统计表https…

PyQt5页面跳转问题及解决方式

问题1&#xff1a;如何实现页面间跳转 主要定义MainWindow类和Actions.py class MainWindow&#xff1a; Actions.py&#xff1a; 问题2&#xff1a;实现定义函数传参功能 大胆修改&#xff0c;将定义函数的参数值改为a&#xff1b;使用函数处将参数加上&#xff1a; 运行&…

腾讯云动态公网IP绑定域名实现内网服务器公网穿透

公众号推广: 目前CSDN进行VIP可见,文章可在微信公众号进行免费的阅读。 文章内容经过认证实践,比较的清晰易懂,适合初次接触的人员。 目录 公众号推广: 需求场景: 解决方案: 实现方案:

1.5.4 HDFS 客户端操作-hadoop-最全最完整的保姆级的java大数据学习资料

文章目录1.5.4 HDFS 客户端操作1.5.4.1 Shell 命令行操作HDFS1.5.4.2 JAVA客户端1.5.4.2.1 客户端环境准备1.5.4.2.2 HDFS的API操作1.5.4.2.2.1 上传文件1.5.4.2.2.2 下载文件1.5.4.2.2.3 删除文件/文件夹1.5.4.2.2.4 查看文件名称、权限、长度、块信息1.5.4.2.2.5 文件夹判断1…

Letbook Cookbook题单——数组2

Letbook Cookbook题单——数组2 39. 组合总和 难度中等 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。…

Qgis加载arcgis的gdb格式数据

方式1&#xff1a;文件浏览器打开可直接看到图层&#xff0c;拖到可视区域即可。 方式2&#xff1a;gdb文件夹拖到可视区域即可。 方式3&#xff1a;图层-矢量-目录 该gdb可能没有坐标信息&#xff0c;需要跟甲方询问或者自己尝试

安卓APP源码和设计报告——快递查询录入系统

《多媒体通信技术》 题 目&#xff1a;快递 完 成 日 期 2022年05月 目 录 1 绪论1 2 设计方案2 2.1 设计思路和方案2 2.2 功能要求2 2.3 设计的流程图2 3 设计过程3 3.1 界面布局3 3.2 功能实现3 4 运行结果与分析4 4.1 设计的使用步骤4 4.2 运行结果与分析4 5 …

【数据结构】—带头双向循环链表的实现(完美链表)

目录前言链表的实现新节点的创建链表初始化尾插与尾删头插与头删查找数据在任意位置的插入与删除链表的销毁总结前言 链表结构一共有八种形式&#xff0c;在前面的文章里已经讲完了不带头单向非循环链表的实现&#xff0c;但是我们发现该链表实现尾插与尾删时比较麻烦&#xff…

Java学习多态之向上转型

目录 一、向上转型的本质 举例说明 父类-Animal 子类-Cat类 测试-PolyDetail 语法 Object类 二、向上转型的特点 第一条 第二条 第三条 例子 Base类 TopBase类 测试类 第四条 运行结果 多态的前提&#xff1a;两个对象&#xff08;类&#xff09;存在继承关系 …

【软件测试】测试新人咋迅速成为bug小能手?刮目相看......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 对于很多新入行测试…

Linux学习笔记

IO操作 概述 根据用户态和内核态的划分&#xff0c;用户态的进程是不能直接访问各种硬件资源的&#xff0c;只能向内核发起系统调用&#xff0c;由内核完成一系列操作再切回用户进程。 用户进程每次想要访问硬件资源&#xff08;包括读和写&#xff09;就叫做一次IO。 IO共有…

梦开始的地方——C语言文件操作详解

文章目录C语言文件操作1. 什么是文件&#xff1f;2.文件指针3.文件的打开和关闭4.文件的顺序读写fgetc&fputcfgets&fputsfread&fwritefscanf&fprintfscanf/fscanf/sscanf 对比 printf/fprintf/sprintf5.文件的随机读写(fseek&ftell &rewind)6. 文件结束…

兄弟机床联网

一、设备信息确认 1、确认型号 看面板颜色&#xff1a; 面板如果是彩色屏幕&#xff0c;大概率是可以做联网采集的。如果是黑白屏则需要进一步确认设备名牌。 看名牌&#xff1a; 名牌一般在设备后面&#xff0c;可以看到数控系统的品牌&#xff0c;一般C00和B00都是可以直接…

怎么写综述类论文? - 易智编译EaseEditing

一、确定综述的主题 每篇综述都应该有一个观点&#xff0c;即想要表达的事物。一篇综述不是简单的对相关发现的罗列。综述的真正功能是迈出下一步。已有的研究告诉了我们什么&#xff1f;以及我们下一步要怎么做&#xff1f; 确定综述的主题在撰写过程中是最重要的一步。这会…

快速上手几个Linux命令

Linux操作系统有很多功能&#xff0c;我们有很多方式可以使用这些功能&#xff0c;其中最简单和直接的方式就是命令行&#xff08;Command Line&#xff09; 用户与密码 当我们打开一个新系统的时候&#xff0c;第一件要做的事就是登录。系统默认有一个 Administrator 用户&a…

Vue学习:Vue中的数据代理

<!-- 准备容器 --><div idroot> <h2>学校名称&#xff1a;{{name}}</h2><h2>学校地址&#xff1a;{{adress}}</h2></div><script>const vm new Vue({ el: #root,data: {name:Jhon,adress:street 10},});</script> vm上…

8.javase_数组2

一 . 二维数组 (1)二维数组 元素为一维数组的数组 (2)定义格式&#xff1a; 数据类型[][] 变量名; int[][] arr; 数据类型 变量名[][]; int arr[][]; 数据类型[] 变量名[]; int[] arr[]; 二.二维数组初始化 (1)静态初始化 格式&#xff1a;数据类型[][] 变量名 new 数据类型…

PNG怎么转成PDF格式?这两种方法一定要尝试一下

图片文件是我们经常使用到的一种文件类型&#xff0c;但是我们通常会有很多的图片需要同时进行发送&#xff0c;这时候发送给别人就不是很便利了&#xff0c;我们一般会需要通过微信进行发送&#xff0c;但是大家都知道&#xff0c;微信一次只能发送九张图片&#xff0c;有时候…

【CDC跨时钟域信号处理】快时钟域到慢时钟域-单bit

快时钟域到慢时钟域分两种情况&#xff1a; 1、允许采样丢失&#xff1a;直接采用同步器即可。 2、不允许采样丢失&#xff1a;原理是保证快时钟域的信号宽度满足一定的条件&#xff0c;使得慢时钟域有足够的时间采样到。 对于情况2有两种方法解决&#xff1a;①信号展宽边沿检…