背景
最近在学习github上的一个mlsql项目的时候,发现了antlr这一强大的语言解析工具。上网搜罗了很多资料,基本都是概念原理之类,示例也比较单一,看了之后难以上手。为了帮助初次接触antlr的童鞋们能够快速运用antlr做出东西来,遂出此文,希望能帮助到迷茫中的朋友。(本人渣渣一枚,没有什么语言解析的基础,仅仅帮助大家使用工具,不谈原理)
概要
本文参照mlsql,定义一种数据加载规则,使用antlr,实现spark加载各种数据源的功能
环境准备
环境:java8+maven+idea
插件:安装idea-antlr4的插件(file-->setting-->plugins-->install plugin from disk)
插件下载
antlr前端
一些概念
- 前端:定义语法规则,antlr通过g4文件来定义
- lexer:词法解规则,就是将一个句子多个字符进行组装分成多个单词的规则
- parser:语法解析,对分词后的整个句子进行解析,可以对每个分词单元做出自定义的处理,从而来实现自己的语法解析功能。
g4文件
g4文件是antlr生成词法解析规则和语法解析规则的基础。该文件是我们自定义的,文件名后缀需要是.g4。g4文件的结构大致为:
- grammar
- comment(同java //)
- options
- import
- tokens
- @actionName
- rule
我们需要关注的主要是grammar与rule
grammar
grammar是规则文件的头,需要与文件名保持一致。当antlr生成词法语法解析的规则代码时,类名就是根据grammar的名字来的。
rule
rule是antlr生成词法语法解析的基础。包括了lexer与parser,每条规则都是key:value的形式,以分号结尾。lexer首字母大写,lexer小写。
g4文件的编写与解释
grammar Dsl;
@header {
package antlr;
}
sta:(sql ender)*;
ender:';';
sql
: SELECT ~(';')* as tableName
| LOAD format '.' path as tableName
;
as: AS;
tableName: identifier;
format: identifier;
path: quotedIdentifier;
identifier: IDENTIFIER | quotedIdentifier;
quotedIdentifier: BACKQUOTED_IDENTIFIER;
AS: [Aa][Ss];
LOAD: [Ll][Oo][Aa][Dd];
SELECT: [Ss][Ee][Ll][Ee][Cc][Tt];
fragment DIGIT:[0-9];
fragment LETTER:[a-zA-Z];
STRING
: '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
| '"' ( ~('"'|'\\') | ('\\' .) )* '"'
;
IDENTIFIER
: (LETTER | DIGIT | '_')+
;
BACKQUOTED_IDENTIFIER
: '`' ( ~'`' | '``' )* '`'
;
SIMPLE_COMMENT: '--' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN);
BRACKETED_EMPTY_COMMENT: '/**/' -> channel(HIDDEN);
BRACKETED_COMMENT : '/*' ~[+] .*? '*/' -> channel(HIDDEN) ;
WS: [ \r\n\t]+ -> channel(HIDDEN);
UNRECOGNIZED: .;
插件配置生成代码
- 创建一个maven项目
- 将Dsl.g4文件放入项目中
- 配置antlr插件的config
- 生成代码
生成代码解释
- DslLexer 词法解析类
- DslParser 语法解析类,在类中有各种Context,每个parser都赌对应了一个xxxContext的内部类,在Context中记录了与其他Context的包含关系,还提供了获取parser中的lexer的方法,以及进出这个rule的回调函数
- DslListener 语法解析监听器。antlr有listener和visitor两种遍历方式,前面配置的时候选择的是listener,因此只生成了listener。 在Listener中提供了进入和退出每一种规则的回调方法。我们可以通过实现Listtener类,按需覆写回调方法,以此来实现我们的业务。
antlr后端
简单使用
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.7.1</version>
</dependency>
public static void main(String[] args) throws IOException {
String sql= "Select 'abc' as a, `hahah` as c From a aS table;";
ANTLRInputStream input = new ANTLRInputStream(sql);
DslLexer lexer = new DslLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
DslParser parser = new DslParser(tokens);
DslParser.StaContext tree = parser.sta();
System.out.println(tree.toStringTree(parser));
}
load语法实现
功能解说
load的语法: load json.'F:\tmp\user' as temp; 通过类似的语法,实现spark加载文件夹的数据,然后将数据注册成一张表。这里的json可以替换为spark支持的文件格式。
实现思路
如load json.'F:\tmp\user' as temp这样一个sql,对应了我们自定义规则的sql规则里面的load分支。 load-->LOAD,json-->format,'F:\tmp\user' -->path, as-->as,temp--> tableName。
我们可以通过覆写Listener的enterSql()方法,来获取到sql规则里面,与之相关联的其他元素,获取到各个元素的内容,通过spark来根据不同的内容加载不同的数据。
实现代码
public class ParseListener extends DslBaseListener {
@Override
public void enterSql(DslParser.SqlContext ctx) {
String keyword = ctx.children.get(0).getText();
if("select".equalsIgnoreCase(keyword)){
execSelect(ctx);
}else if("load".equalsIgnoreCase(keyword)){
execLoad(ctx);
}
}
public void execLoad(DslParser.SqlContext ctx){
List<ParseTree> children = ctx.children;
String format = "";
String path = "";
String tableName = "";
for (ParseTree c :children) {
if(c instanceof DslParser.FormatContext){
format = c.getText();
}else if(c instanceof DslParser.PathContext){
path = c.getText().substring(1,c.getText().length()-1);
}else if(c instanceof DslParser.TableNameContext){
tableName = c.getText();
}
}
System.out.println(format);
System.out.println(path);
System.out.println(tableName);
}
public void execSelect(DslParser.SqlContext ctx){
}
public static void main(String[] args) throws IOException {
String len = "load json.`F:\\tmp\\user` as temp;";
ANTLRInputStream input = new ANTLRInputStream(len);
DslLexer lexer = new DslLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
DslParser parser = new DslParser(tokens);
DslParser.SqlContext tree = parser.sql();
ParseListener listener = new ParseListener();
ParseTreeWalker.DEFAULT.walk(listener,tree);
}
}
ps:由于近期使用,只是大致调试整理了下,仅仅只是为了方便初接触的朋友快速用起来,要深入就要靠自己了,可能有很多错误和见解疏漏的地方,还请大家莫要介意。
正文到此结束