背景
最近在學習github上的一個mlsql項目的時候穆端,發(fā)現(xiàn)了antlr這一強大的語言解析工具袱贮。上網(wǎng)搜羅了很多資料,基本都是概念原理之類体啰,示例也比較單一攒巍,看了之后難以上手。為了幫助初次接觸antlr的童鞋們能夠快速運用antlr做出東西來狡赐,遂出此文窑业,希望能幫助到迷茫中的朋友。(本人渣渣一枚枕屉,沒有什么語言解析的基礎(chǔ)常柄,僅僅幫助大家使用工具,不談原理)
概要
本文參照mlsql搀擂,定義一種數(shù)據(jù)加載規(guī)則西潘,使用antlr,實現(xiàn)spark加載各種數(shù)據(jù)源的功能
環(huán)境準備
環(huán)境:java8+maven+idea
插件:安裝idea-antlr4的插件(file-->setting-->plugins-->install plugin from disk) 插件下載
antlr前端
一些概念
- 前端:定義語法規(guī)則哨颂,antlr通過g4文件來定義
- lexer:詞法解規(guī)則喷市,就是將一個句子多個字符進行組裝分成多個單詞的規(guī)則
- parser:語法解析,對分詞后的整個句子進行解析威恼,可以對每個分詞單元做出自定義的處理品姓,從而來實現(xiàn)自己的語法解析功能。
g4文件
g4文件是antlr生成詞法解析規(guī)則和語法解析規(guī)則的基礎(chǔ)箫措。該文件是我們自定義的腹备,文件名后綴需要是.g4。g4文件的結(jié)構(gòu)大致為:
- grammar
- comment(同java //)
- options
- import
- tokens
- @actionName
- rule
我們需要關(guān)注的主要是grammar與rule
grammar
grammar是規(guī)則文件的頭斤蔓,需要與文件名保持一致植酥。當antlr生成詞法語法解析的規(guī)則代碼時,類名就是根據(jù)grammar的名字來的弦牡。
rule
rule是antlr生成詞法語法解析的基礎(chǔ)友驮。包括了lexer與parser,每條規(guī)則都是key:value的形式驾锰,以分號結(jié)尾卸留。lexer首字母大寫,lexer小寫椭豫。
g4文件的編寫與解釋
grammar Dsl; //定義規(guī)則文件grammar
@header { //一種action,定義生成的詞法語法解析文件的頭耻瑟,當使用java的時候买喧,生成的類需要包名,可以在這里統(tǒng)一定義
package antlr;
}
//parsers
sta:(sql ender)*; //定義sta規(guī)則匆赃,里面包含了*(0個以上)個 sql ender組合規(guī)則
ender:';'; //定義ender規(guī)則,是一個分號
sql //定義sql規(guī)則今缚,sql規(guī)則有兩條分支:select/load
: SELECT ~(';')* as tableName //select語法規(guī)則算柳,以lexer SELECT開頭, 以as tableName 結(jié)尾姓言,其中as 和tableName分別是兩個parser
| LOAD format '.' path as tableName //load語法規(guī)則,大致就是 load json.'path' as table1瞬项,load語法里面含有format,path何荚, as囱淋,tableName四種規(guī)則
; //sql規(guī)則結(jié)束符
as: AS; //定義as規(guī)則,其內(nèi)容指向AS這個lexer
tableName: identifier; //tableName 規(guī)則餐塘,指向identifier規(guī)則
format: identifier; //format規(guī)則妥衣,也指向identifier規(guī)則
path: quotedIdentifier; //path,指向quotedIdentifier
identifier: IDENTIFIER | quotedIdentifier; //identifier,指向lexer IDENTIFIER 或者parser quotedIdentifier
quotedIdentifier: BACKQUOTED_IDENTIFIER; //quotedIdentifier,指向lexer BACKQUOTED_IDENTIFIER
//lexers antlr將某個句子進行分詞的時候戒傻,分詞單元就是如下的lexer
//keywords 定義一些關(guān)鍵字的lexer税手,忽略大小寫
AS: [Aa][Ss];
LOAD: [Ll][Oo][Aa][Dd];
SELECT: [Ss][Ee][Ll][Ee][Cc][Tt];
//base 定義一些基礎(chǔ)的lexer,
fragment DIGIT:[0-9]; //匹配數(shù)字
fragment LETTER:[a-zA-Z]; //匹配字母
STRING //匹配帶引號的文本
: '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
| '"' ( ~('"'|'\\') | ('\\' .) )* '"'
;
IDENTIFIER //匹配只含有數(shù)字字母和下劃線的文本
: (LETTER | DIGIT | '_')+
;
BACKQUOTED_IDENTIFIER //匹配被``包裹的文本
: '`' ( ~'`' | '``' )* '`'
;
//--hiden 定義需要隱藏的文本,指向channel(HIDDEN)就會隱藏需纳。這里的channel可以自定義芦倒,到時在后臺獲取不同的channel的數(shù)據(jù)進行不同的處理
SIMPLE_COMMENT: '--' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN); //忽略行注釋
BRACKETED_EMPTY_COMMENT: '/**/' -> channel(HIDDEN); //忽略多行注釋
BRACKETED_COMMENT : '/*' ~[+] .*? '*/' -> channel(HIDDEN) ; //忽略多行注釋
WS: [ \r\n\t]+ -> channel(HIDDEN); //忽略空白符
// 匹配其他的不能使用上面的lexer進行分詞的文本
UNRECOGNIZED: .;
插件配置生成代碼
- 創(chuàng)建一個maven項目
- 將Dsl.g4文件放入項目中
-
配置antlr插件的config
configure
configure -
生成代碼
generate
generate
生成代碼解釋
- DslLexer 詞法解析類
- DslParser 語法解析類,在類中有各種Context,每個parser都賭對應(yīng)了一個xxxContext的內(nèi)部類不翩,在Context中記錄了與其他Context的包含關(guān)系兵扬,還提供了獲取parser中的lexer的方法,以及進出這個rule的回調(diào)函數(shù)
- DslListener 語法解析監(jiān)聽器口蝠。antlr有l(wèi)istener和visitor兩種遍歷方式器钟,前面配置的時候選擇的是listener,因此只生成了listener亚皂。 在Listener中提供了進入和退出每一種規(guī)則的回調(diào)方法俱箱。我們可以通過實現(xiàn)Listtener類,按需覆寫回調(diào)方法灭必,以此來實現(xiàn)我們的業(yè)務(wù)狞谱。
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); //將輸入轉(zhuǎn)成antlr的input流
DslLexer lexer = new DslLexer(input); //詞法分析
CommonTokenStream tokens = new CommonTokenStream(lexer); //轉(zhuǎn)成token流
DslParser parser = new DslParser(tokens); // 語法分析
DslParser.StaContext tree = parser.sta(); //獲取某一個規(guī)則樹,這里獲取的是最外層的規(guī)則禁漓,也可以通過sql()獲取sql規(guī)則樹......
System.out.println(tree.toStringTree(parser)); //打印規(guī)則數(shù)
}
load語法實現(xiàn)
功能解說
load的語法: load json.'F:\tmp\user' as temp; 通過類似的語法跟衅,實現(xiàn)spark加載文件夾的數(shù)據(jù),然后將數(shù)據(jù)注冊成一張表播歼。這里的json可以替換為spark支持的文件格式伶跷。
實現(xiàn)思路
如load json.'F:\tmp\user' as temp這樣一個sql,對應(yīng)了我們自定義規(guī)則的sql規(guī)則里面的load分支掰读。 load-->LOAD,json-->format叭莫,'F:\tmp\user' -->path蹈集, as-->as,temp--> tableName雇初。
我們可以通過覆寫Listener的enterSql()方法拢肆,來獲取到sql規(guī)則里面,與之相關(guān)聯(lián)的其他元素靖诗,獲取到各個元素的內(nèi)容郭怪,通過spark來根據(jù)不同的內(nèi)容加載不同的數(shù)據(jù)。
實現(xiàn)代碼
public class ParseListener extends DslBaseListener {
@Override
public void enterSql(DslParser.SqlContext ctx) {
String keyword = ctx.children.get(0).getText(); //獲取sql規(guī)則的第一個元素刊橘,為select或者load
if("select".equalsIgnoreCase(keyword)){
execSelect(ctx); //第一個元素為selece的時候執(zhí)行select
}else if("load".equalsIgnoreCase(keyword)){
execLoad(ctx); //第一個元素為load的時候執(zhí)行l(wèi)oad
}
}
public void execLoad(DslParser.SqlContext ctx){
List<ParseTree> children = ctx.children; //獲取該規(guī)則樹的所有子節(jié)點
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);
// spark load實現(xiàn)鄙才,省略
}
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); //規(guī)則樹遍歷
}
}
ps:由于近期使用,只是大致調(diào)試整理了下促绵,僅僅只是為了方便初接觸的朋友快速用起來攒庵,要深入就要靠自己了,可能有很多錯誤和見解疏漏的地方绞愚,還請大家莫要介意叙甸。