应优先使用sqlparse等语法解析器而非正则提取SELECT...FROM,因其能正确处理子查询、注释、括号嵌套、hint及字段别名等复杂结构;纯正则易因贪心匹配、注释干扰或嵌套层级误判而失败。
直接用 re.search(r"SELECT.*?FROM", sql, re.IGNORECASE | re.DOTALL) 很容易截断——比如遇到子查询里的 SELECT、注释里的 SELECT,或者 FROM 被换行/括号包着,就会提前收尾或漏掉字段列表。
更稳妥的做法是:先去掉块注释(/*...*/)和行注释(--...),再找最外层的 SELECT 开头、直到对应层级的 FROM 结束。实际中推荐分两步:
sqlparse 库解析(Python):parsed = sqlparse.parse(sql)[0],然后遍历 parsed.tokens 找到第一个类型为 sqlparse.tokens.Keyword 且值为 "SELECT" 的节点,再向后收集直到遇到同级的 "FROM"
SELECT 后,逐字符扫描,遇到 ( 加1、) 减1,跳过引号内内容,等括号深度归零且下一个单词是 FROM(忽略大小写和空白)时截断因为“核心 SELECT 部分”通常指最终输出结果的那层,不是 CTE 里的 WITH 子句、也不是 IN (SELECT ...) 里的内层查询。错把子查询当主干,会导致字段名解析失败、别名丢失、甚至漏掉 JOIN 条件。
判断依据不是位置靠前,而是语法层级:主查询的 SELECT 必然不在任何括号内(括号深度为 0),且不被 WITH、EXISTS、IN、WHERE 等关键字直接包裹。实操建议:
sqlparse 的 flatten 和 is_group 判断 token 是否属于顶层语句WITH 或左括号前记录当前深度;只有深度回到 0 且前一个 token 不是 "WITH"、"IN"、"EXISTS" 时,才接受这个 SELECT
像 SELECT COUNT(*) AS cnt, UPPER(name) AS uname, CASE WHEN x>0 THEN 'yes' ELSE 'no' END AS flag FROM t 这种,用简单正则切字段会把 CASE 中的 END 当成整个语句结尾,或者把 AS 后面的字符串误判为新字段起始。
真正能稳定拆字段的,是基于语法树的遍历。例如用 sqlparse 后:
for item in parsed.tokens: if item.is_group and item.tokens and item.tokens[0].ttype is sqlparse.tokens.Keyword and item.tokens[0].value.upper() == "SELECT": # 取该 group 下所有逗号分隔的子项,再对每个子项 .get_name() 或 .to_unicode()
如果硬要正则兜底,至少把字段部分单独拎出来再处理:先截出 SELECT ... FROM 整段,再按逗号分割,对每段用括号计数跳过内部逗号(比如 COALESCE(a,b), c 里第一个逗号不能切)。
MySQL 和 Oracle 支持优化器提示(hint),写在 SELECT 后、字段前,形如 SELECT /*+ ... */ col FROM t。这种提示不算字段,但属于 SELECT 子句合法组成部分。直接从 SELECT 后开始截,会把 hint 当成字段一部分;跳过它又可能漏掉关键执行意图。
处理建议:
r"/*+[^*]**+(?:[^/*][^*]**+)*/" 匹配并暂存,再从 SELECT 后剔除它,再取字段/*+*/、/*!40101 ... */ 等),但需注意 MySQL 的条件注释会影响兼容性判断sqlparse 默认不识别 hint,需手动加 sqlparse.keywords.KEYWORDS_COMMON["/*+"] = sqlparse.tokens.Keyword 才能正确分组没有银弹。越长的 SQL,越要放弃纯文本处理;哪怕只是临时脚本,也值得花三分钟装个 sqlparse ——否则调试正则的时间,够你重跑五次查询了。