概念
ANTLR(ANother Tool for Language Recognition)是一个强大的解析器生成工具,用于读取、处理、执行或翻译结构化文本或二进制文件。ANTLR通过定义文法(grammar)来识别、构建和访问语言中的元素。
ANTLR为包括Java、C++、C#在内的多种语言提供了一个通过语法描述来自动构造自定义语言的识别器(recognizer)、编译器(parser)和解释器(translator)的框架。ANTLR使用Adaptive LL(*) 语法分析技术进行语法分析,支持词法分析、语法分析、语义分析以及代码生成等功能。
Antlr 提供了大量的官方Grammar示例,包含了各种场景语言,如SQL、Javascript等。
核心功能
1)自动生成语法分析器:ANTLR可以根据用户定义的语法规则自动生成相应的词法分析器和语法分析器,无需手动编写复杂的语法分析代码;
词法分析器(Lexer):负责将输入文本分割成一个个的标记(Token)。词法分析器不关心所生成的单个Token的语法意义及其与上下文之间的关系;
语法分析器(Parser):使用词法分析器生成的标记来构建语法树。语法分析器关注Token之间的语法关系和上下文信息;
2)语法树构造:ANTLR能够将输入文本转换为语法树(AST,Abstract Syntax Tree),使文本的结构更加清晰易懂;
3)语法错误提示:ANTLR在解析过程中能够发现语法错误,并提供详细的错误提示,帮助用户快速定位和修复问题;
4)自定义语言支持:ANTLR支持自定义语言的识别、解析和翻译,为开发者提供了极大的灵活性;
工作流程
1)定义语法规则:使用ANTLR的语法规范描述语言(通常为.g4文件)定义目标语言的语法规则;
2)生成解析器:使用ANTLR工具,根据语法规则文件,自动生成词法分析器和语法分析器的源码;
3)编译解析器:将自动生成的词法分析器和语法分析器的源代码拷贝到应用项目中,编译成可执行文件或库;
3)使用解析器:在应用程序中调用自动生成的解析器,对输入的文本进行解析,并根据生成的语法树进行其他处理;
ANTLR准备
本篇以IDEA中使用 Antlr 4.x 为例。
4.1 Antlr插件安装
在使用之前,要先安装Antlr插件。安装步骤如下:
打开 File - Settings - Plugins 菜单中,如图:
选择插件市场,搜索antlr。
如果搜索不了,可以修改代理,如图:
在弹出的界面中,选择自动代理,并在url中输入:
https://plugins.jetbrains.com/
如果以上配置还是搜索不了,可以直接在浏览器中访问JetBrains Marketplace,在界面中搜索antlr,并选择安装。如图:
4.2 插件使用
Antlr 插件安装之后,idea 开发工具中才能支持 .g4 文件的创建。
4.2.1 Generate ANTLR Recognizer
在 g4 文件中右击,选择 “Generate ANTLR Recognizer”,在项目的根目录,自动创建一个 gen 目录,自动生成词法分析器和语法分析器的源码。
如 Hello.g4 文件,自动的文件如下:
4.2.2 ANTLR Preview
在 Idea 中打开 “ANTLR Preview”,如下:
选择任意 g4 文件,在左侧输入框中输入满足条件的信息,右侧可以预览生成的解析树。
Hello 示例
5.1 示例
Antlr 支持正则表达式集合表示法。以下示例为使用 hello 开头的短语。
1)创建 Hello.g4 文件,文件内容如下:
grammar Hello; // 定义名字
@header {package com.jingai.antlr; } // java 的package
s : 'hello' ID | EOF ; // 匹配关键字hello和标志符
ID: [a-z]+ ; // 标识符由小写字母组成
WS: [ \t\r\n]+ -> skip ; // 跳过空格、制表符、回车符和换行符
a)文件以 grammar 开头,名称跟着文件名 Hello;
b)@header 可以用于指定自动生成的词法分析器等 java 文件的报名;
c)在本例中,s 为解析树的root节点,如上面 4.2.2 所示;
d)ID 节点匹配任意长度的小写字母;
+:匹配一次或多次;*:匹配零次或多次;?:匹配零次或一次;
e)-> skip 用于指定跳过的信息;
f)EOF 结束标识;
2)使用 ANTLR Preview,可以预留生成的解析树,如 4.2.2 所示;
3)使用 Generate ANTLR Recognizer,自动生成词法分析器和语法分析器的源码,如 4.2.1 所示;
4)在项目中创建一个包,名称为 g4 文件中通过 @header 指定。将自动生成的 java 文件拷贝到包中;
5)自定义Visitor,代码如下:
package com.jingai.anltr.hello;
import org.antlr.v4.runtime.tree.ParseTree;
import java.util.List;
public class EvalVisitor extends HelloBaseVisitor<String> {
@Override
public String visitS(HelloParser.SContext ctx) {
List<ParseTree> children = ctx.children;
StringBuffer sb = new StringBuffer();
for (ParseTree t : children) {
System.out.println("visit child : " + t.getText());
sb.append(t.getText()).append(" ");
}
return sb.toString();
}
}
在 Antlr 中,可以通过实现监听器(Listener)或访问者(Visitor)接口来遍历语法树。在自动生成的代码中,除了词法分析器和语法分析器以外,还生成了对应的 Listener 和 Visitor 接口以及基础的默认实现类。
5)编写测试用例,代码如下:
package com.jingai.anltr.hello;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
public class HelloTest {
public static void main(String[] args) {
// 创建词法分析器 hello world 字符串
HelloLexer lexer = new HelloLexer(CharStreams.fromString("hello world"));
// 获取token
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 解析token
HelloParser parser = new HelloParser(tokens);
// 获取根root
HelloParser.SContext tree = parser.s();
EvalVisitor visitor = new EvalVisitor();
System.out.println(visitor.visit(tree));
}
}
执行以上的测试用例,打印的信息为:
5.2 解析
在上面的自定义Visitor中,在for循环中添加断点,执行信息如下:
1)visitS() 方法访问根节点 s,该根节点存在两个子节点,分别为 hello 和 world;
2)子节点的类型为TerminalNodeImpl,包含 symbol 和 parent 信息。其中symbol为当前节点的信息、parent为父节点信息;
a)symbol 为 CommonToken 类型,打印的信息为:[@1,6:10='world',<2>,1:6] ;
b)CommonToken 的 toString() 源码中,返回的代码如下:
return "[@" + this.getTokenIndex() + "," + // 索引,从0开始 this.start + ":" + this.stop + // 开始和结束位置 "='" + txt + "', // 对应文本 <" + typeString + ">" + // 类型 channelStr + "," + // 频道信息,在 g4 文件中可以通过 channel() 指定 this.line + ":" + // 所在的行,从1开始 this.getCharPositionInLine() + "]"; // 所在行的开始位置,从0开始
c)结合以上分析,[@1,6:10='world',<2>,1:6] 表示如下:
@1表示为第1个,即第2个;
6:10表示文本在第6个字符到第10个字符之间;
<2>表示类型为2。可以在自动生成的代码中查看;如:
1:6表示在第1行,从第6个字符开始;
算式计算示例
以下通过计算表达式的解析,计算表达式的值为例。
1)创建Calcultor.g4文件,代码如下:
grammar Calculator;
@header {package com.jingai.antlr.calculator; } // java 的package
expr
: INT # int
| expr op=('*'|'/') expr # mulDiv
| expr op=('+'|'-') expr # addSub
| '(' expr ')' # brackets
| EOF # e
;
INT : [0-9]+ ;
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
WS: [ \t\r\n]+ -> skip ; // 跳过空格、制表符、回车符和换行符
a)| 表示或的关系,即 expr 是由 INT 或者乘除表达式或者加减表达式或者括号表达式组成;
b)在每一项后面使用 # 加字母,在自动生成的Visitor或Listener中,会自动生成对应的visit或enter方法。如 # mulDiv,在CalcultorVisiter中,自动生成 visitMulDiv()方法;
二义性处理
所谓的二义性是指解析的信息同时匹配多种语法结构。如上面算式表达式,如果要解析的是
5 + 2 * 3,那么 5 + 2 匹配了加法表达式,2 * 3 匹配了乘法表达式,此时就存在二义性。
在 Antlr 中,通过使输入字符串和语法中第一个指定的规则匹配来解决词法二义性。
如上面的 5 + 2 * 3,因为在语法定义中,乘除是放在加减前面,所以会匹配先匹配乘法。解析树为:
如果在语法中修改乘除和加减的位置,同一个表达式,解析树为:
2)通过 Generate ANTLR Recognizer 自动生成源码,并拷贝到项目;
3)编写Visiter类,进行算式运算,代码如下:
package com.jingai.anltr.calculator;
public class MyCalculatorVisitor extends CalculatorBaseVisitor<Integer> {
/**
* 加减运算
*/
@Override
public Integer visitAddSub(CalculatorParser.AddSubContext ctx) {
// 获取加减运算左右的数字
Integer left = visit(ctx.expr(0));
Integer right = visit(ctx.expr(1));
// 根据计算类型,执行相应计算,并返回计算结果
if(ctx.op.getType() == CalculatorParser.ADD) {
return left + right;
}
return left - right;
}
/**
* 乘除运算
*/
@Override
public Integer visitMulDiv(CalculatorParser.MulDivContext ctx) {
// 获取乘除运算左右的数字
Integer left = visit(ctx.expr(0));
Integer right = visit(ctx.expr(1));
if(ctx.op.getType() == CalculatorParser.MUL) {
return left * right;
}
return left / right;
}
@Override
public Integer visitInt(CalculatorParser.IntContext ctx) {
return Integer.parseInt(ctx.INT().getText());
}
@Override
public Integer visitBrackets(CalculatorParser.BracketsContext ctx) {
return visit(ctx.expr());
}
}
4)编写测试用例,代码如下:
package com.jingai.anltr.calculator;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
public class CalculatorTest {
public static void main(String[] args) {
CalculatorLexer lexer = new CalculatorLexer(CharStreams.fromString("12-(3*3)+8-4/2"));
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalculatorParser parser = new CalculatorParser(tokens);
CalculatorParser.ExprContext tree = parser.expr();
MyCalculatorVisitor visitor = new MyCalculatorVisitor();
System.out.println(visitor.visit(tree));
}
}
执行结果如下:
其他语法
1)通过@parser::members{},添加 java 代码,该代码会自动添加到Parser解析器中;
2)使用locals,定义局部变量,添加 java 代码,对应代码会自动添加到Parser解析器对应的标签解析中;
3)默认情况下,Antlr 从左到右结合运算符进行解析,对于特殊情况,需要从右到左的,可以使用assoc手动指定,示例如下:
expr
: expr '^'<assoc=right> expr // 运算符是右结合的
| INT
;
4)支持语义谓词添加,示例如下:
// 示例一
group : INT sequence[$INT.int]; // INT为int类型
sequence[int n] locals [int i = 1;] // 在自动生成的Parser类的sequence()方法中,会添加 int n 的参数
: ({$i <= $n}? INT {$i++;})* ;
INT : [0-9]+ ;
// 示例二
predicates
: expression predicate[$expression.ctx]? // expression的ctx为ParserRuleContext类型
;
predicate[ParserRuleContext value] : // 在自动生成的Parser类的predicate()方法中,会添加ParserRuleContext value的参数
expression
: STR
;
小结
本篇分析到这里,以下做一个小结:
1)Antlr 用于按照编写的语法规则,解析文本字符串。语法支持正则表达式规则;
先将文本按词进行分解,而后通过语法进行匹配分析。
2)通过 ANTLR Preview,可以预留解析树;
3)通过 Generate ANTLR Recognizer,可以自动生成词法分析器和语法分析器;
在语法中,可以按照特定格式添加对应语言的代码,代码将自动添加在Parser语法分析器中。(也可在自动生成后的Parser语法分析器代码中进行修改,该种方式不太规范)
4)通过继承自动生成的Visiter或Listener,可以对解析后的信息进行提取即相应处理;
关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。
参考: