阿川CH
学海无涯,上栽上栽!
Toggle navigation
阿川CH
主页
归档
标签
Antlr4学习
2018-03-30 16:33:06
0
0
0
cqc
## 总览 ### 文法文件经过ANTLR编译后的产物 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a4) ### ANTLR使用流程 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a2) ### Linux 安装、配置 1. 下载antlr4 二进制包,并解压到某目录,该目录以下称为ANTLR_HOME 2. 写个shell,命名为antlr4, 内容为 ``` java -cp "/usr/local/lib/antlr-4.6-complete.jar:$CLASSPATH" org.antlr.v4.Tool $* ``` 3. 写个shell,命名为grun, 内容为 ``` java -cp "/usr/local/lib/antlr-4.6-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig $* ``` ## LEXER 词法分析器 ### LEXER 词法命名 - 词法命名以大写字母开头,正常全部用大写,多个单词以 _ 下划线连接 - 匿名常量声明,声明在语法规则中,如右图的“{”、“}”、“,”,优先级高于有名词法 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a6) ### LEXER 词法规则 - 常量值则直接将值写在单引号中 - 多个可选值可用 | 来分开 - 当可选值都是单字符时,可写在[]中,此时中括号中的值不用- 单引号。当在[]前加上~时,表示除了..之外 - 表达区间的两种方式:[a-z], 或 ‘a’ .. ‘z’ - 对于表示0到N个重复,用* - 对于表示1到N个重复,用+ - 对于表示0到1个重复,用? - 以 ; 封号结束一条规则 - 用 \ 来作为转义符 ### LEXER的作用 将输入的字符流切割成一个个Token。比如这段文法 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a1) 对字符串`{1,2,{3,4},5}`,经过分词后生成 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a3) 下面对其中的一个token进行说明`[@5,8:10='451',<INT>,1:8]` @5表示这是输入的句子中的第5个token(标记) 8:10='451' 表示在整个输入中,在位置8(从0开始)和10之间有文本451 <INT>表示标记类型为INT 1:8 表示从第1行(首行为1)的位置8开始 ### LEXER 的贪婪和懒惰模式 - 正常表达多个重复时,用*或+,默认的是贪婪模式 - 当在*和+后面加上?时,启用懒惰模式 示例: 输入: “Hello””World” 当规则为:'“' . * '”'时,分词后为“Hello””World”一个Token 当规则为:'“' . *? '”'时,分词后为“Hello”和”World”两个Token ### LEXER 的高阶用法 - 在LEXER的规则后面加上 `-> skip`表示匹配到的这些字符丢弃。比如`WS : [\t\r\n]+ -> skip ;` - 在LEXER的规则后面加上 `-> channel(HIDDEN)`表示匹配到的这些字符隐藏起来,不参与解析树。比如`L_WS : ' ' -> channel(HIDDEN) ;` ### fragment 当lexer用fragment来修饰时,表示将不能放在parser中使用,并且也不会被用来解析token,仅可用在lexer中进行引用。比如 ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a7) ![](/api/file/getImage?fileId=5abdf373418f8a54f60000a5) ### LEXER 的匹配优先级 输入流按流的顺序持续匹配各LEXER规则,以匹配最多的LEXER规则作为Token类型。当匹配最多的有多个LEXER规则时,以先声明的规则作为Token类型。 比如这个文法 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000aa) ![](/api/file/getImage?fileId=5abdf648418f8a54f60000ac) ## PARSER 语法分析器 ### PARSER 语法命名 语法命名以小写字母开头,多个单词间可用驼峰写法(presto写法),也可用_下划线连接(hive写法)。比如 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000af) ### PARSER语法规则 - 一条语法规则可由其他语法规则或LEXER组成 - 一个语义可以有多个可选项(如建表语句有多种),多个之间用 | 来分开 - 对于表示0到N个重复,用* - 对于表示1到N个重复,用+ - 对于表示0到1个重复,用? 如 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000a8) ### Parser之Label:给规则细化名称 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000ab) ### 遍历解析树方法之一:Listener 在对文法文件进行编译时,默认会生成两个Listener类,一个接口类,一个实现接口的基础类,而这个基础类实现的方法都是空的。我们要做的就是继承基础类实现方法的业务逻辑 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000ad) 用法为: ```java HplsqlLexer lexer = new HplsqlLexer(new ANTLRInputStream(sql)); CommonTokenStream tokens = new CommonTokenStream(lexer); HplsqlParser parser = new HplsqlParser(tokens); ParseTree parseTree = parser.program(); ParseTreeWalker walker = new ParseTreeWalker(); TableListener listener = new TableListener(); walker.walk(listener, parseTree); return listener.getTransormedSQL(); ``` ### 遍历解析树方法之二:Visitor 在对文法文件进行编译时,默认不会生成Visitor,使用如下命令来生成`antlr4 -no-listener -visitor LabeledExpr.g4`, 同Listener一样,也是生成一个接口类和一个基础类, 基础类也是对接口类的空实现 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000ae) 用法为: ```java String str = "5 * (10 + 2)\n"; LabeledExprLexer lexer = new LabeledExprLexer(new ANTLRInputStream(str)); CommonTokenStream tokens = new CommonTokenStream(lexer); LabeledExprParser parser = new LabeledExprParser(tokens); ParseTree tree = parser.prog(); EvalVisitor eval = new EvalVisitor(); System.out.println("eval.visit(tree) = " + eval.visit(tree)); ``` ## 动作(加入语言代码) ### 动作 每个规则里的组成元素后面都可以加大括号对,然后里面写入当匹配到当前元素时的处理逻辑,比如下面这个语法: ``` root : (line NL)+; line : (left = WORD) {System.out.println("left:"+$left.text);} '=' (right = WORD) {System.out.println("right:"+$right.text);} ; WORD : ~[=\r\n] +; NL : '\r' ? '\n'; ``` ![](/api/file/getImage?fileId=5ca1d6d1418f8a54f60002f6) ### 规则变量 locals ``` localTest locals [int i = 0] : ('a'{ System.out.println(++$i); })* ; ``` 在规则(每个规则在编译后都会转成一个Context类)里使用`locals`来声明一个成员变量: ![](/api/file/getImage?fileId=5ca1c72f418f8a54f60002f2) 本例子的意思是,每出现一个a字符就对i自增并打印当前i的值 ### parser的成员变量和方法 通过声明`parser::members`来添加parser类的成员变量和方法 ``` @parser::members { // add members to generated RowsParser int col; public RowsParser(TokenStream input, int col) { // custom constructor this(input); this.col = col; } } file: (row NL)+ ; row locals [int i=0] : ( STUFF { $i++; if ( $i == col ) System.out.println($STUFF.text); } )+ ; TAB : '\t' -> skip ; // match but don't pass to the parser NL : '\r'? '\n' ; // match and pass to the parser STUFF: ~[\t\r\n]+ ; // match any chars except tab, newline ``` 这个语法是用于打印出第几列csv格式下的文本 ![](/api/file/getImage?fileId=5ca1d6d1418f8a54f60002f3) ### 规则支持自定义输入参数 ``` grammar Data; file : group+ ; group: INT sequence[$INT.int] ;//传入参数 sequence[int n] //定义输入参数n locals [int i = 1;] : ( {$i<=$n}? INT {$i++;} )* // match n integers ; INT : [0-9]+ ; // match integers WS : [ \t\n\r]+ -> skip ; // toss out all whitespace ``` 定义输入参数,其本质是创建一个带该参数的构造函数 ![](/api/file/getImage?fileId=5ca1d6d1418f8a54f60002f4) ![](/api/file/getImage?fileId=5ca1d6d1418f8a54f60002f5) ### 使用语义判定改变语法分析过程 比如有这么一组数字:`2 2 3 4 5 6 7 8`,逻辑要求是: 第一个出现的2表示接下的2个数字 2和3归为一组,然后4又表示接下来的4个数字5 6 7 8归为一组,以此类推。 先上语法规则: ``` grammar Data; file : group+ ; group: INT sequence[$INT.int] ; sequence[int n] locals [int i = 1;] : ( {$i<=$n}? INT {$i++;} )* // match n integers ; INT : [0-9]+ ; // match integers WS : [ \t\n\r]+ -> skip ; // toss out all whitespace ``` `{$i<=$n}?`即为判定规则,当小于n个时一直是true,INT token持续匹配;而当大于n时,就判断结果是false,后续的INT也不再匹配 测试用例: ``` public class TestData extends DataBaseListener{ @Override public void exitGroup(DataParser.GroupContext ctx) { System.out.println(ctx.INT.getText()+":"+ctx.sequence().INT().stream() .map(TerminalNode::getText).collect(Collectors.joining(","))); } public static void main(String[] args) { String input = "2 3 4 5 6 7 8 9"; DataLexer lexer = new DataLexer(CharStreams.fromString(input)); DataParser parser = new DataParser(new CommonTokenStream(lexer)); parser.addParseListener(new TestData()); parser.file(); } } 输出: 2:3,4 5:6,7,8,9 ``` ## 其他 ### Import : 引入外部的文法规则 当文法规则很庞大时,将其切分成一个个模块,或者将一些公共词法剥离出来,让其他方法规则共享,这会是很好的处理方式 ![](/api/file/getImage?fileId=5abdf648418f8a54f60000a9) ### 文法设计原则 - 一个优秀的文法,应该是没有歧义的 - 将无关的字符在LEXER阶段通过skip给丢弃 - 尽量将能放在一起的字符组成一个token免得在parser中还要区分多个的token类型
上一篇:
Web Application Proxy 介绍
下一篇:
Hive技巧:通过增加冗余分区来简化sql复杂度,提高并行度
文档导航