antlr4操作入門(java版本)

背景

最近在學習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)試整理了下,僅僅只是為了方便初接觸的朋友快速用起來,要深入就要靠自己了,可能有很多錯誤和見解疏漏的地方,還請大家莫要介意。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容