SQL表级血缘解析实战:用Python与sqlglot构建数据血缘树
发布时间:2026/9/24 23:22:32来源:尧图网络
前段时间接到一个需求要把平台里几百张报表的数据来源链路理清楚。说白了就是给领导看的“这张表的数据到底是从哪来的”也就是数据血缘。我们这边的报表加工逻辑全写在SQL里而且用的是平台内部一套带方言扩展的SQL我们叫它 ZGLanguage。语法大体和标准SQL一致但多了很多自定义的Hint、变量占位符和脚本标签直接拿现成工具解析基本跑不通。当时我翻了十几个SQL文件一个文件动辄几百行CTE套CTE、子查询套子查询手工追踪一条链路就得花一个下午。后来我决定用 Python 写一个解析器把 ZGLanguage 的 SQL 文本自动转成表级血缘树。搞了差不多两周终于跑通了从单条 SQL 提取关系到全链路拼接树结构的完整流程。这篇文章就把这套方案的思路、踩过的坑和核心代码整理一下适合正在做数据治理、元数据管理或者数仓平台开发的同学参考。1. 项目背景与核心思路拆解1.1 ZGLanguage 到底是什么为什么不能直接读原文ZGLanguage 不是一门和 SQL 完全不同的语言它本质上就是 SQL 方言。方言这个词你应该不陌生就像 MySQL、PostgreSQL、SQL Server 虽然都支持标准 SQL但各自的写法细节不一样。ZGLanguage 也一样它保留了 SELECT、INSERT、CREATE TABLE 这些核心语法又在这个基础上加了变量替换、宏片段、自定义语义标签之类的东西。比如平台里常见的写法是-- 变量占位符 SELECT * FROM ${etl_db}.ods_order WHERE dt {biz_date};还有这种自定义标签-- 语义标签 -- target_table: dws_order_daily -- source_layer: ods INSERT INTO dws_order_daily SELECT ... FROM ods_order;如果直接拿通用 SQL 解析器去读这些占位符和标签大概率会直接报语法错误。所以第一步必须搞清楚 ZGLanguage 的语法边界把非标准的部分在进入正式解析之前做预处理或者扩展解析规则。我后面会用 sqlglot 来做这个事情它能自定义 tokenizer 和方言规则但核心思路是先把变量、标签转换成标准形式再把血缘关系提取出来。1.2 表级血缘树到底是什么表级血缘就是只关心“表”和“表”之间的数据流向不深入到字段级别。比如dws_order_daily这张表的数据来自ods_order、dim_user和dwd_pay这三张表那么血缘关系就是ods_order → dws_order_dailydim_user → dws_order_dailydwd_pay → dws_order_daily血缘树则是从某一张目标表出发向上游方向递归展开。每张表又会有自己的上游表一层一层追溯最终形成一棵树dws_order_daily ├── ods_order │ ├── crm_order │ └── app_order ├── dim_user │ └── ods_user └── dwd_pay └── ods_pay可能你会问数据血缘本质上是个有向无环图DAG因为一张表可能会有多个下游多个上游而且链路可能有交叉为什么最后要展示成树原因在于业务方看血缘的时候绝大多数场景是从一个结果表格出发去追溯上游链路树结构比图结构好理解得多。底层存储我用 DAG展示层再根据指定起点生成树结构两者各司其职。1.3 整体解析流程从一段SQL到一棵血缘树整个解析流程分为四个阶段预处理、AST解析、关系抽取、链路拼接。预处理阶段解决 ZGLanguage 自定义语法的问题比如把${etl_db}替换成实际库名把{biz_date}替换成日期常量。AST解析阶段使用 sqlglot 把 SQL 文本解析成抽象语法树。关系抽取阶段遍历语法树找到“写表”和“读表”的节点抽取出目标表和源表。链路拼接阶段把多条 SQL 抽取到的关系放到一张有向图里再根据指定表名生成血缘树。这个结构一开始我并没有完全想清楚第一版直接用正则表达式提取后来发现只要 SQL 一复杂就崩。后来换成 AST 方案之后整个流程稳健了很多。这四个阶段是串行的每个阶段都有独立的函数调试起来很方便。2. 核心技术方案选型解析SQL的三种路线2.1 正则表达式提取能用但活不过生产环境最开始图省事想用正则直接匹配关键字。比如找出所有FROM和JOIN后面的关键字再加上INSERT INTO后面的表名。简单场景确实管用比如INSERT INTO dws_order_daily SELECT * FROM ods_order o JOIN dim_user u ON o.user_id u.id;用正则抓FROM\s(\w)和JOIN\s(\w)就能拿到ods_order和dim_user。但是实际生产环境里的 SQL 远比这复杂子查询、WITH 子句、视图嵌套、同名列这些会让正则表达式变得极其脆弱。比如这条 SQLINSERT INTO result_table SELECT * FROM ( SELECT * FROM base_table ) t;FROM后面跟的是一个子查询正则直接拿到的是(根本识别不出真实表base_table。再比如 CTE 的名称和真实表重名正则分不清谁是临时表谁是物理表。正则方案只适合做语法高亮、简单文本统计做血缘解析在复杂场景下基本等于不可用。这个坑我踩得挺快写了大约一百行正则之后就果断止损了。2.2 sqlparse能切开单词但拿不到语法关系第二个方案是尝试用sqlparse。这是一个很流行的 Python 库很多 SQL 格式化工具底层就用它。它能做的是把 SQL 拆成 token 流也能把语句分类比如识别这是一条 INSERT 还是一条 SELECT但它不提供真正的抽象语法树。为什么 AST 很重要因为血缘解析本质上需要回答一个问题“这个 FROM 里的表作用域是当前查询还是某个子查询”。sqlparse给你的是扁平的 token 序列你得自己维护括号深度、关键字状态相当于自己写一个简版解析器。对于标准 SQL 都不好搞更别提 ZGLanguage 的变量占位符了。所以sqlparse在我这里只用来做 SQL 文本清洗和切分多语句脚本不做核心血缘抽取。2.3 sqlglot支持AST解析和多方言的完整方案最终我选的是sqlglot。它是一个纯 Python 实现的 SQL 解析器支持 MySQL、PostgreSQL、Hive、Spark SQL 等二十多种方言最关键的是它能把 SQL 解析成完整的 AST并且 AST 节点是可以遍历和修改的。sqlglot 的 AST 模型把一条 INSERT SQL 解析成这样的结构Insert ├── this: Table identifier (目标表) ├── expression: Select │ ├── from: From │ │ └── this: Table (源表) │ └── joins: [ │ Join - Table │ Join - Table │ ]有了这棵树血缘提取就变成了一个明确定义的遍历任务找到整个语句的写表节点再找到所有读表节点排除掉临时表和 CTE 名称剩下的就是直接血缘关系。这比正则和 sqlparse 高了不止一个量级。2.4 方案对比为什么最终选了AST路线这几种方案我实际都跑过一遍列一个对比表给你参考方案解析能力方言适配子查询/CTE处理适用场景正则表达式弱需要自己写不支持简单SQL抽取关键字sqlparse中等一般不支持分词、格式化、多语句切分sqlglot AST强优秀支持血缘解析、SQL改写、方言转换ANTLR 自定义语法最强需要完全自己实现支持复杂方言、深度二次开发ANTLR 是另一个路线适合对语法解析极其苛刻的场景。比如你要重新实现一门数据库引擎或者要解析 ZGLanguage 里那些特别复杂的自定义语法那就值得上 ANTLR。但对大多数数据团队来说sqlglot 已经覆盖了 90% 的需求而且实现成本低很多。我当时评估了一下如果自研语法文件至少要花两周时间去定义词法和语法规则还得维护一个完整的 parse tree visitor以我的精力投入来说不划算。最终我选择 sqlglot 作为底座针对 ZGLanguage 的少量自定义语法做预处理效果非常理想。3. 实操过程完整实现SQL表级血缘提取3.1 环境准备与依赖安装整个工具只需要三个核心依赖sqlglot负责 SQL 解析networkx负责血缘图存储和树生成pandas用来做批量结果展现。安装命令很简单pip install sqlglot networkx pandas如果你是 Python 3.9 以上的环境这几个库的兼容性都很稳。我开发的时候用的是 Python 3.10sqlglot 版本是 20.xAPI 存在一些更新所以代码里我会用当前版本兼容的姿势来写。这里要提醒一句sqlglot 的 API 演进速度比较快早期版本parse_one返回的节点类型和现在略有差异。如果你发现某个节点类找不到先去官方文档确认你装的版本号。我在写代码的日子里至少遇到三次因为升级小版本导致exp.Insert的属性名变化。3.2 核心实现解析单条SQL中的表关系先写最底层的函数输入一条 ZGLanguage SQL 文本输出一个血缘关系元组(来源表列表, 目标表)。import sqlglot from sqlglot import exp def preprocess_zg_sql(sql_text: str) - str: ZGLanguage 预处理把变量占位符替换为可解析的常量 # 更完善的替换逻辑需要结合平台元数据这里先做最小处理 sql_text sql_text.replace({biz_date}, 2024-01-01) # 如果 ${etl_db}. 这种库名前缀替换为常见的 ods / dwd 库名 sql_text sql_text.replace(${etl_db}., ods.) return sql_text def extract_table_lineage(sql_text: str): 从单条 SQL 中提取表级血缘关系返回 (sources, target) sql_text preprocess_zg_sql(sql_text) ast sqlglot.parse_one(sql_text, readmysql) # 1. 识别目标表INSERT INTO / CREATE TABLE AS / CREATE VIEW target_table None if isinstance(ast, exp.Insert): target_table ast.this elif isinstance(ast, exp.Create): target_table ast.this if target_table is None: return None, None # 2. 遍历所有表引用 source_tables [] for table in ast.walk(exp.Table): table_name table.name # 跳过目标表自身处理 INSERT INTO ... SELECT ... FROM 目标表 的极端情况 if table target_table: continue # 跳过 CTE 别名 if _is_cte_or_subquery_alias(table): continue source_tables.append(table_name) return list(set(source_tables)), target_table.name这段代码里有两个辅助函数_is_cte_or_subquery_alias用来判断一个 Table 节点到底是一个真实物理表还是一个 CTE 名称或子查询别名。sqlglot 是支持表别名识别的Table节点如果来源于 JOIN 或 FROM还会包含alias属性但真正的表名还是name属性。判断 CTE 的要点是先收集当前 AST 里所有 WITH 子句定义的 CTE 名称集合然后看当前表节点在不在这个集合里。3.3 处理INSERT INTO、CREATE TABLE AS SELECT和视图实际平台里的加工脚本主要有三种写法这三种都是血缘解析的重点场景。第一种是 INSERT INTO 写入INSERT INTO dws_order_daily SELECT o.order_id, u.user_name, SUM(o.amount) AS total_amount FROM ods_order o LEFT JOIN dim_user u ON o.user_id u.id GROUP BY o.order_id, u.user_name;这对应代码里的exp.Insert节点。ast.this是目标表ast.expression是 SELECT 语句。遍历ast.expression下的所有exp.Table节点就能拿到ods_order和dim_user。第二种是 CREATE TABLE AS SELECT简写 CTASCREATE TABLE dws_order_daily AS SELECT * FROM ods_order;这种在 ZGLanguage 里也常用对应exp.Create节点。ast.this是目标表SELECT 部分也能通过遍历拿到。第三种是视图定义CREATE VIEW v_order_daily AS SELECT * FROM ods_order;注意视图本身不是物理表它是逻辑表。在血缘链路中v_order_daily可以继续作为上游表被其他 SQL 引用。所以我处理视图的方式是如果遇到 CREATE VIEW也提取(ods_order, v_order_daily)这条关系同时打上一个类型标记view表示目标表是一个视图。后续链路拼接时视图可以继续参与血缘追踪这样从最终报表反查时能穿透视图找到最底层的物理表。3.4 从多条SQL构建血缘树用 networkx 拼装 DAG单条 SQL 的血缘关系只是图里的一条边要形成血缘树必须把平台里的所有 SQL 都跑一遍把这些边全部加到一张有向图里。我用的是 networkx 的DiGraph。import networkx as nx from pathlib import Path def build_full_lineage_graph(sql_files: list) - nx.DiGraph: graph nx.DiGraph() for sql_file in sql_files: sql_text Path(sql_file).read_text(encodingutf-8) # 一个文件里可能有多条 SQL需要先做语句块切分 statements split_sql_statements(sql_text) for statement in statements: sources, target extract_table_lineage(statement) if target is None or not sources: continue for source in sources: graph.add_edge(source, target, sql_filesql_file) return graphsplit_sql_statements是一个小工具函数因为 ZGLanguage 脚本文件里可能一个文件包含多条 INSERT 语句中间用分号分隔。这里我直接用 sqlglot 的parse方法复数版本它会自动按语句拆分返回一个 AST 列表比手动 split 靠谱得多。建图之后生成血缘树就是一个递归函数。从目标表出发找到它的所有直接上游节点再递归往上找def build_lineage_tree(graph: nx.DiGraph, target_table: str, visitedNone): if visited is None: visited set() if target_table in visited: return None # 防止环导致死循环 visited.add(target_table) predecessors list(graph.predecessors(target_table)) children [build_lineage_tree(graph, p, visited) for p in predecessors] return { table: target_table, children: [child for child in children if child is not None] }这里有一个小坑如果数据流图里存在循环依赖比如 A 依赖 BB 又依赖 A这在不规范加工脚本里会存在不处理visited就会无限递归。我加了visited集合之后遇到环会直接截断避免把内存跑爆。实际遇到环的次数不多但一旦遇到就是生产事故级别的这个保护必须有。输出血缘树的时候我通常还会附加上一些表的基础元数据比如表中文名、所属数仓分层。做法是在目标表和上游表节点上追加属性graph.nodes[table_name][layer] detect_layer(table_name) graph.nodes[table_name][comment] get_table_comment(table_name)这样最终 JSON 输出可以直接对接前端的血缘可视化页面。3.5 适配ZGLanguage自定义语法的兼容策略ZGLanguage 里最麻烦的是平台自己造的“伪 SQL”写法sqlglot 直接解析会报错。我总结下来主要有三类变量占位符、宏片段、自定义 Hint。变量占位符的处理方式最简单就是前面代码里的preprocess_zg_sql。把${xxx}替换成真实表名或库名把{biz_date}替换成日期字符串。注意替换顺序先替换表名变量再替换日期变量避免日期字符串被误当成表名。宏片段的处理稍微复杂一点。比如 ZGLanguage 里可能会定义类似这样的宏{% macro select_base_fields(entity_name) %} SELECT * FROM {{ entity_name }} {% endmacro %}在解析 SQL 之前得先用 Jinja2 模板引擎把宏展开渲染成普通 SQL 再进入血缘解析。这一步要在源头搞定否则无论后端解析能力多强都没用。自定义 Hint 则要简单看待。像-- target_table: xxx这种注释其实是语义提示不是真正的 SQL 语法直接当作注释保留就行。但如果 Hint 是/* parallel(4) */这种方式sqlglot 在解析标准 SQL 时有可能会报错我遇到这种情况的处理是先用正则把这些 Hint 删除。这步删除不影响表血缘结果因为 Hint 里不会出现物理表名。我自己实际搭建的预处理流程是原始SQL - Jinja宏展开 - 变量占位符替换 - Hint清洗 - 标准SQL - sqlglot解析这个流程跑到现在解析几千条线上的常规加工 SQL成功率在 99% 以上剩下的 1% 基本都是因为 SQL 脚本本身报错无关血缘解析。4. 常见问题与排查技巧实录4.1 WITH子句和CTE别名的坑WITH 子句CTE是血缘解析里最容易出错的地方。看下面这条 SQLWITH tmp_order AS ( SELECT * FROM ods_order WHERE dt 2024-01-01 ) INSERT INTO dws_order_daily SELECT * FROM tmp_order;很多没经验的人会直接把tmp_order也当成一张物理表加进上游表列表。实际上tmp_order只是一个临时视图不是真实的数据来源真实来源是ods_order。我的处理办法是在 AST 遍历之前先收集所有 CTE 的名称集合cte_names set() for with_node in ast.walk(exp.CTE): cte_names.add(with_node.alias)然后在遍历exp.Table的时候如果当前表节点的名字在这个集合中就跳过它。注意 ZGLanguage 的 CTE 还可能带自己的方言前缀比如WITH {var}_tmp AS (...)这要在预处理阶段先把它展开。还有一种特殊场景是递归 CTE比如组织架构表经常用递归把层级扁平化WITH RECURSIVE org_tree AS ( SELECT * FROM org WHERE id 1 UNION ALL SELECT * FROM org o JOIN org_tree t ON o.parent_id t.id ) INSERT INTO flat_org SELECT * FROM org_tree;这里org_tree既是 CTE 又会引用自身处理时同样跳过org_tree上游只保留真实的org表血缘关系就可以正确表达。4.2 子查询别名和同名列的识别问题SQL 里子查询很常见INSERT INTO result_table SELECT t.id, t.name FROM ( SELECT id, name FROM base_table ) t;这种情况下t是子查询别名base_table才是物理表。判断依据是FROM子句的this节点如果不是exp.Table类型而是一个子查询exp.Subquery或exp.Select那么这个层级下没有直接表引用需要递归深入到子查询内部去找表。子查询里涉及的同名列反而不会给表血缘带来太大问题因为表血缘关心的是“哪些表被读了”不关心“哪些列被读了”。真正需要注意的是一个子查询里同时引用了多张表而且这些表之间有列名冲突这是字段级血缘的难点表级血缘不受影响。4.3 性能优化批量解析上千个SQL文件一开始跑全量解析几千个文件逐个调用 sqlglot耗时大概在十几分钟。其实单条 SQL 解析也就几十毫秒主要瓶颈在文件读取和 ZGLanguage 预处理模板渲染上。优化手段有三个第一个是使用多进程。Python 的 GIL 会让多线程在 CPU 密集场景下失效但 sqlglot 解析是纯 CPU 计算用concurrent.futures.ProcessPoolExecutor可以把利用率拉满。我实测在 8 核机器上速度能提升到原来的 5 到 6 倍。第二个是复用sqlglot的方言实例和 tokenizer。sqlglot 在重复调用parse_one时会缓存一些词法信息但这个缓存不是跨调用复用的。如果你的方言扩展很多建议只初始化一次Dialect对象然后重复使用。第三个是增加预处理结果的缓存。因为有些公共 SQL 片段被嵌入在很多脚本里模板渲染那一层很耗时间。我加了一个基于文件 hash 的简单缓存第二次跑全量解析时直接命中缓存大幅节省时间。4.4 血缘结果校验如何证明你解析得准做完解析器最大的挑战是“怎么证明结果是对的”。我当时的做法是准备了一个 100 条 SQL 的测试集覆盖了简单查询、多层子查询、CTE、JOIN、INSERT OVERWRITE、CREATE VIEW 等各种场景。每条 SQL 都人工确认了真实血缘关系做好标注然后写一个断言脚本去对比解析结果。测试框架大概长这样test_cases [ { sql: INSERT INTO a SELECT * FROM b JOIN c ON b.id c.id;, expect_sources: [b, c], expect_target: a, }, # ... 更多测试用例 ] def test_lineage(): for case in test_cases: sources, target extract_table_lineage(case[sql]) assert target case[expect_target] assert set(sources) set(case[expect_sources])我还加了“反向测试”就是故意构造一些错误解析的用例确保解析器不会把它们误判为有血缘关系。这种测试集对后续维护非常重要每修一个 bug 就加一条对应用例避免回归。常见问题里我还想单独列一个速查表问题现象可能原因解决办法解析报错 Unknown tokenZGLanguage 自定义语法未预处理在预处理阶段替换变量/Hint/宏血缘多出来一张临时表CTE 别名被误判为物理表收集 CTE 名称集合遍历时跳过血缘少了上游表JOIN 里的表名被正则替换掉了改用 AST 遍历不要用正则提取表名循环依赖导致树生成异常加工脚本存在环networkx 遍历时记录 visited目标表无法识别SQL 使用了 MERGE 或 UPDATE 语法扩展对 exp.Merge 的处理逻辑5. 进阶从表级血缘到字段级血缘5.1 为什么要做字段级血缘表级血缘解决的是“数据从哪张表来”的问题但业务方经常会继续追问“这个订单金额字段到底是怎么算出来的来自上游的哪一个字段” 这时候表级血缘就无能为力了。字段级血缘的解析难度比表级高出不少核心是要在 SELECT 投影中追踪每个输出列的来源。比如INSERT INTO dws_order_daily (order_id, user_id, amount) SELECT o.id, u.id, o.pay_amount * 0.9 FROM ods_order o JOIN dim_user u ON o.user_id u.id;字段级血缘要能识别出dws_order_daily.amount来自ods_order.pay_amount经过了一个表达式变换。sqlglot 的 AST 中Select节点的expressions列表对应每个输出列每个表达式再往上逐步推导来源列。这个推导过程比较复杂因为中间会有子查询、JOIN、聚合函数、表达式运算。5.2 字段级血缘的核心实现思路我的做法是在表级血缘解析的基础上对每个输出列构建一个“列表达式树”。遍历 SELECT 投影中的每个exp.Alias或exp.Column然后沿着 AST 递归搜索每个列的来源。伪代码如下def trace_column(source_col, ast, lineage_map): # 找到 source_col 在 SQL 中的定义位置 # 如果是直接 select 某表字段则记录 (表名, 字段名) # 如果是表达式运算则继续递归表达式的子节点 # 如果是函数调用sum/count/min则记录来源字段和聚合类型 ...字段级血缘对表达式处理要求很高比如o.pay_amount * 0.9血缘结果既要有ods_order.pay_amount也要记录这是一次“乘法变换”。我一般输出的时候会带上 transform 信息方便业务方理解字段加工口径。5.3 字段级解析需要注意的边界字段级血缘比表级更容易误判。最典型的场景是SELECT *你根本不知道展开后具体是哪些字段。方案是先解析出目标表的元数据目标表的字段结构再去匹配源表的字段。如果目标表字段顺序变了就很容易错位。另一个坑是 CASE WHEN 表达式。一个输出列可能来自多个源列的分支字段血缘会是一对多关系。我建议这部分不要试图用纯解析器解决而是要结合数仓的元数据信息比如字段注释、ETL 映射关系做一个引导式的血缘推断。如果你已经做出了表级血缘字段级血缘建议分两个阶段推进先做“同名字段直接匹配”的简化版把 60% 的常规场景覆盖掉再逐步补充表达式解析逻辑。一上来就追求 100% 准确投入产出比会很差。6. 工程落地与实际效果6.1 从手工追踪到秒级自动呈现血缘解析工具上线之后最直观的变化是效率。之前我手工理一条链路少则一小时多则半天。现在把报表表名传进去血缘树在几秒钟内就能生成还能输出 JSON 给前端做可视化。原本需要跟业务方反复解释“这个数据是从哪来”的沟通成本也大幅降低直接甩一张血缘图过去清清楚楚。落地形态上我把它做成了三个部分一个 Python 包负责底层的 SQL 解析和血缘抽取一个命令行工具跑全量任务并输出血缘 JSON 文件一个简单的前端页面读 JSON 渲染成树形图。整体架构很轻不需要引入重量级调度系统团队内部用起来也顺手。6.2 血缘分析在数据治理里的实际价值血缘不仅仅是为了回答“数据从哪来”。在数据治理里血缘有两个真正的杀手级应用影响分析和数据质量追溯。影响分析是反向的。比如上游某张ods_order表结构要大改有了血缘 DAG我可以一键查出所有直接或间接依赖它的下游表提前评估改动影响范围。这个在平台做升级时特别关键能避免“改了一张表炸了十张报表”的线上事故。数据质量追溯则是正向的。报表数据出现异常顺着血缘树一级一级往下排查很快就能定位到出问题的源头表以及对应的加工环节不是像以前那样全链路手动去比对。现在团队里遇到数据问题第一个动作就是打开血缘系统查链路定位效率提升非常明显。6.3 后续可以扩展的方向表级血缘跑通之后我计划做几个扩展。第一个是把 ZGLanguage 的解析能力封装成 HTTP 服务方便其他组的数据开发在写 SQL 的同时实时预览血缘关系而不是等到任务上线后才统一解析。第二个是接入任务调度系统在每次 ETL 任务跑完之后自动更新血缘图让血缘关系和线上任务保持同步。第三件事是构建“表热度分析”。有了血缘 DAG从图中节点的出入度就能统计出哪些表是核心资产、哪些表是孤岛。这种分析对数据资产管理有直接价值能辅助判断哪些低频表适合归档冷存储哪些核心表需要加强备份和监控。这些都不需要另起炉灶都是基于本套血缘解析能力做的衍生应用。最后说一点个人体会。做血缘解析这类工具技术上并不复杂真正复杂的是对业务方言的理解和持续的场景适配。我在写这套系统的过程中踩了不少坑从正则到 sqlparse 再到 sqlglot每一次方案推翻其实都是对问题本身的一层更深理解。如果你也在尝试做类似的事情我的建议很简单一开始就走 AST 路线不要贪图正则的“看起来不错”而走弯路同时一定要建立测试集血缘解析的改动非常容易回归没有用例保护的代码走不远。另外就是不要想着一口气做到字段级表级血缘本身就有很大的实际价值先把这一层吃透再逐步扩展节奏会舒服很多。
网站建设高端定制企业官网