news 2026/10/3 14:43:17

SQL解析利器sqlparse:从格式化到AST遍历的Python实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL解析利器sqlparse:从格式化到AST遍历的Python实战指南

做数据开发这几年,我越来越觉得,sqlparse就是那种平时不起眼、但真到用的时候能救命的小工具。它是Python生态里最常用的SQL解析库,不依赖任何第三方库,纯Python实现,做的事情很专一:帮你把SQL语句拆开、理顺、格式化,甚至按语法结构一层层剥开来看。最近好几个项目——一个是自动化报表平台的SQL审计,一个是数据脱敏工具——都靠它撑起来的。这篇文章我就把sqlparse的完整玩法、踩过的坑、以及怎么把它嵌进真实工程里的经验一次性讲透,希望能给同样在处理SQL文本、SQL脚本的读者一点参考。

在动手写业务代码之前,先聊清楚一个问题:为什么我们不自己写字符串处理,非要用一个解析库?

这个问题的答案,得从SQL文本的复杂性说起。想想你平时面对的SQL长什么样:有换行、有缩进、有注释、有字符串常量、有多层嵌套的子查询,还有各种奇奇怪怪的写法。如果只用正则或split去切,很容易翻车。比如按分号切分,但字符串或者注释里也有分号;比如按空格提取关键词,但函数参数里的空格要怎么处理;再比如注释里的内容长得像SQL,却要被识别成注释而不是语句。这些都是边界问题,自己造轮子写正则,写完一轮又一轮,总有覆盖不完整的地方。

sqlparse解决的核心问题,就是对SQL做词法分析和语法解析。词法分析就是把字符流拆成有意义的“单词”,也就是Token;语法解析就是把这些Token按SQL语法规则组合成有层级关系的结构。有了这个能力,你就可以做很多事:把乱糟糟的SQL格式化得漂漂亮亮,把一条长脚本拆成一条条语句,或者遍历解析出的Token树,提取表名、字段、WHERE条件,做静态分析和审计。它的解析是非验证性的——也就是说,它不检查你的SQL是不是符合所有语法规则、表是否存在,它只管“能不能读出来”。这个特点既是优点也是坑,后面我会单独讲。

适合用sqlparse的人,我总结下来主要有几类:一是写SQL客户端的后端开发,要美化用户输入的SQL、做语法高亮的预处理;二是做数据平台或可视化工具的人,比如Superset、Redash这类开源项目里就内嵌了它;三是写数据校验、脱敏、审计脚本的工程师;四是想深入理解SQL结构、但不想自己手写解析器的同学。这篇文章的内容不挑基础,我会从安装到深层API一路讲下去,新手可以跟着操作,老手则可以看看第四节里的遍历技巧和工程化注意点。

1. sqlparse的定位:它不是一个“校验器”

先把最容易被误会的地方说清楚。很多人看到“SQL Parser”这个名字,以为它能帮你检查SQL写得对不对。实际上,sqlparse的设计目标并不是验证SQL语法正确性,而是理解SQL的结构。官方文档说得明白:它只做非验证性的解析。这意味着,即使你给它一段明显有语法错误的SQL,它也能解析出一个结果来,只是那个结果可能不符合你的预期。

这个定位的好处是解析速度快、容错性好,能处理各种方言、各种不标准写法,甚至能解析残缺的SQL片段。坏处是,千万别拿它当语法校验工具用。我见过有同事在自动化测试里用sqlparse来判断SQL是否正确,结果把错误SQL放过了。要校验SQL语法正确性,应该用特定数据库的解析器,比如PostgreSQL的pg_query、MySQL的sqlglot结合方言模式,或者干脆把SQL丢给数据库EXPLAIN一下。

另一个容易混淆的点是,sqlparse也不会执行SQL,它输入输出都是字符串或内存中的对象,不连数据库,也不做元数据解析。所以它极轻量,整个库就是几个模块,几乎不占依赖体积。

下面是sqlparse的几个核心模块名称和作用,先建立一个整体地图:

模块/对象作用
sqlparse.lexer词法分析器,把字符串拆成Token流
sqlparse.parser语法解析器,把Token流组合成语句结构
sqlparse.sql定义Token、TokenList、Identifier、Statement等核心类
sqlparse.tokensToken类型常量,比如Keyword、Name、Operator、Whitespace
sqlparse.filters过滤器框架,用于格式化、脱敏、去注释等
sqlparse.formatter封装了格式化入口,内部通过Filters实现
sqlparse.engine分组聚合逻辑,把Token组织成更上层的结构
sqlparse.keywordsSQL关键字定义集合,供词法分析使用

这种模块设计思路值得学。它把“切词”“组句”“结构抽象”“行为处理”四个层次分开,每层只做一件事。你平时要是自己写文本解析工具,也可以借鉴这种分层:先有lexer产出原子单位,再通过parser组装成中间结构,最后用filters做变换,而不是把所有逻辑堆在一个函数里。

2. 安装与环境准备

sqlparse的安装非常简单,它没有那些令人头大的编译依赖,不需要编译C扩展,不挑操作系统。直接一句话:

pip install sqlparse

如果你机器上有多个Python环境,比如conda、pyenv或系统自带的Python,要小心pip装到哪个环境里了。我建议在项目目录里用虚拟环境管理:

python -m venv venv source venv/bin/activate # Windows上用 venv\Scripts\activate pip install sqlparse python -c "import sqlparse; print(sqlparse.__version__)"

这里有一个我在实操里遇到过的细节:部分老旧教程会推荐pip install sqlparse==0.2.4这样固定版本。如果你只是做基础格式化,老版本也能用,但如果你要用get_type()、get_name()这类方法,或者要处理一些边缘语法,我强烈建议装到0.4.x以上版本,因为0.4.0之后修复了大量语句分组问题,walk()方法的行为也稳定了很多。我当前环境里跑的是0.4.4,下面所有代码都以这个版本为基准。

装完之后快速验证一下能不能用,跑这个:

import sqlparse sql = "select a,b,c from test_table where id=1" print(sqlparse.format(sql, reindent=True, keyword_case='upper'))

正常情况下你应该会看到一条被规范化的SQL输出。如果这一步就报错了,大概率是Python版本或者pip源的问题。建议升级pip:python -m pip install --upgrade pip,然后把下载源切到国内镜像(比如清华源)。

3. 三个最常用的入口:format、split、parse

sqlparse对绝大多数人来说只需要掌握三个顶层函数:format()、split()、parse()。很多高阶玩法都是围绕它们展开的,所以我把这三个函数的参数和返回结果彻底讲明白。

3.1 format:格式化SQL

sqlparse.format(sql, **options)接受的第一个参数是原始SQL字符串,然后用关键字参数控制格式化行为。我最常用的几个参数:

  • keyword_case='upper'或'lower':统一关键词大小写。SQL习惯上关键词大写,但很多开发写代码时习惯小写,这个参数能一键统一。
  • identifier_case:统一表名、字段名大小写,可选'upper'或'lower',谨慎使用,因为数据库里的表名可能区分大小写。
  • strip_comments=True:删除所有注释,包括行注释和块注释。
  • reindent=True:重新缩进,格式化层级关系。
  • indent_width=4:缩进几个空格,默认4,也可以indent_tabs=True改成Tab。
  • indent_columns=True:对列名进行对齐缩进,多列时看起来更整齐。
  • wrap_line_length=80:超过长度自动换行,适合放入代码审查或日志系统。
  • compact=True:去掉多余空格,把语句压得更紧凑,适合做存储优化或日志压缩。

举个完整的例子看看效果:

import sqlparse ugly_sql = """ SELECT order_id, customer_name, sum(amount) as total_amount from orders o join users u on o.user_id=u.id where o.status='PAID' and u.vip_level>2 group by order_id,customer_name having sum(amount)>1000 order by total_amount desc; """ good_sql = sqlparse.format( ugly_sql, reindent=True, keyword_case='upper', strip_comments=True, indent_width=4, ) print(good_sql)

输出大概是:

SELECT order_id, customer_name, sum(amount) AS total_amount FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'PAID' AND u.vip_level > 2 GROUP BY order_id, customer_name HAVING sum(amount) > 1000 ORDER BY total_amount DESC;

这里我的私人经验是:不要把keyword_case和identifier_case同时设置成upper。因为有的数据库里,表名或字段名是大小写敏感的,尤其是用引号包起来的那种字段名,一旦被统一成大写,实际执行时会报“对象不存在”。格式化这种操作最好是“能不改业务语义就不改”。我一般只开启reindent和keyword_case,其他参数按需开。

3.2 split:拆分SQL脚本

sqlparse.split(sql)的返回结果是一个字符串列表,每个元素是一条独立的SQL语句。它的拆分依据是分号,但智能地绕开了注释、字符串和某些函数体中的分号。

import sqlparse script = """ -- 创建用户表 create table users ( id int primary key, name varchar(64) not null ); insert into users(id, name) values (1, 'a;b'); select * from users; -- 这一行末尾有注释 """ statement_list = sqlparse.split(script) for i, stmt in enumerate(statement_list, 1): print(f'语句{i}: {stmt.strip()}')

输出会按三条语句剥离出来。注意第二条values (1, 'a;b')里的分号在字符串内部,不会被误切,这就是词法分析比字符串split强的地方。

但要注意一个边界:split()返回的是字符串,不是Statement对象。如果你需要拿到语句类型,比如区分“这是INSERT还是SELECT”,得用parse()代替。

3.3 parse:返回可遍历的Statement对象

sqlparse.parse(sql)是最核心的入口,它返回一个Statement列表。Statement本质上是一个TokenList,你可以遍历它、打印它、访问它的各种属性。同一个SQL用split()和parse()得到的东西,一个相当于“切好的菜”原始文本块,一个相当于“摆好结构的菜”可编程对象。

import sqlparse sql = "select id, name from users where id = 1" statements = sqlparse.parse(sql) stmt = statements[0] print(type(stmt)) print(stmt.get_type()) # 返回 DML 类型,比如 SELECT print(stmt.get_name()) # 对某些语句返回对象名 print(type(stmt.tokens)) # 子token列表

get_type()是我用得最多的方法。它能返回SELECT、INSERT、UPDATE、DELETE、CREATE等语句类型。有了它,你在做SQL审计的时候就能快速把增删改查分类。get_name()则在一些场景里能直接帮你提取出主要操作对象,不过它依赖语句结构,具体行为我建议在目标SQL上实测一下,不要想当然。

4. 深入解析:理解Token和TokenList

如果你只是想格式化SQL,看完上一节就够了。但如果你想写自定义的SQL分析、脱敏、提取工具,就必须理解sqlparse的“内部语言”:Token和TokenList。

4.1 Token是什么

在sqlparse的世界里,一条SQL被拆解成一个一个的Token。Token代表的是“不可再拆分的原子片段”,比如一个关键字、一个操作符、一个数字、一个字符串常量、一段空白。每个Token有两个关键属性:ttype(Token的类型)和value(实际文本)。

import sqlparse sql = "select id from users where age > 18" stmt = sqlparse.parse(sql)[0] for token in stmt.tokens: print(repr(token.ttype), '|', repr(token.value))

输出可以看到类似:

<TokenType.Name.Builtin> | 'select' <Whitespace> | ' ' <TokenType.Name> | 'id' <Whitespace> | ' ' <TokenType.Keyword> | 'from' ...

ttype是判断Token角色的关键。比如sqlparse.tokens.Keyword.DML表示增删改查这类DML关键字,sqlparse.tokens.Keyword.DDL表示建表删表这类DDL关键字,sqlparse.tokens.Name表示标识符,sqlparse.tokens.Whitespace表示空白。你可以用判断来进行精细过滤。

实际开发中,直接遍历stmt.tokens是远远不够的,因为很多Token不是孤立的,它们会被组合成更高层的结构,比如Identifier(标识符)、Function(函数)、Where(条件)等。这些结构本身又是TokenList,里面还可以嵌套其他TokenList。相当于从“单词级”上升到了“短语级”的语法分析。

4.2 使用walk遍历整棵树

要遍历一棵完整的解析树,最方便的方法是TokenList.walk()。它采用深度优先遍历,把所有直接和间接的子Token都“摊平”出来,你可以过滤出你感兴趣的类型。

一个很经典的场景是:提取SQL里所有涉及的表名。

先说明为什么不能简单地用“from后面跟着的字符串”来提取。因为:SQL里可能有别名、可能有子查询、可能有JOIN、可能FROM跟的是括号内的子查询而不是真实表名。手工处理这些情况非常痛苦,用Tree结构会清晰很多。我常用的实现是这样:

import sqlparse from sqlparse.sql import Identifier, IdentifierList from sqlparse.tokens import Keyword, Punctuation def extract_tables(sql): tables = set() stmt = sqlparse.parse(sql)[0] for token in stmt.tokens: if token.ttype is Keyword: # 只处理 FROM 和 JOIN 这些关键位置 if token.value.upper() in ('FROM', 'JOIN', 'UPDATE', 'INTO', 'TABLE'): token_idx = stmt.token_index(token) # 取这个关键字之后的第一个非空白Token next_token = None for t in stmt.tokens[token_idx+1:]: if not t.is_whitespace: next_token = t break if isinstance(next_token, Identifier): tables.add(next_token.get_real_name()) elif isinstance(next_token, IdentifierList): for ident in next_token.get_identifiers(): tables.add(ident.get_real_name()) # 如果是INSERT,需要额外处理 elif token.ttype is Keyword.DML and token.value.upper() == 'INSERT': # 找INTO后面的表 pass return tables

为了不让这段代码过于复杂,我只做了基本处理。但通过这个例子你可以看到核心思路:先定位关键字Token,再找到它后面紧跟的结构化对象,再从对象中提取真实表名(get_real_name()会去掉别名返回原始表名)。如果是JOIN位置,还要处理后面可能是子查询的情况,这时Identifier可能代表一个子查询别名,get_real_name()可能返回None,需要进一步判断里面有没有嵌套SELECT。

我的经验是:提取表名永远是“再简单也比想象的复杂”的活。最好准备一套针对常见SQL模式的测试用例,至少覆盖:单表查询、JOIN多表、子查询、INSERT INTO、UPDATE多表、带别名的查询。只有测试用例跑过了,你才敢放到生产环境的分析管道里。

4.3 修改SQL:基于树的操作

Tree结构还有一个强力好处:你可以原地修改Token,生成一个新的SQL字符串。比如数据脱敏场景,要把SELECT语句里的敏感字段名替换掉或者打码。做法是先找到目标Identifier,替换它的Token值,再调用str(token_list)得到完整SQL。

import sqlparse from sqlparse.sql import Identifier def mask_columns(sql, sensitive_columns, replacement='***'): stmt = sqlparse.parse(sql)[0] for token in stmt.tokens: if isinstance(token, Identifier): # 这里简单判断列名 real_name = token.get_real_name() if real_name in sensitive_columns: # 用 replacement 替换原 token 的 value for sub in token.flatten(): if sub.ttype is sqlparse.tokens.Name: sub.value = replacement return str(stmt)

这段代码演示了修改Token值然后重新拼回SQL的思路。但必须提醒,SQL脱敏是高风险操作,改错了轻则业务逻辑出问题,重则数据泄露。所以写脱敏工具时,我建议先用解析出来的结构做一个“脱敏日志”,记录每个替换发生的位置和原值,再在测试环境跑一遍脱敏前后的SQL比对。另外要注意,不要简单地把字段名替换成***,因为SQL里引号、类型推断都会出问题,更稳妥的做法是把查询改成SELECT NULL AS col_name或者改写为CASE WHEN ... THEN 'MASKED' END这种结构。

5. 真实场景:如何把sqlparse嵌进工程

这一节我会介绍三个真实工程里我用sqlparse完成的典型需求,每个需求都附有实现思路和代码骨架。

5.1 场景一:慢SQL日志自动分类

线上数据库慢查询日志里,有大量重复形态的SQL,只是参数值不同。为了快速分析哪些“形态”的SQL最慢,我们需要把SQL去变量化(Parameterize),再做聚簇统计。sqlparse在这里起到关键作用:先解析SQL,然后把类型是Token.Literal.Number.Integer、Token.Literal.String.Single这类值的Token统一替换成占位符?。

import sqlparse from sqlparse.tokens import Literal def parameterize_sql(sql): stmt = sqlparse.parse(sql)[0] for token in stmt.tokens: if token.ttype in (Literal.Number.Integer, Literal.String.Single): token.value = '?' return str(stmt)

这样一个“慢SQL聚合分析”脚本就有了基础结构。配合正则把所有空白归一,然后按哈希分组,就能得到同一形态SQL的执行次数、平均耗时。使用sqlparse而不是纯正则的好处是对字符串内的内容免疫,不会把WHERE name = '张三'里的'张三'漏掉,也不会误伤字符串里恰好长得像数字的部分。

5.2 场景二:查询日志中的库表引用审计

另一个场景是审计业务系统访问了哪些核心表。这个需求要求从每天数万条SQL日志中提取表名清单。sqlparse虽然性能不算极致,但处理单条SQL的速度极快,实测下来每秒几千条没问题,瓶颈通常在日志读取和IO上。

我封装了一个提取器类:

import sqlparse class TableExtractor: def __init__(self): self.tables = set() def extract(self, sql): stmts = sqlparse.parse(sql) for stmt in stmts: for token in stmt.tokens: if isinstance(token, sqlparse.sql.Identifier): real_name = token.get_real_name() if real_name: self.tables.add(real_name) def get_tables(self): return sorted(self.tables)

这里要注意,直接遍历stmt.tokens只能拿到顶层Token,会漏掉FROM后面的联合子查询内部的表名。要更全面,可以用walk():

def extract_tables_advanced(sql): tables = set() stmt = sqlparse.parse(sql)[0] for token in stmt.walk(): if isinstance(token, sqlparse.sql.Identifier): # 只考虑FROM/JOIN/INTO等上下文中的Identifier parent = token.parent if parent is not None: grandparent = parent.parent if grandparent is not None: # 这里要用上下文判断 pass return tables

用walk()虽然能把所有层级的Identifier都遍历出来,但会带来一个新问题:WHERE条件中的字段名也是Identifier,FROM子句里的表名也是Identifier,你怎么区分?最常见的方法是检查这个Identifier的父节点上下文,或者检查它后面所跟的Token。我自己的经验是:做提取时不要硬刚语法分析,先用get_type()判断语句类型,再去特定位置找,这样准确率更高。

5.3 场景三:SQL格式化服务化

我还做过一个内部API服务,接收用户提交的随意SQL,返回美化后的格式化版本,供开发在缺陷单或文档中展示。这个服务直接用sqlparse.format()实现,但加了几个工程细节:

from flask import Flask, request, jsonify import sqlparse app = Flask(__name__) @app.post('/format_sql') def format_sql(): raw_sql = request.json.get('sql', '') if not raw_sql.strip(): return jsonify({'error': 'empty sql'}) try: formatted = sqlparse.format( raw_sql, reindent=True, keyword_case='upper', strip_comments=False, indent_width=2, wrap_line_length=100, ) return jsonify({'formatted': formatted}) except Exception as e: return jsonify({'error': str(e)}), 400

工程上要加三个防线:一是限制单条SQL长度,避免超大文本压垮服务;二是限制并发,因为format是纯CPU计算,高并发会打满CPU,最好用线程池或异步队列控速;三是对格式化结果做截断,防止返回超长文本导致网络阻塞。这些小坑都是我线上踩过的。

6. 常见问题与排查技巧

sqlparse很好用,但在使用过程中因为理解不到位,也会出现不少“看起来奇怪”的现象。这里我整理了几个高频问题,帮你少走弯路。

6.1 为什么split后语句不完整

很多人会用split()先拆语句,再逐条处理。遇到的情况是:语句末尾注释或分号不见了,或者第一条语句开头多了些奇怪东西。这通常是因为SQL文本本身不干净,比如前面有BOM头、换行、不可见字符。建议在进入sqlparse之前先做一层文本清洗:

sql = sql.strip() sql = sql.lstrip('\ufeff') # 去掉BOM头

另外,如果SQL里包含存储过程或触发器的BEGIN...END;块,split()会因为分号出现在END内部而拆到不应该拆的地方。这种场景推荐用parse()后逐个拿Statement,因为Statement的分组逻辑会更高级一点,但仍然无法完美处理过程型SQL,可能需要正则辅助。

6.2 为什么get_name()返回None

get_name()不是对所有语句都有效的。比如INSERT INTO users ...里它可能返回None,CREATE TABLE users ...里能返回users,SELECT ...里它可能返回第一个Identifier。这个行为依赖sqlparse对语句类型的解析。建议不要依赖get_name(),而是自己写提取逻辑。拿到Statement后先看get_type(),再对不同类型的语句走不同的提取分支。

6.3 为什么格式化后语义变了

有几种情况会导致格式化后SQL语义发生变化:

第一,注释被误删。如果你设置了strip_comments=True,那么片段里的业务说明就被删掉了。如果只是展示用问题不大,但如果要回放或执行,删除注释通常不影响语义,但有可能影响某些数据库对hint的识别。比如MySQL的/*+ ... */优化器hint也是注释,被删掉之后执行计划就可能变化。所以生产环境回放SQL时,我绝不加strip_comments这个参数。

第二,关键字大小写改变。前文提过,某些数据库对大小写敏感,如果字段名恰好是小写并且被统一成了大写,执行时可能查不到列。虽然这种情况少,但一旦出现排查起来也很耗时。

第三,缩进导致的换行。有些SQL语句特别长,格式化成多行后,截断位置如果发生在字符串中间或注释中间,就有问题了。sqlparse一般不会在字符串内部换行,但如果你使用了wrap_line_length,还是建议把输出结果再做一次单测校验。

6.4 性能与内存优化建议

sqlparse是纯Python实现的,性能天然比不了C扩展解析器。如果你要批量处理百万行SQL日志,我的建议是:不要频繁调用format或parse的单条接口,那会有很大的函数调用开销。用生成器和批处理配合:

import sqlparse def batch_parse(sql_iter, batch_size=10000): buffer = [] for sql in sql_iter: buffer.append(sql) if len(buffer) >= batch_size: yield sqlparse.parse('\n'.join(buffer)) buffer = [] if buffer: yield sqlparse.parse('\n'.join(buffer))

当然,如果你对性能的要求到了毫秒级,比如在网关层对每个请求的SQL做实时解析,那可能需要考虑C扩展版本或Rust、Go等语言的解析库。但在绝大多数后台任务场景,sqlparse的性能完全够用。一个参考数据:我本地处理20万条平凡SQL的提取表名任务,总耗时大约50秒左右,单条在0.2毫秒量级。

6.5 版本更新带来的兼容性问题

sqlparse的API在过去几年发生过一些调整,有些方法在旧版本不可用,有些行为在新版本有变化。如果你遇到“莫名的报错”,优先检查版本。我遇到过的典型差异是:0.2.x里parse('')可能返回包含空语句的列表,0.4.x里对空输入返回空列表;还有Identifier.get_real_name()在旧版本里对一些带引号标识符的处理有明显bug。所以项目里最好锁定一个大版本,比如用sqlparse>=0.4.3,<0.5。

6.6 关于中文编码

如果你的SQL里包含中文注释或中文表名,一定要确保传入Python的字符串是str类型,并且终端环境的编码是UTF-8。sqlparse本身对Unicode的支持没问题,但如果你从文件读取时用了错误的编码,会导致解析时出现奇怪的UnicodeDecodeError。读取SQL文件我一般这样写:

with open('script.sql', 'r', encoding='utf-8') as f: sql = f.read()

如果遇到历史遗留的GBK文件,建议先转换编码再处理,不要在链路上混用两种编码。

7. 扩展思路:sqlparse能做的六件小事

最后分享几个我用sqlparse实现过的小工具思路,每个都是几十行代码就能搞定的。这些例子也许能激发你自己的玩法。

第一,SQL关键词高亮。解析出Token,根据Token类型给文本包上HTML标签或ANSI颜色码,就能做终端或网页里的高亮展示。

第二,SQL敏感信息打码。遍历Token,把字符串常量'xxx'统一替换为'***',用于日志脱敏。比正则更安全的是,它天然区分了字符串常量和注释。

第三,DDL反向生成JSON模型。解析CREATE TABLE语句,提取表名和字段列表,转换成JSON结构,用于自动生成文档或表结构对比。

第四,SQL拆分后逐条执行。在数据库迁移脚本中,用split拆出每条语句,再逐条执行并记录执行状态,比整批执行更容易定位报错位置。

第五,查询特征指纹。把SQL的常量值替换为?,关键字统一大写,空白归一,再计算哈希,用于同一语法树的SQL聚类分析。

第六,基于sqlparse的自定义Lint。检查SQL里是否存在某些危险模式,比如禁用SELECT *、禁止完全没有WHERE条件的DELETE。只需在解析结果里遍历TokenList即可。

最后再分享一点我自己的体会

用sqlparse这几年,我最大的感受是:解析类库的核心价值不在于它提供了一个完美的结果,而在于它替你承担了词法分析和基础语法分组的复杂度,把“从字符串到结构”这一层难度整个抹平了。但代价是你要理解它“非验证型解析器”的边界,知道它哪里能帮上忙、哪里会给你挖坑。正则表达式处理文本结构问题时,总有“管中窥豹”的感觉,而以树形结构去处理SQL,才真正拥有了全局视野。如果你正在做任何需要理解SQL语句内部结构的工作,花一个下午把sqlparse的Token类型、Identifier结构和walk遍历摸熟,绝对是一笔划算的投资。最后一个小技巧:调试解析结果时,别急着打印str(stmt),先调用stmt._pprint_tree()(是的,这个私有方法很好用),把Token树完整打印出来,你一眼就能看出解析分组和你预想的是否一致。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 14:41:26

校园AI Agent落地实践:RAG+MCP构建可交付服务闭环

1. 项目概述&#xff1a;这不是一个“玩具Demo”&#xff0c;而是一套可落地的校园服务闭环 你有没有遇到过这样的场景&#xff1a;新生入学季&#xff0c;教务处热线被打爆&#xff0c;90%的问题都是“这门课在哪个楼&#xff1f;”“实验课要带什么材料&#xff1f;”“重修流…

作者头像 李华
网站建设 2026/10/3 14:40:49

理工科论文降AI率攻略:术语不动,六个技法轻松过检测

理工科毕业季&#xff0c;最折磨人的不是改数据&#xff0c;也不是调格式&#xff0c;而是论文被AI检测系统盯上。我当年通宵写完全文&#xff0c;一查“疑似AI生成比例”直接飙到百分之四十多&#xff0c;最气人的是&#xff0c;我明明自己吭哧吭哧分析实验数据&#xff0c;公…

作者头像 李华
网站建设 2026/10/3 14:40:42

模糊神经网络Python实现:BP模糊分类源码与Iris数据集实战

简介&#xff1a;这份资源面向具备一定Python与神经网络基础、希望深入理解模糊逻辑与BP算法结合方式的学习者与开发者&#xff0c;提供BP模糊神经网络的完整Python实现方案&#xff0c;可用于分类预测等实验场景&#xff0c;帮助解决从理论到代码落地的衔接问题。压缩包共7个文…

作者头像 李华
网站建设 2026/10/3 14:40:03

Mac mini 搭建本地 AI 工作流:n8n 调度 + Ollama + Chroma 实战指南

1. 项目概述&#xff1a;为什么一台 Mac mini 能撑起整个家庭的 AI 基础设施&#xff1f;Mac mini 不是玩具&#xff0c;更不是“轻办公摆设”。过去三年我亲手部署过 7 套家庭级 AI 工作流&#xff0c;其中 5 套最终稳定运行在 M1/M2/M3 芯片的 Mac mini 上——它不是“勉强能…

作者头像 李华
网站建设 2026/10/3 14:39:47

Matlab联合优化调度:破解热电联产机组风电消纳难题

1. 项目背景与核心问题拆解1.1 热电联产机组为什么会“堵住”风电消纳做风电、火电联合调度的朋友应该都遇到过这个场景&#xff1a;冬季夜间&#xff0c;风电场满发&#xff0c;但全网用不完这些电&#xff0c;于是调度只能下令弃风。明明是可再生能源&#xff0c;白白扔掉&am…

作者头像 李华
网站建设 2026/10/3 14:38:47

STM32时钟配置卡死排查:SystemClock_Config与HSE起振失败全解析

如果你已经折腾STM32一段时间&#xff0c;大概率遇到过这个经典场景&#xff1a;新板子焊好、程序下载进去&#xff0c;LED不亮&#xff0c;串口没反应。打开调试器一暂停&#xff0c;发现程序死死地停在SystemClock_Config这个函数里出不来&#xff0c;有时候停在HAL_RCC_OscC…

作者头像 李华