Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the wp-pagenavi domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114

Notice: 函数 _load_textdomain_just_in_time 的调用方法不正确twentyseventeen 域的翻译加载触发过早。这通常表示插件或主题中的某些代码运行过早。翻译应在 init 操作或之后加载。 请查阅调试 WordPress来获取更多信息。 (这个消息是在 6.7.0 版本添加的。) in /var/www/blog.zhujinhui.net/wp-includes/functions.php on line 6114
项目管理 – 煅魂-JeffreyChu的修炼屋

sqlparse,除了解析还能做什么

之前在项目开发中发现每个人编写的SQL格式都有点不一样,比如缩进/关键字大小写不一样,当时就在想有没有类似Python的Black库那样可以格式化Python代码来保持一样代码风格?

那就是今天要介绍的sqlparse库了。

简介

sqlparse是一个用来解析SQL查询语句的轻量级Python库。它并不执行SQL查询操作,而是专注于解析和格式化SQL语句。它可以帮助开发者处理SQL代码的缩进、格式化以及拆分复杂查询,甚至支持对查询结构的分析。

  • 格式化:对凌乱的SQL语句进行格式化,输出可读性强的代码。
  • 解析:将SQL语句解析为结构化的token流,便于进一步的操作。
  • SQL分析:支持对SQL语句中的结构进行深入分析,识别出关键字表名列名函数等组件。

安装

在开始使用sqlparse之前,我们需要先安装该库。你可以通过Python的包管理工具pip进行安装:

pip install sqlparse

安装完成后,便可以在项目中导入并使用sqlparse了。

基础用法

sqlparse的核心功能主要包括两部分:格式化和解析。

格式化

SQL语句的格式化是sqlparse最基本的功能之一。通过简单的函数调用,开发者可以将一行SQL语句自动格式化为多行、缩进合理的代码,从而提高可读性。

假设有一个复杂的SQL语句,它在一行中很难阅读:

import sqlparse

sql =  """
select a.id, b.key, c.name, d.addr, f.mid from a left join b on a.id = b.id left join c on a.name = c.name left join d on d.key = b.key left join (select e.addr, f.mid from e join f on e.mid = f.mid) t on t.addr = d.addr WHERE b.key IS NOT NULL and c.name != '' order by a.id desc limit 100
"""
formatted_sql = sqlparse.format(sql, reindent=True, keyword_case='upper')
print(formatted_sql)

运行这段代码后,你将得到一个格式化后的SQL语句:

SELECT a.id,
       b.key,
       c.name,
       d.addr,
       f.mid
FROM a
LEFT JOIN b ON a.id = b.id
LEFT JOIN c ON a.name = c.name
LEFT JOIN d ON d.key = b.key
LEFT JOIN
  (SELECT e.addr,
          f.mid
   FROM e
   JOIN f ON e.mid = f.mid) t ON t.addr = d.addr
WHERE b.key IS NOT NULL
  AND c.name != ''
ORDER BY a.id DESC
LIMIT 100

sqlparse.format()函数提供了许多可选参数,使开发者能够自定义格式化的行为:

  • keyword_case:用于控制关键字的大小写。可选值包括:
    • 'upper':将关键字转换为大写。
    • 'lower':将关键字转换为小写。
  • reindent:启用后将自动重新缩进SQL代码,默认值为False
  • indent_width:指定缩进的宽度,默认为2
  • strip_comments:启用后将删除SQL语句中的所有注释,默认值为False

所以,我们可以启用多个选项来生成更加个性化的格式, 以美化我们平常写出来的复杂的SQL语句。

解析

sqlparse不仅可以格式化SQL,还可以对SQL进行解析。它可以将SQL语句拆分成多个token,便于开发者对SQL的结构进行深入分析。

基本解析

我们可以使用sqlparse.parse()函数来解析SQL语句,并生成一个Statement对象列表,其中每个Statement对象表示一条SQL查询语句。

import sqlparse

sql = """
select id, name, age from users limit 10;
select id, name from coms limit 10;
"""
parsed = sqlparse.parse(sql)
for index, stmt in enumerate(parsed):
    print(index, ":", stmt.value.strip())

输出:

0 : select id, name, age from users limit 10;
1 : select id, name from coms limit 10;

这将输出被解析的SQL语句的结构。在这里,parsed是一个包含Statement对象的列表,开发者可以对每个Statement对象进行进一步操作。

Token 的概念

sqlparse使用Token的概念来表示SQL语句中的每一个组成部分,包括关键字、表名、操作符等。通过sqlparse,我们可以轻松提取这些信息。下面是一个简单的示例:

import sqlparse

sql = """select id, name, age from users limit 10;"""

parsed = sqlparse.parse(sql)

for stmt in parsed:
    for token in stmt.tokens:
        print(token.ttype, token.value)

输出:

Token.Keyword.DML select
Token.Text.Whitespace  
None id, name, age
Token.Text.Whitespace  
Token.Keyword from
Token.Text.Whitespace  
None users
Token.Text.Whitespace  
Token.Keyword limit
Token.Text.Whitespace  
Token.Literal.Number.Integer 10
Token.Punctuation ;

这段代码将逐个输出每个token的类型和值。ttypetoken的类型,如关键字运算符等,而valuetoken的实际值。

Token类型分类

sqlparse库中的Token类型被分类为多个类别,如:

  • KeywordSQL中的关键字,如SELECTFROM等。
  • Identifier:表名、列名等标识符。
  • Literal:字面值常量,如数字和字符串。
  • OperatorSQL中的操作符,如=><等。开发者可以根据需求对这些不同类型的token进行分类或过滤。

SQL分析

借助sqlparse的解析能力,我们可以进一步对SQL结构进行分析。这对复杂的SQL查询尤为有用,尤其是当开发者需要从查询中提取表名列名或其他组件时。

提取表名

通过sqlparse,我们可以轻松提取出SQL查询语句中使用的表名。以下是一个简单的示例:

import sqlparse

from sqlparse.sql import Identifier
from sqlparse.tokens import Keyword

def extract_tables(sql):
    parsed = sqlparse.parse(sql)
    stmt = parsed[0]
    tables = []
    for token in stmt.tokens:
        if isinstance(token, Identifier):
            tables.append(token.get_real_name())
        elif token.ttype is Keyword and token.value.upper() == 'FROM':
            pass
    return tables

sql = "SELECT id, name FROM users u JOIN orders o ON u.id = o.user_id"
tables = extract_tables(sql)
print(tables)

该函数会解析SQL并返回查询中涉及到的所有表名。在这个例子中,输出为:

['users', 'orders']

提取列名

类似地,我们也可以提取SQL查询中的列名。这个操作非常适合用于需要对SQL查询进行静态分析的场景。

import sqlparse

from sqlparse.sql import IdentifierList

def extract_columns(sql):
    parsed = sqlparse.parse(sql)
    stmt = parsed[0]
    columns = []
    for token in stmt.tokens:
        if isinstance(token, IdentifierList):
            for identifier in token.get_identifiers():
                columns.append(identifier.get_real_name())
    return columns

sql = "SELECT id, name, age FROM users"
columns = extract_columns(sql)
print(columns)

输出结果将为:

['id', 'name', 'age']

高级用法

在前面的基础功能介绍之外,sqlparse还提供了一些高级特性,帮助开发者处理更加复杂的SQL查询。

SQL语句拆分

sqlparse支持对多条SQL语句进行拆分,这对需要批量处理或分析多个SQL查询时非常有用。以下示例展示了如何使用sqlparse.split()函数来拆分多条SQL语句:

import sqlparse

sql = "SELECT * FROM users; INSERT INTO users(id, name) VALUES (1, 'Alice');"
statements = sqlparse.split(sql)
for statement in statements:
    print(statement)

输出:

SELECT * FROM users;
INSERT INTO users(id, name) VALUES (1, 'Alice');

sqlparse.split()函数会根据SQL中的分号(;)将SQL语句拆分成多个独立的语句。

定制化解析

除了内置的Token类型和解析逻辑,sqlparse还允许你定制解析过程。通过对Statement对象的深入分析,开发者可以编写自定义规则来处理复杂的SQL查询。

过滤器

sqlparse中的过滤器机制可以让开发者在解析过程中动态操作Token流。你可以通过编写过滤器来操作SQL结构中的各个部分,例如关键字标识符表达式

例如,我们可以编写一个简单的过滤器,将所有SQL关键字转换为小写(这个其实可以用sqlparse.format()自身也可以实现):

import sqlparse

class LowerKeywordFilter:
    def process(self, stmt):
        def process_token(token):
            if token.is_group:
                for sub_token in token.tokens:
                    process_token(sub_token)
            elif token.is_keyword:
                token.value = token.value.lower()
            return token

        for token in stmt.tokens:
            process_token(token)

        return stmt

sql = "SELECT ID, NAME FROM USERS WHERE AGE > 30;"
parsed = sqlparse.parse(sql)[0]
lower_keyword_filter = LowerKeywordFilter()
processed_stmt = lower_keyword_filter.process(parsed)
print(processed_stmt)

输出结果:

select ID, NAME from USERS where AGE > 30;

通过过滤器,开发者可以对SQL语句中的任何元素进行操作。

SQL格式定制化

除了基础的SQL格式化功能外,sqlparse还允许开发者根据需求进一步定制输出格式。

比如。忽略部分元素。sqlparse允许你在格式化时忽略某些元素。例如,如果SQL语句中包含注释,但你希望忽略这些注释,可以使用strip_comments=True选项:

import sqlparse

sql = "SELECT ID, NAME -- this is a comment\nFROM USERS"
formatted_sql = sqlparse.format(sql, reindent=True, strip_comments=True)
print(formatted_sql)

输出结果:

SELECT ID,
       NAME
FROM USERS

注释被自动移除,这对在生产环境中处理分析大量SQL查询时可能非常有用。

SQL代码审查

sqlparse不仅仅是可以用于分析SQL代码,还可以用于审查SQL,提升SQL安全性和规范性。

比如在项目过程中硬性规定创建一个表设计不能超过5个普通索引,那我们可以拉出数据库的建表语句或者保留在项目中DDL(Data Definition Language,数据定义语言) 文件, 通过sqlparse检查脚本检测哪些已存在的或准备提交创建的表不合规(当然这只是其中一种思路,方法不只一种)

import sqlparse

def check_indexes_count(ddl_statement):
    index_count = 0
    parsed = sqlparse.parse(ddl_statement)
    for statement in parsed:
        tokens = statement.flatten()
        for token in tokens:
            if token.ttype == sqlparse.tokens.Keyword and token.value.upper() == 'INDEX':
                index_count += 1
    return index_count > 5

ddl = """CREATE TABLE my_table (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    address INT,
    INDEX idx_name (name),
    INDEX idx_age_name (age, name),
    INDEX idx_another (id),
    INDEX idx_more (age),  
    INDEX idx_extra (name),  
    INDEX idx_address (address)
);"""
if check_indexes_count(ddl):
    print(f"该DDL语句中有超过5个索引。")
else:
    print(f"该DDL语句中没有超过5个索引。")

输出:

该DDL语句中有超过5个索引

甚至我们在单元测试的时候,审查级别还可以下沉到DML(Data Manipulation Language, 数据操作语言) 即将操作的层面来进行SQL分析审查,比如检查改查询语句where条件里是否用了非索引字段进行查询,是否有SQL注入风险等

应用场景

由于sqlparse具备灵活的SQL解析与格式化功能,它在多个开发场景中扮演着重要角色。以下是sqlparse的一些典型应用场景:

  • 在开发过程中,SQL查询语句常常会变得复杂且难以阅读。sqlparse可以自动对SQL进行格式化,确保查询语句结构清晰、缩进合理。这在代码审查中尤为重要,有助于提高SQL代码的可读性维护性
  • 使用sqlparse的解析功能,开发者可以自动化地分析SQL查询,提取表名列名关键字索引等信息。这种静态分析能力可以帮助检测潜在的SQL性能问题,并为查询优化提供数据支持。
  • 在数据库系统中,SQL查询日志记录了用户的所有查询操作。sqlparse可以用于解析和分析这些日志,帮助开发者追踪查询行为、检测潜在的安全问题或生成审计报告。
  • 在数据迁移和管理过程中,sqlparse可以解析复杂的SQL脚本,自动调整查询结构或生成新的SQL语句。这对于大规模数据库迁移或自动化SQL脚本生成非常有用。

总结

sqlparse不仅是一个简单的SQL格式化工具,它还提供了强大的SQL解析功能。

通过使用sqlparse,开发者可以简化复杂的SQL查询操作,提升SQL代码的可读性,并为静态分析审查与查询优化提供坚实的基础。

如果你觉得本文对您有一点点帮助的话,希望能得到您的一点点支持,也欢迎在评论区说出您自己的见解。

flake8,一个让你的Python代码更漂亮的库

众所周知,PEP 8规范是 Python 社区制定的一份关于代码格式和风格的优秀指南。

高质量的代码不仅提高了软件的可维护性和可读性,还减少了错误和漏洞,从而提升了软件的可靠性和安全性。

概念

Flake8 是一个广泛使用的 Python 代码质量检查工具,它结合了多个流行的静态代码分析工具,为开发人员提供了全面的代码检查功能。Flake8 的核心组件包括:

  • PyFlakes:检测代码中的错误,例如未使用的变量或未定义的名称。
  • pycodestyle (原名 PEP8):检查代码是否符合 PEP 8,即 Python 的官方编码规范。
  • McCabe:计算代码的圈复杂度,帮助开发人员识别复杂度过高的函数和方法。

为何要装

  • 保持代码的一致性和可读性
  • 发现代码中的潜在错误和问题
  • 让代码遵循 Python 编码规范(PEP 8)

安装和配置

pip安装

pip install flake8

接下来,在项目根目录创建.flake8配置文件,如:

[flake8]
max-line-length = 88
exclude = 
  .git,
  __pycache__,
  old,
  build,
  dist
ignore = E501,W503

上述ini代码配置中:

  • max-line-length = 88表示设置了代码行的最大长度限制为 88 字符。如果代码行超过这个长度,Flake8 将报告一条错误信息。这个值不同于默认的 79 字符,通常是为了与其他工具(如black格式化器)保持一致。
  • exclude这一项指定了 Flake8 在进行代码检查时要排除的目录列表。配置中的这些目录将不会被 Flake8 检查,比如例子就排除了.git__pycache__oldbuilddist这几个目录。
  • ignore = E501,W503则选项用于指定 Flake8 在进行代码检查时应忽略的错误和警告类型。这里忽略了两种特定的错误和警告

使用 Flake8 检查代码

配置之后,就可以使用flake8进行检查代码质量,

单个文件
flake8 your_script.py
整个项目
flake8 .

其实还可以这样临时指定参数运行,这里命令行参数会覆盖掉上面的.flake8文件里的配置参数

flake8 --ignore=E501,W503 your_script.py

输出解读

在执行完flake8命令后,比如用 Flake8 检查这段代码:

def my_function():
    print("Hello, World!")
print("This line is too long, it exceeds the maximum line length recommended by PEP 8.")

我们可能看到类似这样的错误或告警的输出

example.py:3:1: E305 expected 2 blank lines after class or function definition, found 1
example.py:4:80: E501 line too long (86 > 79 characters)

这些错误/告警提示,都是在指导你怎么把代码写的更加PEP 8规范化,提示也是比较明显了。更多错误可以参考这个PEP 8文档:

https://pep8.readthedocs.io/en/latest/intro.html#error-codes

忽略规则

在 Flake8 中,除了通过在.flake8配置文件中指定文件或文件夹外,它还提供了使用特定的注释来告诉 Flake8 在代码中忽略特定行的检查。这些注释称为 “Flake8 noqa comments”。

在代码中插入 # noqa 注释可以告诉 Flake8 忽略该行的检查。这在某些情况下很有用,例如当你知道某一行违反了规范,但你有充分的理由保留它时(尤其是一些比较急的热修复时)。

全局禁用
# flake8: noqa
import os  # noqa

在这个例子中,# flake8: noqa 告诉 Flake8 忽略整个文件的检查,而 # noqa 注释告诉 Flake8 忽略 import os 这一行的检查。

特定禁用
import os  # noqa: F401

在这个例子中,# noqa: F401 告诉 Flake8 忽略 import os 这一行的 F401 错误(未使用的导入)检查。

注意事项

尽量减少使用 # noqa 注释,它们应该只在确实需要的情况下使用,并且要确保有充分的理由来禁用检查,否则会导致漏检查引发隐藏问题。

集成到工作流

  • 在 IDE/编辑器中集成 Flake8(如 VSCode、PyCharm),这样在写代码的时候就能注意到问题
  • 在 CI/CD 管道中使用 Flake8, 比如在 GitHub Actions 中使用 Flake8
name: Flake8 Lint

on: [push, pull_request]

jobs:
  flake8:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.x'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install flake8
      - name: Run Flake8
        run: |
          flake8 .

这样就能保证合并到仓库的代码是统一规范的。

插件

Flake8 还提供了其他优秀插件扩展功能,比如flake8-docstrings可以帮我们进行一些文档字符串检查(如函数/类/模块是否有文档注释等)

总结

Flake8 是 Python 开发中常用的代码质量检查工具,通过集成多个静态分析工具,帮助开发者确保代码符合PEP 8规范,提高可读性和一致性,提高了团队开发的效率。

参考链接

  • flake8 官方文档:https://flake8.pycqa.org/en/latest/index.html#quickstart
  • PEP 8 编码规范:https://wangmeng-python.readthedocs.io/en/latest/readability/PEP8.html