如何用Python的sqlparse来分析SQL

sqlparse是Python的一个解析SQL语言的库,安装和文档我也不累赘说了,提供一下官网的地址python-sqlparse,在官网给出的github库里面,有一个提取表名的example,挺好用的,大家可以直接使用。

官方的文档比较简单,所以我希望在这篇文章里面补充一些信息,帮助大家理解文档和使用这个工具包。

sqlparse有几个最简单的工具:split,format,parse,分别是提取sql单个语句、格式化sql的语句以及解析sql,这几个函数官网和其他文章都有大量的例子,也就不再介绍。

先给出这篇文章使用的sql例子:

CREATE TABLE TABLE_TO_CREATE NOLOGGING AS
SELECT	DISTINCT
	A.COLA,
	B.COLB,
	DECODE(A.DECODE_CONDITION, 1, '是', '否') DECODED,
	ROW_NUMBER() OVER(PARTITION BY A.CLASS_CONDITION ORDER BY A.RAND_CONDITION DESC) RN
FROM	FSCRM.TABLE_A A,
	(SELECT * FROM TABLE_C C WHERE C.SOMETHING='SOMETHING' AND C.NUM=1234) B 
WHERE	A.COMPARE_CONDITION=B.COMPARE_CONDITION
AND 	A.NUM NOT IN (1, 2, 3)
AND 	NOT EXISTS (SELECT D.COLD FROM TABLE_D WHERE A.COLA=D.COLD)
ORDER BY A.ORDER_CONDITION
/* COMMENTS */
;

了解过sqlparse的人都知道,使用parse后,sql语句被解析成一棵树。这棵树跟常见的树不一样,因为它的父节点,是完全包含了子节点的信息。例如在上面的例子:

In [1]: import sqlparse

In [2]: with open('sample.sql', 'r', encoding='utf8') as sql_file:
   ...:     file_parse = sqlparse.parse(sql_file.read().strip())
   ...:

In [3]: file_parse
Out[3]: (<Statement 'CREATE...' at 0x49E6CF0>,)

In [4]: for token in file_parse[0].tokens:
   ...:     print(type(token), token.ttype, token.value)
   ...:
<class 'sqlparse.sql.Token'> Token.Keyword.DDL CREATE
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword TABLE
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Identifier'> None TABLE_TO_CREATE NOLOGGING
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword AS
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

<class 'sqlparse.sql.Token'> Token.Keyword.DML SELECT
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword DISTINCT
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.IdentifierList'> None A.COLA,
                B.COLB,
                DECODE
<class 'sqlparse.sql.IdentifierList'> None (A.DECODE_CONDITION, 1, '是', '否')
ECODED,
                ROW_NUMBER
<class 'sqlparse.sql.Identifier'> None () OVER
<class 'sqlparse.sql.Identifier'> None (PARTITION BY A.CLASS_CONDITION ORDER BY
A.RAND_CONDITION DESC) RN
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

<class 'sqlparse.sql.Token'> Token.Keyword FROM
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.IdentifierList'> None FSCRM.TABLE_A A,
                (SELECT * FROM TABLE_C C WHERE C.SOMETHING='SOMETHING' AND C.NU
=1234) B
<class 'sqlparse.sql.Token'> Token.Text.Whitespace

<class 'sqlparse.sql.Where'> None WHERE A.COMPARE_CONDITION=B.COMPARE_CONDITION
AND     A.NUM NOT IN (1, 2, 3)
AND     NOT EXISTS (SELECT D.COLD FROM TABLE_D WHERE A.COLA=D.COLD)

<class 'sqlparse.sql.Token'> Token.Keyword ORDER
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword BY
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Identifier'> None A.ORDER_CONDITION
/* COMMENTS */

<class 'sqlparse.sql.Token'> Token.Punctuation ;

这里要提醒一下的就是,从上面的out[3]大家可以看到parse语句返回的是一个tuple,就算只有一个statement,也是一个tuple,所以我们在使用的时候需要加上下标[0]。

解析出来的statement其实也是token的一个子类,而如果token的is_group是True的话(这个group跟sql的group不是一回事),每个token都有tokens属性,存了一个子token的list。当我们遍历后会发现,里面的数据类型除了基本类型Token,还有Where,Identifier,IdentifierList等等,这些都是继承了Token的子类,但大多没有额外的方法,基本就是多了一些声明包含关键字的常量,都是类属性。而我们查看ttype这个属性,主要是标注了一些关键字或者标点符号如空格、括号、分号等。我们进行选择的时候可以通过ttype做个初步的筛选。而value存的是这个token的整个语句,即便是它是一个group,里面有很多子token,它也是完全存了所有子token拼成的整个语句。所以当我们使用子token的parent属性时,很容易就可以找到父结点的整句sql语句。

下面我们来介绍Identifier和IdentifierList:

In [10]: identifier_list = file_parse[0].tokens[13]

In [11]: identifier_list
Out[11]: <IdentifierList 'A.COLA...' at 0x4BCD138>

In [12]: identifier_list.get_identifiers()
Out[12]: <generator object IdentifierList.get_identifiers at 0x0000000004C22BF8>


In [13]: for identifier in identifier_list.get_identifiers():
    ...:     print(type(identifier), identifier.ttype, identifier.value)
    ...:
<class 'sqlparse.sql.Identifier'> None A.COLA
<class 'sqlparse.sql.Identifier'> None B.COLB
<class 'sqlparse.sql.Identifier'> None DECODE

In [14]: for identifier in identifier_list.get_identifiers():
    ...:     print(type(identifier), identifier.ttype, identifier.value, identi
    ...: fier.get_real_name())
    ...:
<class 'sqlparse.sql.Identifier'> None A.COLA COLA
<class 'sqlparse.sql.Identifier'> None B.COLB COLB
<class 'sqlparse.sql.Identifier'> None DECODE DECODE

从上述代码可以看到,IdentifierList比普通的token多了一个方法get_identifiers方法,返回的是一个Identifier的列表,而每个Identifier可以使用get_real_name来获取真名,这个用在提取字段名和表名的时候非常有用。

最后一个想介绍的就是Where类型,这个类型下面主要是一系列的条件:

In [4]: where = file_parse[0].tokens[22]

In [5]: where
Out[5]: <Where 'WHERE ...' at 0x49E6E58>

In [6]: for token in where.tokens:
   ...:     print(type(token), token.ttype, token.value)
   ...:
<class 'sqlparse.sql.Token'> Token.Keyword WHERE
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Comparison'> None A.COMPARE_CONDITION=B.COMPARE_CONDITION
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

<class 'sqlparse.sql.Token'> Token.Keyword AND
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Identifier'> None A.NUM
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword NOT
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword IN
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Parenthesis'> None (1, 2, 3)
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

<class 'sqlparse.sql.Token'> Token.Keyword AND
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword NOT
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Token'> Token.Keyword EXISTS
<class 'sqlparse.sql.Token'> Token.Text.Whitespace
<class 'sqlparse.sql.Parenthesis'> None (SELECT D.COLD FROM TABLE_D WHERE A.COLA
=D.COLD)
<class 'sqlparse.sql.Token'> Token.Text.Whitespace.Newline

这里重点要看的是,对于比较语句,有个特别的token子类叫Comparison,但是诸如not in、not exist等特殊语句,在这里面是拆分成几个token的,这点比较不友好。我们在编程的时候如果要计算条件数,不能直接数token数,计算and的个数比较好。

最后在提几个小细节,如果我们不想按层次去遍历语句,工具包提供了一个方法flatten,直接可以把所有子、孙结点平铺输出:

In [9]: for id, item in enumerate(where.flatten()):
   ...:     print(id, item.value)
   ...:
0 WHERE
1
2 A
3 .
4 COMPARE_CONDITION
5 =
6 B
7 .
8 COMPARE_CONDITION
9

10 AND
11
12 A
13 .
14 NUM
15
16 NOT
17
18 IN
19
20 (
21 1
22 ,
23
24 2
25 ,
26
27 3
28 )
29

30 AND
31
32 NOT
33
34 EXISTS
35
36 (
37 SELECT
38
39 D
40 .
41 COLD
42
43 FROM
44
45 TABLE_D
46
47 WHERE
48
49 A
50 .
51 COLA
52 =
53 D
54 .
55 COLD
56 )
57

这个种方式的用途之一可以一次过查找所有子查询,然后通过parent语句找出父节点:

In [11]: for item in file_parse[0].flatten():
    ...:     if item.ttype is sqlparse.tokens.Keyword.DML and item.value.upper(
    ...: ) == 'SELECT':
    ...:         print(item.parent)
    ...:
CREATE TABLE TABLE_TO_CREATE NOLOGGING AS
SELECT  DISTINCT
                A.COLA,
                B.COLB,
                DECODE(A.DECODE_CONDITION, 1, '是', '否') DECODED,
                ROW_NUMBER() OVER(PARTITION BY A.CLASS_CONDITION ORDER BY A.RAND
_CONDITION DESC) RN
FROM    FSCRM.TABLE_A A,
                (SELECT * FROM TABLE_C C WHERE C.SOMETHING='SOMETHING' AND C.NUM
=1234) B
WHERE   A.COMPARE_CONDITION=B.COMPARE_CONDITION
AND     A.NUM NOT IN (1, 2, 3)
AND     NOT EXISTS (SELECT D.COLD FROM TABLE_D WHERE A.COLA=D.COLD)
ORDER BY A.ORDER_CONDITION
/* COMMENTS */
;
(SELECT * FROM TABLE_C C WHERE C.SOMETHING='SOMETHING' AND C.NUM=1234)
(SELECT D.COLD FROM TABLE_D WHERE A.COLA=D.COLD)

以上就是sqlparse的解析过程,实践出真知,大家可以在解析的过程中多输出各个token的类型、属性,借此来深入了解这个工具包。希望本文对你有所帮助。

    原文作者:KionWong
    原文地址: https://blog.csdn.net/qq_39607437/article/details/79620383
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞