前言
作为一个开发实践项目,实现对HiveSQL语句的解析可以很有效的作为管理用户查询权限的管理。对于这方面的知识本身也不是非常熟悉了解,很多时候也是边学边记。身边也没有人指导这个方案具体该怎么实现怎么做,只有需求是要将复杂查询或者是关联建表的SQL语句能够将其库名和表名全都给提取出来并且能够实现上下游的追溯。这个功能最好还是用JAVA或者Scala写,毕竟Hive的底层还是JAVA写的方便,但是初步改写的话我常用语言还是python,java还是有点生疏的。因此本人是打算使用python来进行sql的血缘解析的,在网上搜索一番后发现有个系列正好是关于此方法的:系列:用python+antlr解析hive sql获得数据血缘关系。
该方法我也完全实现了但是只能对一般的sql进行解析,对于那些出现过复杂SQL关联或者是二次建表语句就会报错,无奈只能先搁置该方法一段时间,因为涉及到HiveSQL本身的grammar文件的改写,这方面确实实在难以下手而且本人对JAVA源码改写完全达不到水准...只能先记录下这次尝试,望以后有机会再来改进!
文章紧接上篇内容。
一.parser grammar改写
前面python测试代码里,parser.statement() 这个调用是固定的,HiveParser.g里对应的规则片段是这样的:
// starting rule statement : explainStatement EOF | execStatement EOF ;
那个starting rule是hive自身的注释。antlr的语法规则被执行时也有一个入口,就和c语言的main函数,java类里的main方法类似。
在一大堆已有的语法规则里,找到入口规则还是比较简单的,因为这个入口有个硬性要求,它必须能处理完整输入,规则匹配条件里就必须带有EOF这个特定的Token,它表示输入的结束(End Of File)。
找到入口规则后,python里的调用语句就知道怎么写了,不过因为java是个强类型语言,python的变量不能随便传,必须符合java的类型系统才行,所以整个调用链条有一个长长的倒推过程:
parser需要有个入口 => 通过EOF找到parser.statement()
Parser实例需要有个初始化函数 => 初始化函数的签名可以通过Parser.javaconstructor这个列表查阅到
Parser是用autoclass自动查找java类名后,映射到python里的"类定义对象",在python里用type检查它的话,类型是JavaClass。
在jinus的文档里找到JavaClass里有__javaconstructor__这个属性
__javaconstructor__的属性是list of tuples, 每个tuple对应一种可接受的构造函数签名,tuple的第1个还是tuple,代表需要传入的参数类型,tuple的的第2个是返回值类型,因为java的构造函数不需要返回值,那里固定是false
这个Parser的构造函数其实也可以通过查阅antlr的说明文档得到,但隐藏得挺深,还有v2 v3 v4的版本区别,不如使用jnius反射回的结果直接。
Parser构造函数签名[('(Lorg/antlr/runtime/TokenStream;)V', False), ('(Lorg/antlr/runtime/TokenStream;Lorg/antlr/runtime/RecognizerSharedState;)V', False)]需要符合TokenStream的这个接口作为输入,在antlr里的文档里查到CommonTokenStream这个类
CommonTokenStream这个类的构造函数需要TokenSource接口,构造函数签名[('()V', False), ('(Lorg/antlr/runtime/TokenSource;I)V', False), ('(Lorg/antlr/runtime/TokenSource;)V', False)]正是前面得到的HiveLexer类实现的接口
HiveLexer类的构造函数需要CharStream接口,构造函数签名[('(Lorg/antlr/runtime/CharStream;)V', False), ('(Lorg/antlr/runtime/CharStream;Lorg/antlr/runtime/RecognizerSharedState;)V', False), ('()V', False)]查antlr的文档知道有StringStream和FileStream两个现成的类,理论上也可以自行用java实现自定义的CharStream
StringStream的构造函数签名`[('()V', False), ('(Ljava/lang/String;)V', False), ('([CI)V', False)]`,这里的的java.lang.String已经是jnius能自动映射到python 内置类型str的类了
构造sql字符串,完成调用链。
要实用,从解决上面的简单测试里就发现的3个问题开始
- token的大小写问题, Hive里select 和SELECT都能接受
- 分号问题,也就是必须能解析一个字符串里包含多个sql语句的情况
- 解析规则,类似insert-select这种hive里接受,但HiveParser.g文件里没有定义的情况
二、得到AST
上一篇的代码其实已经走到了临门一脚。作为解析入口的parser.statement()这个方法是有返回值的,默认生成的返回类型是自动生成的一个类, HiveParser.statement_return, AST 就藏在这个类里,可以通过这个类的getTree(),得到一个类型为CommonTree 的对象。用python代码拿到这个CommonTree的代码如下
import jnius_config jnius_config.set_classpath('./','./grammar/hive310/antlr-3.5.2-complete.jar') import jnius StringStream = jnius.autoclass('grammar.hive310.ANTLRNoCaseStringStream') Lexer = jnius.autoclass('grammar.hive310.HiveLexer') Parser = jnius.autoclass('grammar.hive310.HiveParser') TokenStream = jnius.autoclass('org.antlr.runtime.CommonTokenStream') sql_string = ( "SELECT hour(from_unixtime(cast(gpstime/1000 as BIGINT),'yyyy-MM-dd HH:mm:ss')),dt " "from track_point_traffic_dev.tk_track_point_attach_road_info " "where admincode ='110105'" "and dt BETWEEN '2022-07-28' and '2022-08-04'" "limit 1000000" ) sqlstream = StringStream(sql_string) inst = Lexer(sqlstream) ts = TokenStream(inst) parser = Parser(ts) ret = parser.statements() treeroot = ret.getTree() lex=[] def walktree(node,depth = 0): print("%s%s=%s" % (" "*depth,node.getText(),node.getType())) if(node.getType()==24): lex.append(node.getText()) children = node.children if not children: return ch_size = children.size() for i in range(ch_size): ch =children.get(i) walktree(ch,depth + 1) def get_table(treeroot,depth=0): children = treeroot.children ch_size = children.size() ch = children.get(1) walktree(treeroot,0) print(lex)
遍历AST需要先查阅一下CommonTree这个类的[API文档](https://www.antlr3.org/api/Java/org/antlr/runtime/tree/CommonTree.html) ,AST的每个节点都是一个CommonTree这个类的实例,有token这个Field可以访问节点本身代表的token,有getType和getText这样的方法可以直接访问token上的属性,节点的子节点可以访问children这个Field,也可以通过getChildren方法得到,也有相应的parent和getParent。有了这些,在整个AST树上就可以随意游走了。
children的java类型是java.util.List, 不能直接在python里做iteration,代码里通过for循环访问下标做访问。上面的代码输出结果为
三、优化问题
这里其实还有很多问题需要处理,但是有二个问题很容易想到分号和大小写。本篇文章已经够多内容了,放到下篇再讲。