概览
作为一款语言识别工具,
它可以解析(自定义)规则的语句,生成执行树
分有几个阶段
1.词法分析阶段 (lexical analysis)
根据我们定义的词法
解析出我们对应的关键词出来
2.解析阶段
根据我们定义的语法
对解析出来的词进行构建,生成一个语法树
应用场景
1.定制特定领域语言(DSL)
类似hibernate中的HQL,用DSL来定义要执行操作的高层语法,这种语法接近人可理解的语言,由DSL到计算机语言的翻译则通过ANTLR来做,可在ANTLR的结构语言中定义DSL命令具体要执行何种操作。
spark,hive中的解析语法也是用的antlr4
2.文本解析 可利用ANTLR解析JSON,HTML,XML,EDIFACT,或自定义的报文格式。解析出来的信息需要做什么处理也可以在结构文件中定义。
安装
antlr4在使用的时候需要自己定义一个xx.g4文件,然后通过antlr4程序对其进行代码自动生成(SimpleTemplate),当然也可以自定义
代码生成出几个文件
XXXBaseListener.java
XXXLexer
XXXListener(监听模式)
XXXParser
以及一些语法文件
根据需要还可以生成XXXBaseVisitor.java(访问者模式)
这里介绍两种途径进行生成代码
- 依赖idea的插件进行配置生成
- 通过antlr4的jar包(https://www.antlr.org/download/)antlr-4.0-complete.jar
使用idea的插件的方法的话,对应的生成的代码的版本受到idea本身插件兼容的版本的限制,想要自己生成指定版本的代码并运行比较麻烦
而antlr-4.0-complete.jar 可以根据自己需要的版本进行下载然后构建比较灵活
IDEA安装antlr
由于我已经安装好了,安装后重启,就会发现下标栏出现了对应的工具
新建maven项目,引入jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.luke</groupId>
<artifactId>Antlr_pro</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
<phase>none</phase>
</execution>
</executions>
<configuration>
<outputDirectory>src/test/java</outputDirectory>
<listener>true</listener>
<treatWarningsAsErrors>true</treatWarningsAsErrors>
</configuration>
</plugin>
</plugins>
</build>
</project>
新建一个g4文件,并编写自己的规则,这里引用一个四则运算的规则
grammar Demo;
//parser
prog:stat
;
stat:expr|NEWLINE
;
expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
|INT
|ID
;
//lexer
ID:('a'..'z'|'A'..'Z')+;
INT:'0'..'9'+;
NEWLINE:'\r'?'\n';
WS:(' '|'\t'|'\n'|'\r')+{skip();};
编写成功后可以通过安装antlr的插件进行规则预览
选择语法文件,对其中某个语法规则进行测试,可以看到有视图弹出
便可以测试你的规则的效果了
待你认为规则没问题后,你便可以进行配置,然后生成代码了,主要就算指定一些代码的生成路径,包名,解析成那种程序语言,默认java
antlr工具包生成
到上方的网址选择自己想要的antlr版本之后,根据自己的g4文件开始构建
java -jar antlr-4.8-complete.jar
可以看到他提供了很多配置参数可以用
这里我们默认执行
java -jar antlr-4.8-complete.jar Demo.g4
可以看到它在默认的当前目录下生成了文件,这个时候你可以选择在当前目录构建按一个java项目,也可以将这些文件拷贝到现有的java项目之下,然后选择的antlr4-runtime包需要和你的complete的包的版本一致,不然会导致一些编译问题
使用方法
g4文件的构造
规则识别文件在antlr4的github官网里面有很多别人的样例可供学习,你也可以定制一套属于自己的规则
g4构造分有几个部分
-
grammar Name (包括了词法和语法的声明写法,有的为了重用性和解耦,可以将词法和语法进行分开,也就是 lexer grammar Name和parser grammar Name 进行声明)
-
options (通过这个声明一些配置信息,可选)
-
import (如果你不是合并式的写法,词法和语法是分开写的,就可以在语法声明文件中通过import,导入对应的词法文件)
-
actionName (用来定义一些文件头…之类的)
-
rule (主要的语法规则,可以是多个)
这是核心,表示规则,以 “:” 开始, “;” 结束, 多规则以 “|” 分隔。
ID : [a-zA-Z0-9|'_']+ ; //数字 STR:'\'' ('\'\'' | ~('\''))* '\''; WS: [ \t\n\r]+ -> skip ; // 系统级规则 ,即忽略换行与空格 sqlStatement : ddlStatement | dmlStatement | transactionStatement | replicationStatement | preparedStatement | administrationStatement | utilityStatement ;
和前文说的一样,g4文件主要处理了两件事,
一个是解析出你要的词法:
比如我定义了规则里面的INT就只能是数字
INT:'0'..'9'+;
比如我定义了ID只能是英文
ID:('a'..'z'|'A'..'Z')+;
一个是根据你的词法指定的规则
比如我要指定元素之间的加减乘除
prog:stat
;
stat:expr|NEWLINE
;
expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
|INT
|ID
;
同一个语法里面可以通过|进行多个结果的匹配,相当于或
语法树的遍历使用
语法树的遍历使用分有两种,一种是监听者模式,一种是访问者模式
使用监听者模式,主要借助了ParseTreeWalker 这样一个类,相当于是一个hook
每经过一个树的节点,便会触发对应节点的方法.好处就算是比较方便,但是灵活性不够,不能够自主性的调用任意节点进行使用
使用访问者模式,将每个数据的节点类型高度抽象出来够,根据你传入的上下文类型来判断你想要访问的是哪个节点,触发对应的方法
这边通过对刚刚的四则运算语法进行简单的测试
grammar Demo;
//parser
prog:stat
;
stat:expr|NEWLINE
;
expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
|INT
|ID
;
//lexer
ID:('a'..'z'|'A'..'Z')+;
INT:'0'..'9'+;
NEWLINE:'\r'?'\n';
WS:(' '|'\t'|'\n'|'\r')+{skip();};
测试的规则语法树如图:(a+b)+2*3
监听者模式
在生成的代码结构里面,新建一个Main类进行调用测试
public static void runListener() throws Exception{
//对每一个输入的字符串,构造一个 ANTLRStringStream 流 in
ANTLRInputStream in = new ANTLRInputStream("(a+b)+2*3\n");
//用 in 构造词法分析器 lexer,词法分析的作用是产生记号
DemoLexer lexer = new DemoLexer(in);
//用词法分析器 lexer 构造一个记号流 tokens
CommonTokenStream tokens = new CommonTokenStream(lexer);
//再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
DemoParser parser = new DemoParser(tokens);
DemoParser.StatContext stat = parser.stat();
//调用lister
DemoBaseListener demoBaseListener = new DemoBaseListener();
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
parseTreeWalker.walk(demoBaseListener,stat );
System.out.println("end");
}
官网说的监听器的遍历规则是从左往右的深度优先遍历,眼见为虚,实践为实,这边重写一下生成的DemoBaseListener,简单的打印当前节点的信息,观察一下是否符合
// Generated from D:/GitPro/My_pro/Antlr_pro/src/test/antlr\Demo.g4 by ANTLR 4.8
package Demo;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
public class DemoBaseListener implements DemoListener {
@Override public void enterProg(DemoParser.ProgContext ctx) {
System.out.println("enterProg:"+ctx.getText());
}
@Override public void exitProg(DemoParser.ProgContext ctx) {
// System.out.println("exitProg: " +ctx.getText());
}
@Override public void enterStat(DemoParser.StatContext ctx) {
System.out.println("enterStat:"+ctx.getText());
}
@Override public void exitStat(DemoParser.StatContext ctx) {
// System.out.println("exitStat:"+ctx.getText());
}
@Override public void enterExpr(DemoParser.ExprContext ctx) {
System.out.println("enterExpr:"+ctx.getText());
}
@Override public void exitExpr(DemoParser.ExprContext ctx) {
// System.out.println("exitExpr: "+ctx.getText());
}
@Override public void enterMultExpr(DemoParser.MultExprContext ctx) {
System.out.println("enterMultExpr:"+ctx.getText());
}
@Override public void exitMultExpr(DemoParser.MultExprContext ctx) {
// System.out.println("exitMultExpr:"+ctx.getText());
}
@Override public void enterAtom(DemoParser.AtomContext ctx) {
System.out.println("enterAtom:"+ctx.getText());
}
@Override public void exitAtom(DemoParser.AtomContext ctx) {
// System.out.println("exitAtom:"+ctx.getText());
}
@Override public void enterEveryRule(ParserRuleContext ctx) {
// System.out.println("enterEveryRule:"+ctx.getText());
}
@Override public void exitEveryRule(ParserRuleContext ctx) {
// System.out.println("exitEveryRule:"+ctx.getText());
}
@Override public void visitTerminal(TerminalNode node) {
System.out.println("visitTerminal:"+node.getText());
}
@Override public void visitErrorNode(ErrorNode node) {
System.out.println("visitErrorNode:"+node.getText());
}
}
执行调用我们的main方法
对比我们解析的语法树,验证了这个遍历顺序
访问者模式
新写一个访问者的方法
public static void runVistor(){
//新建输入流
ANTLRInputStream in = new ANTLRInputStream("(a+b)+2*3\n");
//新建词法解析器
DemoLexer vistorLexer = new DemoLexer(in);
//对解析token进行缓存
CommonTokenStream vistorToken = new CommonTokenStream(vistorLexer);
//新建解析器进行解析
DemoParser vistorParser = new DemoParser(vistorToken);
//通过访问者的模式进行访问
DemoBaseVisitor<String> DemoBaseVisitor = new DemoBaseVisitor<String>();
//传入需要解析的节点
DemoBaseVisitor.visit(vistorParser.prog());
DemoBaseVisitor.visit(vistorParser.stat());
DemoBaseVisitor.visit(vistorParser.multExpr());
}
public static void main(String[] args) throws Exception{
// runListener();
runVistor();
}
本次我们可以通过传入我们需要访问的节点:prog,stat,multExpr
并重写访问者的对应节点的代码
@Override public T visitProg(DemoParser.ProgContext ctx) {
System.out.println("visitProg:"+ctx.getText());
return visitChildren(ctx);
}
...
@Override public T visitStat(DemoParser.StatContext ctx) {
System.out.println("visitStat:"+ctx.getText());
return visitChildren(ctx); }
...
@Override public T visitMultExpr(DemoParser.MultExprContext ctx) {
System.out.println("visitMultExpr:"+ctx.getText());
return visitChildren(ctx); }
查看结果确实可以根据你的需要访问到对应的节点,如果多个节点的名字相同,则会按照从左到右,深度优先的顺序依次触发
样例
在这个基础上,我们尝试着解析一下sql的语法,为了简单测试,只测试查询语法,这边在git上拉了一份g4文件用来测试
grammar MysqlQuery;
// @header{package com.antlr.mysql.query;}
AS : A S;
SELECT : S E L E C T;
FROM : F R O M;
TABLE : T A B L E;
MAX : M A X;
SUM : S U M;
AVG : A V G;
MIN : M I N;
COUNT : C O U N T;
ALL : A L L;
DISTINCT : D I S T I N C T;
WHERE : W H E R E;
GROUP : G R O U P;
BY : B Y ;
ORDER : O R D E R;
HAVING : H A V I N G;
NOT : N O T;
IS : I S ;
TRUE : T R U E;
FALSE : F A L S E;
UNKNOWN : U N K N O W N;
BETWEEN : B E T W E E N;
AND : A N D;
IN : I N;
NULL : N U L L;
OR : O R ;
ASC : A S C;
DESC : D E S C;
LIMIT : L I M I T ;
OFFSET : O F F S E T;
fragment A : [aA];
fragment B : [bB];
fragment C : [cC];
fragment D : [dD];
fragment E : [eE];
fragment F : [fF];
fragment G : [gG];
fragment H : [hH];
fragment I : [iI];
fragment J : [jJ];
fragment K : [kK];
fragment L : [lL];
fragment M : [mM];
fragment N : [nN];
fragment O : [oO];
fragment P : [pP];
fragment Q : [qQ];
fragment R : [rR];
fragment S : [sS];
fragment T : [tT];
fragment U : [uU];
fragment V : [vV];
fragment W : [wW];
fragment X : [xX];
fragment Y : [yY];
fragment Z : [zZ];
fragment HEX_DIGIT: [0-9A-F];
fragment DEC_DIGIT: [0-9];
fragment LETTER: [a-zA-Z];
ID: ( 'A'..'Z' | 'a'..'z' | '_' | '$') ( 'A'..'Z' | 'a'..'z' | '_' | '$' | '0'..'9' )*;
TEXT_STRING : ( '\'' ( ('\\' '\\') | ('\'' '\'') | ('\\' '\'') | ~('\'') )* '\'' );
ID_LITERAL: '*'|('@'|'_'|LETTER)(LETTER|DEC_DIGIT|'_')*;
REVERSE_QUOTE_ID : '`' ~'`'+ '`';
DECIMAL_LITERAL: DEC_DIGIT+;
tableName : tmpName=ID;
column_name :ID;
function_name : tmpName=ID ;
selectStatement:
SELECT
selectElements
(
FROM tableSources
( whereClause )?
( groupByCaluse )?
( havingCaluse )?
) ?
( orderByClause )?
( limitClause )?
;
selectElements
: (star='*' | selectElement ) (',' selectElement)*
;
tableSources
: tableName (',' tableName)*
;
whereClause
: WHERE logicExpression
;
logicExpression
: logicExpression logicalOperator logicExpression
| fullColumnName comparisonOperator value
| fullColumnName BETWEEN value AND value
| fullColumnName NOT? IN '(' value (',' value)* ')'
| '(' logicExpression ')'
;
groupByCaluse
: GROUP BY groupByItem (',' groupByItem)*
;
havingCaluse
: HAVING logicExpression
;
orderByClause
: ORDER BY orderByExpression (',' orderByExpression)*
;
limitClause
: LIMIT
(
(offset=decimalLiteral ',')? limit=decimalLiteral
| limit=decimalLiteral OFFSET offset=decimalLiteral
)
;
orderByExpression
: fullColumnName order=(ASC | DESC)?
;
groupByItem
: fullColumnName order=(ASC | DESC)?
;
logicalOperator
: AND | '&' '&' | OR | '|' '|'
;
comparisonOperator
: '=' | '>' | '<' | '<' '=' | '>' '='
| '<' '>' | '!' '=' | '<' '=' '>'
;
value
: uid
| textLiteral
| decimalLiteral
;
decimalLiteral
: DECIMAL_LITERAL
;
textLiteral
: TEXT_STRING
;
selectElement
: fullColumnName (AS? uid)? #selectColumnElement
| functionCall (AS? uid)? #selectFunctionElement
;
fullColumnName
: column_name
;
functionCall
: aggregateWindowedFunction #aggregateFunctionCall
;
aggregateWindowedFunction
: (AVG | MAX | MIN | SUM) '(' functionArg ')'
| COUNT '(' (starArg='*' | functionArg?) ')'
| COUNT '(' aggregator=DISTINCT functionArgs ')'
;
functionArg
: column_name
;
functionArgs
: column_name (',' column_name)*
;
uid
: ID
;
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
可以看到,当是简单的查询就涉及到这么多的词法和语法了,所以这个语法规则的编写能拿现成就拿现成的吧
同样的先对规则进行测试,查看能否正常生成语法树
然后在通过插件生成代码,进行测试
public class mysqlMain {
public static void listenerRun(){
//新建流
ANTLRInputStream input = new ANTLRInputStream("SELECT column1,column2,column3 from tableC where column1 = 1");
//新建词法解析
MysqlQueryLexer mysqlQueryLexer = new MysqlQueryLexer(input);
//缓存词法解析token
CommonTokenStream token = new CommonTokenStream(mysqlQueryLexer);
//解析语法
MysqlQueryParser parser = new MysqlQueryParser(token);
//通过监听器的方式
ParseTreeWalker parserWalk = new ParseTreeWalker();
//重写部分监听操作,输出列名和表名
parserWalk.walk(new MysqlQueryBaseListener(),parser.selectStatement() );
}
同样的,重写一部分的监听代码,方便我们查看
执行
成功识别到每个节点的数据
同样的还有大数据架构里面的hive和spark,同样使用了antlr
如果你搭建了spark的源码环境,就可以看到它的g4文件
或者去它的git仓库看一下
https://github.com/apache/spark/tree/master/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser