使用ANTLR做一个简单的Python SQL语法解析器

最近在做一个数据库相关的平台(按照潮流现在平台应该叫做SAAS平台。。。),对于数据库相关的平台来说,SQL的语法分析就比较重要了,不管是从格式化SQL,还是分析SQL所涉及到的表、列来做安全审计,或者是自助SQL的提交所涉及到的语法检查及内部规范要求,都需要用到语法分析。

语法分析最容易想到的就是学校里的编译原理课程了,对于想希望知道底层原理的人来说当然建议去看龙书啦,不过从实用角度来说我们就不用去关心相关的原理了,我们的目的就是使用相关的工具解析对应的SQL。

目前能找到的比较常见的工具有lex+yacc,antlr。最近对这两个工具都试用了一下,功能都狠强大。虽然MySQL内部使用了自定义的lexer+yacc来实现语法解析,但lex+yacc的相关资料实在是太少,在我遇到的一些问题后很难找到相关的资料。因此最后决定使用antlr。如果读者有兴趣想要学习一下的话,建议先看下《lex and yacc》这本书,然后去看下《The Definitive ANTLR 4 Reference》。前者讲的比较纯粹,适合入门,后者由于antrl的功能丰富了许多因此讲的东西会多一些。

antrl目前我用的是v4版本,因为这个版本的parser文件和lex+yacc的很像。antrl默认会生成java的解析文件,我们这里需要把它转成Python。

首先我们来看下一个parser文件的内容,最简单的例子如下:

[stack@dev a]$ cat SQLLexer.g4 
lexer grammar SQLLexer;

SELECT
    : 'select'
    ;

FROM
    : 'from'
    ;

COMMA
    : ','
    ;

ID
    : ( 'a' .. 'z' | 'A' .. 'Z' | '_' )+
    ;

WS
    : ( ' ' | '\t' | '\n' | '\r' )+ -> skip
    ;
[stack@dev a]$ cat SQLParser.g4 
parser grammar SQLParser;

options
   { tokenVocab = SQLLexer; }

stat
    : select_clause
    ;

select_clause
    : SELECT column_list_clause FROM table_references
    ;

column_list_clause
    : column_name (COMMA column_name)*
    ;

column_name
    : ID
    ;

table_references
    : ID
    ;
[stack@dev a]$ antlr4 -visitor -Dlanguage=Python2 SQLLexer.g4 
[stack@dev a]$ antlr4 -visitor -Dlanguage=Python2 SQLParser.g4 
[stack@dev a]$ ls -trl
total 40
-rw-rw-r-- 1 stack stack   206 Nov 29 11:58 SQLLexer.g4
-rw-rw-r-- 1 stack stack   298 Nov 29 12:00 SQLParser.g4
-rw-rw-r-- 1 stack stack  2166 Nov 29 12:00 SQLLexer.py
-rw-rw-r-- 1 stack stack    60 Nov 29 12:00 SQLLexer.tokens
-rw-rw-r-- 1 stack stack 10583 Nov 29 12:00 SQLParser.py
-rw-rw-r-- 1 stack stack  1326 Nov 29 12:00 SQLParserListener.py
-rw-rw-r-- 1 stack stack   903 Nov 29 12:00 SQLParserVisitor.py
-rw-rw-r-- 1 stack stack    60 Nov 29 12:00 SQLParser.tokens

这里我们的parser会分析最简单的SELECT语句。当我们执行了上面的命令后,我们会看到当前目录下生成了很多的py文件。这些文件就能被我们的执行程序调用啦。一个简单的例子为:

import sys

from antlr4 import *
from SQLParser import SQLParser 
from SQLLexer import SQLLexer
from SQLParserListener import SQLParserListener
from SQLParserVisitor import SQLParserVisitor


def main(argv):
    input = FileStream(argv[1])
    lexer = SQLLexer(input)
    stream = CommonTokenStream(lexer)
    parser = SQLParser(stream)
    tree = parser.stat()
    v = SQLParserVisitor()
    v.visit(tree)
 
if __name__ == '__main__':
    main(sys.argv)

然后我们执行如下命令:

[stack@dev a]$ echo 'select a from b' > sqls.txt
[stack@dev a]$ python test.py sqls.txt 

我们会发现什么都没有输出。我们改下sqls.txt的内容,写一个不符合我们上面定义的语句进去:

[stack@dev a]$ echo 'selet a from b' > sqls.txt
[stack@dev a]$ python test.py sqls.txt 
line 1:0 missing 'select' at 'selet'
line 1:6 extraneous input 'a' expecting {'from', ','}

可以看到当我们漏写了select中的一个e后,我们的test.py解析文件就会报错了。

下面来看下如何得到我们的表名和列名。我们在parser文件的这一行后面添加一个#开头的内容:

: SELECT column_list_clause FROM table_references # printSQLInfo

然后运行我们上面的命令后,在SQLParserVisitor.py文件中我们可以看到visitPrintSQLInfo这个method:

    # Visit a parse tree produced by SQLParser#printSQLInfo.
    def visitPrintSQLInfo(self, ctx):
        return self.visitChildren(ctx)

我们先在这个visitPrintSQLInfo中加一个print ‘here’,然后运行我们的test.py文件:

[stack@dev a]$ python test.py sqls.txt 
here

可以看到输出了here。原理其实很简单,tree = parser.stat()其实已经得到了我们完整的语法树信息,相关信息存放在tree中。调用visit其实就是后期在这个tree中游走罢了。这里游走到了语法书的visitPrintSQLInfo,然后就会输出我们的print语句内容。下面我们来获得表名和列名。

首先修改我们的parser文件如下:

[stack@dev a]$ cat SQLParser.g4 
parser grammar SQLParser;

options
   { tokenVocab = SQLLexer; }

stat
    : select_clause 
    ;

select_clause
    : SELECT column_list_clause FROM table_references # printSQLInfo
    ;

column_list_clause
    : column_name (COMMA column_name)*
    ;

column_name
    : ID # printColumnName
    ;

table_references
    : ID # printTableName
    ;

接着修改我们的visit文件如下:

# Generated from SQLParser.g4 by ANTLR 4.5.1
from antlr4 import *

# This class defines a complete generic visitor for a parse tree produced by SQLParser.

class SQLParserVisitor(ParseTreeVisitor):

    # Visit a parse tree produced by SQLParser#stat.
    def visitStat(self, ctx):
        return self.visitChildren(ctx)


    # Visit a parse tree produced by SQLParser#printSQLInfo.
    def visitPrintSQLInfo(self, ctx):
        return self.visitChildren(ctx)


    # Visit a parse tree produced by SQLParser#column_list_clause.
    def visitColumn_list_clause(self, ctx):
        return self.visitChildren(ctx)


    # Visit a parse tree produced by SQLParser#printColumnName.
    def visitPrintColumnName(self, ctx):
        print 'column:', ctx.getText()
        return self.visitChildren(ctx)


    # Visit a parse tree produced by SQLParser#printTableName.
    def visitPrintTableName(self, ctx):
        print 'table:', ctx.getText()
        return self.visitChildren(ctx)

最后运行我们的test文件:

[stack@dev a]$ python test.py sqls.txt 
column: a
table: b

很简单对吧:)。其实现在已经可以看出,在使用了antlr或者是lex+yacc这种工具后,对于我们的最大的难点就不是编译原理中的那些东西了,难点现在变成了如何编写我们的grammar文件(也就是本文的SQLParser.g4和SQLLexer.g4)。本文的SQLParser.g4文件只能解析最简单的select语句,如何扩展到支持MySQL、Oracle、PG等SQL的语法规则(虽然它们大部分都支持SQL/92标准,但也有相当多的自定义规则)这是我们目前要去慢慢做的。目前有一个可以作为base的grammar文件:https://github.com/apache/incubator-tajo/blob/master/tajo-core/tajo-core-backend/src/main/antlr4/org/apache/tajo/engine/parser 。

当然解析器本身的编写也是一个难点。递归递归递归。。。

当然通过antlr能做的不仅仅是SQL的解析啦,当有一天有人让我们把一个java文件转成python文件,或者是要做语法高亮之类的事情的时候,通过antlr就可以做了~另外http://ruslanspivak.com/这个人的博客里他在写如何写一个简单的解析器,也可以看看。

1 Response

  1. Orozco 2016年2月21日 / 下午2:29

    請問,關於SQL語法的解析,後來您有更多的grammar嗎?
    因為我需要解析sql 語法,真的發現寫 grammar 很難,除了參考你給的連接之外,
    還有其他的嗎?

    謝謝

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*