Antlr4在数据加工语句上的使用

前言

现有一个需求需要实现SLS那样的加工DSL语句,前面有一篇文章介绍了JavaCC,同样还存在一个类似的工具叫ANTLR(ANother Tool for Language Recognition)也是一个非常强大的词法和语法解析器代码生成器,当前大版本为ANTLR4。整体思路基本上和JavaCC一致,相比于JavaCC而言ANTLR4提供了较简单强大的语法文件调试功能,AST树的遍历方法,还有多语言支持(java,golang,c++,python等)。ANTLR4也被各种强大的中间件使用,比如GroovySparkSQLPrestoHIVEdebezium等。

整体逻辑

image.png

一些细节
1,用户编写xx.g4语法文件。
2,使用Antlr4生成词法解析器和语法解析器。
3,用户输入待解析的文本,词法解析器将待解析的文本内容转换成Tokens流,并过滤一些没用的字符串。
4,词法解析器将Tokens转换成AST树。
5,Antlr4提供了两种遍历AST树的方法,Listener和Visitor。

基础知识

ANTLR4 xx.g4语法文件

antlr4 grammars-v4中提供了大量g4的例子,基本上有需求照着修改即可。一般如果不是很复杂的规则文法一个g4文件即可,比如lucene的LuceneLexer.g4,如果是比较复杂的规则文法则一般放两个g4文件,比如MYSQL的规则文法可描述为MySqlLexer.g4MySqlParser.g4。模板如下。

/** Optional javadoc style comment */
/** 详情可以查看官方文档https://github.com/antlr/antlr4/blob/master/doc/grammars.md */
grammar Name;
options {...}
import ... ;
     
tokens {...}
channels {...} // lexer only
@actionName {...}
      
rule1 // parser and lexer rules, possibly intermingled
...
ruleN

g4文件一些细节
0,g4文件中的关键字import, fragment, lexer, parser, grammar, returns,locals, throws, catch, finally, mode, options, tokens。
1,grammar 名称必须和文件名要一致。
2,语法规则(Parser)以小写字母开始,词法文件(Lexer)以大写字母开始,一般全部大写,所有的Lexer规则无论写在哪里都会被重排到Parser规则之后。
3,同理JavaCC的匹配冲突,先出现的规则优先匹配。
4,g4代码中注释使用和java一致,比如/** block comment /和// line comment。
5,anltr4默认使用<assoc=left>左结合,可以手动指定<assoc=right>右结合,anltr4默认对一些常用操作符做了特殊处理比如加减乘除等,这些就不需要再特殊处理。
6,fragment关键字可以给 Lexer 规则中的公共部分命名。
7,词法和语法规则均以分号 ';' 终结。
8,产生式后面 # label 可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式。
9,用 'string' 单引号引出字符串,| 用于分隔两个产生式,(a|b) 括号用于指定子产生式,?+
用法同正则表达式。
10,符号#表示替代标签,可以作为变量使用,注意和产生式后的#符号区分。
11,动作Action,@header设置生成的代码的package信息,@members可以定义额外的一些变量到Antlr4语法文件中。
12,options主要是是设置语法生成的一些规则,比如设置生成的目标语言,编码等。

ANTLR4语法模式

ANTLR4主要有4中语法模式来定义语法规则。序列模式(普通序列模式,带终止符序列模式),选择模式,词法符号依赖模式,嵌套结构模式。

# 序列模式
一系列元素,它是一个任意长的,可能为空的序列,其中的元素可以是词法符号或者子规则。序列模式的例子包括变量声明和整数序列等等。
比如:
'[' NUMBER+ ']'

## 带终止符序列模式
比如:
(script ';')*  //语句集合
(text '\n')* //多行数据

## 带分隔符的序列模式
比如:
params: expression ( ',' expression )*;

# 选择模式
使用|来分隔同一个语言规则的若干备选分支。
比如:
expr:NUMBER | TEXT;

# 词法符号依赖模式
一个词法符号需要和某处的另外一个词法符号配对。这样的例子包括配对的圆括号(),花括号{},方括号[]和尖括号<>。
比如:
tarray: '[' NUMBER+ ']' ; //[11,22,33]

# 嵌套结构
自相似的语言结构,表达式。一般用作最顶层的词法分析器的入口。类似java的内部类,嵌套的代码块。
比如:
expression: ID '(' expression ')' #funcDef
    |  '(' expression ')'        #funcExp
    |  function                  #funcBase
    |  bool                      #funcBool
    |  TEXT                      #text
    |  NUMBER                    #number
    ;

ANTLR4生成文件

<Grammar>Lexer.java: Lexer
<Grammar>Parser.java: Parser
<Grammar>Listener.java: Listener 接口
<Grammar>BaseListener.java: Listener 默认实现
<Grammar>Visitor.java: Visitor 接口
<Grammar>BaseVisitor.java: Visitor 默认实现
<Grammar>[Lexer].tokens: 当语法被拆分成多个多个文件时用于同步编号

ANTLR4 Listener和Visitor两种树遍历方式对比

1,Listener由ParseTreeWalker对象自动调用遍历所有节点。Visitor为访问者模式,树的遍历由自己自动手动控制,子节点需要主动调用visit()访问,否则某写子节点则不会被调用。
2,Listener模式不能返回值,因此一般使用队列或者栈保存中间结果,而Visitor模式可以返回任何自定义类型。
3,如果要实现树上的解释器,则使用Visitor是最好的,比如函数调用 print(concat("Hello ", "World"))" ,这边只需要只执行concat函数,如果使用Visitor则非常方便,而Listener的ParseTreeWalker则会一直按顺序遍历,就不方便处理。
4,Listener在访问所有节点的时候,会依次触发进入时(enterXXX方法)和退出时(exitXXX方法),且都没有返回值。

ANTLR4 Idea插件安装

在JavaCC的时候想判断xx.jj文件是否能正确解析我的语句非常麻烦,但是Antlr4则提供了一个ANTLR Preview工具非常好用。官方安装文档
1,依次打开IDEA Settings -> Plugins。
2,输入antlr搜索。

image.png

3,安装,重启IDEA。
4,检查,简单写一个g4文件,出现如下表示安装成功。
image.png

写个DEMO

现有个数据加工语句的DSL需求,类似与SLS加工函数语法,这边实现一个Demo,使用Visitor遍历方式简单实现解析函数表达式" str_join('##',str_lowercase('aBc5'),'d',1,true) "。如果是入门例子可以查看官网的calculator.g4例子。

编写FuncBase.g4文件

//--文法名字必须和文件名相同
grammar FuncBase;

//--设置包名信息
@header { package com.xxx.demo2.antlr4.a5; }

//--语法分析器起点,表示可以输入methodExec,TEXT,NUMBER,BOOL匹配规则
expression:    methodExec
    |  TEXT
    |  NUMBER
    |  BOOL
    ;

//--函数定义语法
methodExec
    : methodName '(' methodExecArguments? ')'
    ;

//--函数名语法
methodName
    : ID
    ;

//--函数参数,正则语法表示使用逗号分割,可以输入多个参数
methodExecArguments
 : expression ( ',' expression )*
 ;

//--bool数据类型词法定义
BOOL
 : TRUE
 | FALSE
 ;

//--函数名词法定义
ID           : [a-zA-Z_] [a-zA-Z0-9_]*;

//--true词法定义
TRUE         : 'true';

//--false词法定义
FALSE        : 'false';

//--数值词法定义
NUMBER       : '-'?( [0-9]* '.' )? [0-9]+;

//--字符串文本定义
TEXT         : ('"'|'\'') ~[\r\n']* ('"'|'\'');

//--忽略字符
WS           : [\t\r\n]+ -> skip;

调试xx.g4语法文件

输入函数语法" str_join('##',str_lowercase('aBc5'),'d',1,true) ",可以看到左侧的函数表达式翻译成了右侧的AST语法树。


image.png

如果输入的语法有错,则左边的AST树也会提示


image.png

生成分析器代码

生成代码有多种方式,因为我这边IDEA已经装了Antlr插件,直接生成即可,其他方式可以查看官网文档(Runtime Libraries and Code Generation Targets),还包括其他语言的生成方式。

image.png

Visitor遍历树

使用Visitor遍历树,并执行函数返回相应的结果,这里只是一个demo,如果是正式业务开发,一般为了避免频繁修复g4文件,不会直接将函数定义到g4中,而是定义一个抽象的函数模型,然后再代码中做相关的业务处理,然后遍历函数列表和参数列表,做相关的函数执行器逻辑,就像我下面代码一样。

public class MyFuncBaseVisitor extends FuncBaseBaseVisitor<Object> {

    @Override
    public Object visitExpression(FuncBaseParser.ExpressionContext ctx) {
        if (null != ctx.methodExec()) {
            return visitMethodExec(ctx.methodExec());
        } else if (null != ctx.BOOL()) {
            return Boolean.parseBoolean(ctx.BOOL().getText());
        } else if (null != ctx.NUMBER()) {
            return Double.parseDouble(ctx.NUMBER().getText());
        } else {
            return ctx.TEXT().getText();
        }
    }

    @Override
    public Object visitMethodExec(FuncBaseParser.MethodExecContext ctx) {
        // --获得函数名
        String methodName = ctx.methodName().getText();

        // --获得参数信息
        List<String> argList = ctx.methodExecArguments().expression().stream().map(expressionContext -> {
            Object o = visitExpression(expressionContext);
            if (null == o) {
                return null;
            }
            return o.toString().replaceAll("'", "");
        }).collect(Collectors.toList());

        // --函数处理,这里只是demo,一般情况下需要使用不同放入函数执行逻辑器来处理相应的功能
        if ("str_join".equals(methodName)) {
            // --System.out.println("str_join,params:" + argList);
            String splitChar = argList.get(0);
            return String.join(splitChar, argList.subList(1, argList.size()));
        } else if ("str_lowercase".equals(methodName)) {
            // --System.out.println("str_lowercase,params:" + argList);
            StringBuilder strBuffer = new StringBuilder();
            for (int i = 0; i < argList.size(); i++) {
                strBuffer.append(argList.get(i).toLowerCase());
                if (i < argList.size() - 1) {
                    strBuffer.append(",");
                }
            }
            return strBuffer.toString();
        } else {
            System.err.println("Unknown Method " + methodName);
            return null;
        }
    }
}

测试解析器

public class MyFuncBaseTest {
    public static void main(String[] args) {

        // --编写DSL语句
        String exprStr = "str_join('##',str_lowercase('aBc5'),'d',1,true)";
        CodePointCharStream codePointCharStream = CharStreams.fromString(exprStr);

        // --创建词法解析器(Lexer)
        FuncBaseLexer lexer = new FuncBaseLexer(codePointCharStream);

        // --获得Tokens流
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        // --创建语法解析器(parser)
        FuncBaseParser parser = new FuncBaseParser(tokens);

        // --获得AST解析树
        FuncBaseParser.ExpressionContext parserTree = parser.expression();

        // --打印AST解析树
        System.out.println("ParserTree: " + parserTree.toStringTree(parser));

        // --使用visitor遍历树
        MyFuncBaseVisitor myFuncBaseVisitor = new MyFuncBaseVisitor();
        Object val = myFuncBaseVisitor.visit(parserTree);
        System.out.println("Visitor Result: { " + val + " }");
    }
}

返回结果


image.png

其他的函数可以同理实现。

总结

Antlr4和JavaCC思路大体一致。不过个人在开发使用上,感觉Antlr4更加人性化,尤其是Antlr4语法文件的IDEA调试和AST树的遍历上。
1,大体流程都是将语法文件翻译词法解析器(Lexer)和语法分析器(Parser)。
2,Antlr4和JavaCC语法文件除了格式不一样,思路差不多,词法器描述主要是使用正则匹配,语法器描述使用EBNF语法。
2,Antlr4提供了多语言(java,C++,Python,golang等)的支持,JavaCC只能翻译成java的Lexer和Parser。
3,Antlr4 Idea Plugin在调试语法文件上非常人性化,直观简单。
4,Antlr4提供的Listener和Visitor两种树遍历器非常方便,jjTree使用上则需要一定的功底。
5,一般业务场景都是使用Antlr4处理SQL解析,除了大名鼎鼎的Calcite,我们也可以使用Antlr4处理相应的SQL解析在执行的业务,就像SparkSQL一样。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似sql的查询语言去查询库里包含的内容...
    yufeiyang1995阅读 5,010评论 0 3
  • > Antlr4 是一个强大的解析器的生成器,可以用来读取、处理、执行或翻译结构化文本,ANTLR可以从语法上来生...
    kikiki4阅读 186评论 0 2
  • > Antlr4 是一个强大的解析器的生成器,可以用来读取、处理、执行或翻译结构化文本,ANTLR可以从语法上来生...
    kikiki4阅读 204评论 0 2
  • > Antlr4 是一个强大的解析器的生成器,可以用来读取、处理、执行或翻译结构化文本,ANTLR可以从语法上来生...
    kikiki4阅读 80评论 0 2
  • > Antlr4 是一个强大的解析器的生成器,可以用来读取、处理、执行或翻译结构化文本,ANTLR可以从语法上来生...
    kikiki4阅读 83评论 0 1