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


    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文件,记得文件名要和里面定义的语法规则名称保持一致。 

    1. grammar Expr;
    2. prog: expr EOF ;
    3. expr: expr ('*'|'/') expr
    4. | expr ('+'|'-') expr
    5. | INT
    6. | '(' expr ')'
    7. ;
    8. NEWLINE : [\r\n]+ -> skip;
    9. INT : [0-9]+ ;
    10. 复制代码

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

     

    Java API使用

    1. 引入Pom

    1. <dependency>
    2. <groupId>org.antlr</groupId>
    3. <artifactId>antlr4</artifactId>
    4. <version>4.10.1</version>
    5. </dependency>
    6. 复制代码

    2. 插件生成Java代码

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

    其中文件的含义:

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

    3. 使用生成代码

    1. package me.aihe.bizim.expr;
    2. import me.aihe.bizim.expr.ExprParser.ProgContext;
    3. import org.antlr.v4.runtime.CharStreams;
    4. import org.antlr.v4.runtime.CodePointCharStream;
    5. import org.antlr.v4.runtime.CommonTokenStream;
    6. import org.antlr.v4.runtime.tree.ParseTreeWalker;
    7. public class ExprDemo {
    8. public static void main(String[] args) {
    9. // 构建字符流
    10. CodePointCharStream charStream = CharStreams.fromString("1+2+3*4");
    11. // 从字符流分析词法, 解析为token
    12. ExprLexer lexer = new ExprLexer(charStream);
    13. // 从token进行分析
    14. ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
    15. // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
    16. ProgContext prog = parser.prog();
    17. ParseTreeWalker walker = new ParseTreeWalker();
    18. walker.walk( new ExprBaseListener(), prog );
    19. // 使用visitor,生成自定义的对象
    20. Object accept = prog.accept(new ExprBaseVisitor<>());
    21. // 打印生成的语法树
    22. System.out.println( prog.toStringTree(parser));
    23. }
    24. }
    25. 复制代码

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

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

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

    1. grammar Expr;
    2. prog: expr EOF ;
    3. expr: expr ('*'|'/') expr #MultiOrDiv
    4. | expr ('+'|'-') expr #AddOrSub
    5. | INT #Lieteral
    6. | '(' expr ')' #Single
    7. ;
    8. NEWLINE : [\r\n]+ -> skip;
    9. INT : [0-9]+ ;
    10. 复制代码
    1. package me.aihe.bizim.expr;
    2. import java.util.Objects;
    3. import me.aihe.bizim.expr.ExprParser.AddOrSubContext;
    4. import me.aihe.bizim.expr.ExprParser.ExprContext;
    5. import me.aihe.bizim.expr.ExprParser.LieteralContext;
    6. import me.aihe.bizim.expr.ExprParser.MultiOrDivContext;
    7. import me.aihe.bizim.expr.ExprParser.ProgContext;
    8. import me.aihe.bizim.expr.ExprParser.SingleContext;
    9. public class EvalExprVisitor extends ExprBaseVisitor<Integer>{
    10. /**
    11. * 入口处调用
    12. * @param ctx the parse tree
    13. * @return
    14. */
    15. @Override
    16. public Integer visitProg(ProgContext ctx) {
    17. ExprContext expr = ctx.expr();
    18. return visit(expr);
    19. }
    20. /**
    21. * expr: expr ('*'|'/') expr
    22. * | expr ('+'|'-') expr
    23. * | INT
    24. * | '(' expr ')'
    25. * @param ctx the parse tree
    26. * @return
    27. */
    28. /**
    29. * expr ('*'|'/') expr
    30. */
    31. @Override
    32. public Integer visitMultiOrDiv(MultiOrDivContext ctx) {
    33. Integer op1 = visit(ctx.expr(0));
    34. Integer op2 = visit(ctx.expr(1));
    35. String operator = ctx.getChild(1).getText();
    36. if (Objects.equals(operator, "*")){
    37. return op1 * op2;
    38. }
    39. if (Objects.equals(operator, "/")){
    40. return op1 / op2;
    41. }
    42. return 0;
    43. }
    44. /**
    45. * expr ('+'|'-') expr
    46. */
    47. @Override
    48. public Integer visitAddOrSub(AddOrSubContext ctx) {
    49. Integer op1 = visit(ctx.expr(0));
    50. Integer op2 = visit(ctx.expr(1));
    51. String operator = ctx.getChild(1).getText();
    52. if (Objects.equals(operator, "+")){
    53. return op1 + op2;
    54. }
    55. if (Objects.equals(operator, "-")){
    56. return op1 - op2;
    57. }
    58. return 0;
    59. }
    60. /**
    61. *
    62. * @param ctx the parse tree
    63. * @return
    64. */
    65. @Override
    66. public Integer visitSingle(SingleContext ctx) {
    67. return visit(ctx);
    68. }
    69. /**
    70. * INT
    71. * @return
    72. */
    73. @Override
    74. public Integer visitLieteral(LieteralContext ctx) {
    75. return Integer.valueOf(ctx.INT().getText());
    76. }
    77. }
    78. 复制代码

    5. 验证程序

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

    1. import java.util.Arrays;
    2. import java.util.List;
    3. import java.util.Objects;
    4. import me.aihe.bizim.expr.ExprParser.ProgContext;
    5. import org.antlr.v4.runtime.CharStreams;
    6. import org.antlr.v4.runtime.CodePointCharStream;
    7. import org.antlr.v4.runtime.CommonTokenStream;
    8. import org.springframework.util.Assert;
    9. public class ExprDemo {
    10. public static void main(String[] args) {
    11. List<String> testSet = Arrays.asList(
    12. "1+2",
    13. "1+2+3*4",
    14. "3/3",
    15. "10/2",
    16. "5*5+10+5*5"
    17. );
    18. List<Integer> res = Arrays.asList(
    19. 3,15,1,5,60
    20. );
    21. for (int i = 0; i < testSet.size(); i++) {
    22. // 构建字符流
    23. CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));
    24. // 从字符流分析词法, 解析为token
    25. ExprLexer lexer = new ExprLexer(charStream);
    26. // 从token进行分析
    27. ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
    28. // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
    29. ProgContext prog = parser.prog();
    30. // 使用visitor,生成自定义的对象
    31. Integer integer = prog.accept(new EvalExprVisitor());
    32. System.out.println(integer);
    33. Assert.isTrue(Objects.equals(integer, res.get(i)),"");
    34. }
    35. }
    36. }
    37. 复制代码

    6. 生成调用图

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

    1. package me.aihe.bizim.expr;
    2. import java.util.List;
    3. import java.util.Set;
    4. import java.util.Stack;
    5. import me.aihe.bizim.expr.ExprParser.ProgContext;
    6. import org.antlr.v4.runtime.ParserRuleContext;
    7. import org.antlr.v4.runtime.misc.MultiMap;
    8. import org.antlr.v4.runtime.misc.OrderedHashSet;
    9. public class EvalExprListener extends ExprBaseListener {
    10. private Graph graph = new Graph();
    11. Stack<String> current = new Stack<>();
    12. String prefix = "exp_";
    13. @Override
    14. public void enterEveryRule(ParserRuleContext ctx) {
    15. if (ctx instanceof ProgContext){
    16. return;
    17. }
    18. // 生成唯一的节点标识
    19. String n = prefix + System.identityHashCode(ctx);
    20. String node = n + String.format("[label=<%s>]",ctx.getText());
    21. graph.nodes.add(node);
    22. if (!current.empty()){
    23. graph.edge( current.peek(), n );
    24. }
    25. current.push(n);
    26. }
    27. @Override
    28. public void exitEveryRule(ParserRuleContext ctx) {
    29. if (ctx instanceof ProgContext){
    30. return;
    31. }
    32. current.pop();
    33. }
    34. public String toDot(){
    35. return graph.toDot();
    36. }
    37. static class Graph {
    38. Set<String> nodes = new OrderedHashSet<>();
    39. MultiMap<String, String> edges = new MultiMap<>();
    40. public void edge(String source, String target) {
    41. if (source == null || target == null){
    42. return;
    43. }
    44. edges.map(source, target);
    45. }
    46. public String toDot() {
    47. StringBuffer sb = new StringBuffer();
    48. sb.append("digraph G { \n");
    49. sb.append("node[shape=plaintext,style=filled];graph[splines=ortho];\n");
    50. // 声明节点
    51. for (String node : nodes) {
    52. sb.append(node).append(";");
    53. sb.append("\n");
    54. }
    55. sb.append("\n");
    56. for (String source : edges.keySet()) {
    57. List<String> targets = edges.get(source);
    58. for (String target : targets) {
    59. sb.append(" ")
    60. .append(source)
    61. .append(" -> ")
    62. .append(target).append("\n");
    63. }
    64. }
    65. sb.append("}\n");
    66. return sb.toString();
    67. }
    68. }
    69. }
    70. 复制代码

    使用监听器:

    1. CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));
    2. // 从字符流分析词法, 解析为token
    3. ExprLexer lexer = new ExprLexer(charStream);
    4. // 从token进行分析
    5. ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
    6. // 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
    7. ProgContext prog = parser.prog();
    8. // 使用visitor,生成自定义的对象
    9. Integer integer = prog.accept(new EvalExprVisitor());
    10. System.out.println(integer);
    11. Assert.isTrue(Objects.equals(integer, res.get(i)),"");
    12. ParseTreeWalker walker = new ParseTreeWalker();
    13. EvalExprListener listener = new EvalExprListener();
    14. walker.walk(listener, prog );
    15. System.out.println(listener.toDot());
    16. 复制代码

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

    1. digraph G {
    2. node[shape=plaintext,style=filled];graph[splines=ortho];
    3. exp_2085857771[label=<5*5+10+5*5>];
    4. exp_248609774[label=<5*5+10>];
    5. exp_708049632[label=<5*5>];
    6. exp_1887400018[label=<5>];
    7. exp_285377351[label=<5>];
    8. exp_344560770[label=<10>];
    9. exp_559450121[label=<5*5>];
    10. exp_716083600[label=<5>];
    11. exp_791885625[label=<5>];
    12. exp_2085857771 -> exp_248609774
    13. exp_2085857771 -> exp_559450121
    14. exp_248609774 -> exp_708049632
    15. exp_248609774 -> exp_344560770
    16. exp_708049632 -> exp_1887400018
    17. exp_708049632 -> exp_285377351
    18. exp_559450121 -> exp_716083600
    19. exp_559450121 -> exp_791885625
    20. }
    21. 复制代码

    在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引入依赖:

    1. <dependency>
    2. <groupId>net.sourceforge.plantuml</groupId>
    3. <artifactId>plantuml</artifactId>
    4. <version>1.2022.13</version>
    5. </dependency>
    6. 复制代码

    使用Java生成对应的点图:

    1. public class Demo {
    2. public static void main(String[] args) throws IOException {
    3. FileFormat gif = FileFormat.valueOf("SVG");
    4. StringBuffer sb = new StringBuffer();
    5. // 注意 digraph g {的后面不要有空格
    6. sb.append("@startuml\n"
    7. + "digraph g {\n"
    8. + "node[shape=plaintext,style=filled];graph[splines=ortho];\n"
    9. + "exp_2085857771[label=<5*5+10+5*5>];\n"
    10. + "exp_248609774[label=<5*5+10>];\n"
    11. + "exp_708049632[label=<5*5>];\n"
    12. + "exp_1887400018[label=<5>];\n"
    13. + "exp_285377351[label=<5>];\n"
    14. + "exp_344560770[label=<10>];\n"
    15. + "exp_559450121[label=<5*5>];\n"
    16. + "exp_716083600[label=<5>];\n"
    17. + "exp_791885625[label=<5>];\n"
    18. + "\n"
    19. + " exp_2085857771 -> exp_248609774\n"
    20. + " exp_2085857771 -> exp_559450121\n"
    21. + " exp_248609774 -> exp_708049632\n"
    22. + " exp_248609774 -> exp_344560770\n"
    23. + " exp_708049632 -> exp_1887400018\n"
    24. + " exp_708049632 -> exp_285377351\n"
    25. + " exp_559450121 -> exp_716083600\n"
    26. + " exp_559450121 -> exp_791885625\n"
    27. + "}\n"
    28. + "@enduml");
    29. String str = sb.toString();
    30. System.out.println(str);
    31. SourceStringReader dot = new SourceStringReader(str);
    32. FileOutputStream os = new FileOutputStream(new File("result.svg"));
    33. dot.outputImage(os, new FileFormatOption(gif));
    34. os.flush();
    35. }
    36. }
    37. 复制代码

    8. 添加错误监听器

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

    自定义语法

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

    lab.antlr.org/

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

    总结

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

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

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

    参考:

  • 相关阅读:
    冰冰学习笔记:内存地址空间
    入门力扣自学笔记109 C++ (题目编号1161)
    docker下的onlyoffice安装(for seafile)
    Android显示系统-GraphicBuffer和Gralloc分析
    #13Maven打包生成MD5校验文件的两种方式
    ENVI IDL:对于GEOTIFF结构体的说明
    低代码平台那些事儿
    redis和nacos的安装
    Ansible之Ansible Tower使用User和Team管理访问权限的笔记
    java8---1、为什么要用lambda表达式?
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128215337