Python实现sql列级血缘解析
·
数据治理工作中需要明确数据库各个层级间的字段对应关系,尝试用python sqllineage包处理获得列级别的血缘关系
代码如下:
# -*- coding: utf-8 -*-
# author: zs
from sqllineage.runner import LineageRunner
import pandas as pd
from datetime import datetime
import os
import re
import sqlparse
def write_lineage_to_excel(lineage_list, excel_path, sql_source=None, sheet_name='Sheet1'):
"""
将列级血缘列表写入 Excel 文件(增量追加)
参数:
lineage_list: sqllineage 返回的列级血缘列表(get_column_lineage() 的结果)
excel_path: Excel 文件路径
sql_source: 可选,标识 SQL 来源(如文件名或 SQL 摘要),将作为一列记录
sheet_name: 工作表名称,默认 'Sheet1'
"""
# 构建新数据行列表
new_rows = []
for lineage in lineage_list:
upstream_cols = lineage[:-1] # 上游列(可能多个)
downstream_col = lineage[-1] # 下游列
# 将多个上游列合并为一个字符串,用分号分隔
upstream_str = '; '.join([str(col) for col in upstream_cols])
downstream_str = str(downstream_col)
new_rows.append({
'source_columns': upstream_str,
'target_column': downstream_str,
'sql_source': sql_source if sql_source else '',
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
if not new_rows:
print("没有血缘数据可写入")
return
new_df = pd.DataFrame(new_rows)
# 判断文件是否存在,执行增量写入或新建
if os.path.isfile(excel_path):
# 文件已存在:读取原有数据,追加新数据
try:
existing_df = pd.read_excel(excel_path, sheet_name=sheet_name, engine='openpyxl')
# 合并新旧数据(保留所有行,如需去重可添加 drop_duplicates)
combined_df = pd.concat([existing_df, new_df], ignore_index=True)
# 可选:根据指定列去重(例如去除完全相同的血缘记录)
combined_df.drop_duplicates(subset=['source_columns','target_column','sql_source'], keep='last', inplace=True)
except Exception as e:
print(f"读取现有 Excel 文件失败,将新建文件:{e}")
combined_df = new_df
else:
# 文件不存在:直接使用新数据
combined_df = new_df
# 写入 Excel(使用 openpyxl 引擎支持 .xlsx)
with pd.ExcelWriter(excel_path, engine='openpyxl', mode='w') as writer:
combined_df.to_excel(writer, sheet_name=sheet_name, index=False)
print(f"血缘结果已写入 {excel_path},共 {len(combined_df)} 行(新增 {len(new_rows)} 行)")
def remove_sql_comments_sqlparse(sql):
"""
使用 sqlparse 去除 SQL 中的注释,保留字符串内容。
需要安装:pip install sqlparse
"""
# 解析 SQL 并格式化(strip_comments=True 会移除注释)
formatted = sqlparse.format(sql, strip_comments=True)
# 可选:去除多余空行
return '\n'.join(line for line in formatted.splitlines() if line.strip())
# ========== 主程序示例 ==========
if __name__ == "__main__":
# 待解析的 SQL(可改为读取文件)
sql_text_source = """
INSERT INTO dws.sales_summary (product_id, total_amount, sale_date)
SELECT
product_id,
SUM(amount) AS total_amount,
DATE(order_time) AS sale_date
FROM ods.orders
WHERE status = 'completed'
GROUP BY product_id, DATE(order_time);
"""
# 写入 Excel 路径/文件名
excel_file = r"D:\JetBrains\PyCharm_project\column_lineage.xlsx"
# 处理读取的SQL去掉注释
sql_text = remove_sql_comments_sqlparse(sql_text_source)
# 正则表达式模式:
# insert into - 字面匹配,不区分大小写(通过 re.IGNORECASE)
# .*? - 非贪婪匹配任意字符(包括换行,因为使用了 re.DOTALL),直到第一个分号
# ; - 字面分号
pattern = r'insert into.*?;'
# 使用 re.findall 查找所有匹配
# flags:
# re.IGNORECASE - 忽略大小写,匹配 INSERT INTO、Insert Into 等
# re.DOTALL - 让点号(.)匹配包括换行符在内的任意字符,实现跨行匹配
matches = re.findall(pattern, sql_text, flags=re.IGNORECASE | re.DOTALL)
for sql in matches:
# 1. 解析列级血缘
# 可选的 dialect 参数指定 SQL 方言,如 'hive', 'sparksql', 'mysql' 等,默认为 'ansi'
runner = LineageRunner(sql, dialect='ansi')
column_lineages = runner.get_column_lineage()
# 2. 打印到控制台
print("列级血缘解析结果:")
for lineage in column_lineages:
upstream = ', '.join([str(col) for col in lineage[:-1]])
downstream = str(lineage[-1])
print(f"下游列: {downstream} <-- 上游列: {upstream}")
# 3. 写入 Excel(增量追加)
# sql_source 可传入 SQL 文件路径或标识,这里用固定字符串示例
write_lineage_to_excel(column_lineages, excel_file, sql_source="example_query.sql")
print("处理完成")
简单测试后效果看着还是可以的
excel结果:

之后进行复杂一点的sql测试发现一下问题:
1.对于嵌套2层以上子查询的表,源头表名没有拿到只拿到了表别名
2.对于子查询是select * 的写法无法获取到表名只拿到了表别名
3.对于只拿到表别名的字段还需要返回sql语句确认是哪张表,但是在复杂嵌套sql里可能有多个相同的别名
注意:主要的解析逻辑只能一次处理一段insert into逻辑 所以本逻辑里先把SQL文件读取后用正则提取每一段insert into逻辑(提取insert into逻辑是以insert into为开头;为结尾的,如果SQL不加;会获取不到)
添加读取指定文件夹内的所有文件遍历处理
# -*- coding: utf-8 -*-
# author: zs
from sqllineage.runner import LineageRunner
import pandas as pd
from datetime import datetime
import os
import re
import sqlparse
from pathlib import Path
def write_lineage_to_excel(lineage_list, excel_path, sql_source=None, sheet_name='Sheet1'):
"""
将列级血缘列表写入 Excel 文件(增量追加)
参数:
lineage_list: sqllineage 返回的列级血缘列表(get_column_lineage() 的结果)
excel_path: Excel 文件路径
sql_source: 可选,标识 SQL 来源(如文件名或 SQL 摘要),将作为一列记录
sheet_name: 工作表名称,默认 'Sheet1'
"""
# 构建新数据行列表
new_rows = []
for lineage in lineage_list:
upstream_cols = lineage[:-1] # 上游列(可能多个)
downstream_col = lineage[-1] # 下游列
# 将多个上游列合并为一个字符串,用分号分隔
upstream_str = '; '.join([str(col) for col in upstream_cols])
downstream_str = str(downstream_col)
new_rows.append({
'source_columns': upstream_str,
'target_column': downstream_str,
'sql_source': sql_source if sql_source else '',
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
if not new_rows:
print("没有血缘数据可写入")
return
new_df = pd.DataFrame(new_rows)
# 判断文件是否存在,执行增量写入或新建
if os.path.isfile(excel_path):
# 文件已存在:读取原有数据,追加新数据
try:
existing_df = pd.read_excel(excel_path, sheet_name=sheet_name, engine='openpyxl')
# 合并新旧数据(保留所有行,如需去重可添加 drop_duplicates)
combined_df = pd.concat([existing_df, new_df], ignore_index=True)
# 可选:根据指定列去重(例如去除完全相同的血缘记录)
combined_df.drop_duplicates(subset=['source_columns','target_column','sql_source'], keep='last', inplace=True)
except Exception as e:
print(f"读取现有 Excel 文件失败,将新建文件:{e}")
combined_df = new_df
else:
# 文件不存在:直接使用新数据
combined_df = new_df
# 写入 Excel(使用 openpyxl 引擎支持 .xlsx)
with pd.ExcelWriter(excel_path, engine='openpyxl', mode='w') as writer:
combined_df.to_excel(writer, sheet_name=sheet_name, index=False)
print(f"血缘结果已写入 {excel_path},共 {len(combined_df)} 行(新增 {len(new_rows)} 行)")
def remove_sql_comments_sqlparse(sql):
"""
使用 sqlparse 去除 SQL 中的注释,保留字符串内容。
需要安装:pip install sqlparse
"""
# 解析 SQL 并格式化(strip_comments=True 会移除注释)
formatted = sqlparse.format(sql, strip_comments=True)
# 可选:去除多余空行
return '\n'.join(line for line in formatted.splitlines() if line.strip())
def read_all_files_in_directory(directory_path, file_extension=None, encoding='utf-8'):
"""
读取指定目录下所有文件的内容(可选按扩展名过滤)
参数:
directory_path : str 或 Path - 要读取的目录路径
file_extension : str, 可选 - 如果提供,只读取该扩展名的文件(如 '.txt')
encoding : str - 文件编码,默认 utf-8
返回:
一个字典,键为文件名,值为文件内容(字符串)
"""
# 将路径转换为Path对象以便操作
base_path = Path(directory_path)
# 检查目录是否存在
if not base_path.exists():
raise FileNotFoundError(f"目录不存在: {directory_path}")
if not base_path.is_dir():
raise NotADirectoryError(f"路径不是目录: {directory_path}")
# 存储文件名和内容的字典
files_content = {}
# 遍历目录中的条目
for item in base_path.iterdir():
# 只处理文件,跳过子目录
if item.is_file():
# 如果指定了扩展名,检查是否匹配
if file_extension and item.suffix.lower() != file_extension.lower():
continue
try:
# 方法1:一次性读取整个文件内容(适合小文件)
with open(item, 'r', encoding=encoding) as f:
content = f.read()
# 方法2:如果需要逐行处理,可以使用以下循环(注释掉上面,改用下面)
# content_lines = []
# with open(item, 'r', encoding=encoding) as f:
# for line in f:
# # 对每一行进行处理(例如去除首尾空白)
# line = line.strip()
# if line: # 只保留非空行
# content_lines.append(line)
# content = '\n'.join(content_lines)
files_content[item.name] = content
print(f"成功读取: {item.name}")
except Exception as e:
print(f"读取文件 {item.name} 时出错: {e}")
files_content[item.name] = None # 或根据需要跳过
return files_content
# ========== 主程序示例 ==========
if __name__ == "__main__":
directory = r"D:\JetBrains\PyCharm_project\pythonProject\sql_texts" # 替换为你的目录路径
# 读取所有文件 (不限制扩展名)
all_files = read_all_files_in_directory(directory)
# 可选:只读取 .txt 文件
# txt_files = read_all_files_in_directory(directory, file_extension='.txt')
# print(f"读取了 {len(txt_files)} 个 .txt 文件")
# 写入 Excel 路径/文件名
excel_file = r"D:\JetBrains\PyCharm_project\column_lineage.xlsx"
for filename, content in all_files.items():
if content is not None:
print(f"\n--- {filename} ---")
# 处理读取的SQL去掉注释
sql_text = remove_sql_comments_sqlparse(content)
# 正则表达式模式:
# insert into - 字面匹配,不区分大小写(通过 re.IGNORECASE)
# .*? - 非贪婪匹配任意字符(包括换行,因为使用了 re.DOTALL),直到第一个分号
# ; - 字面分号
pattern = r'insert into.*?;'
# 使用 re.findall 查找所有匹配
# flags:
# re.IGNORECASE - 忽略大小写,匹配 INSERT INTO、Insert Into 等
# re.DOTALL - 让点号(.)匹配包括换行符在内的任意字符,实现跨行匹配
matches = re.findall(pattern, sql_text, flags=re.IGNORECASE | re.DOTALL)
for sql in matches:
# 1. 解析列级血缘
# 可选的 dialect 参数指定 SQL 方言,如 'hive', 'sparksql', 'mysql' 等,默认为 'ansi'
runner = LineageRunner(sql, dialect='ansi')
column_lineages = runner.get_column_lineage()
# 2. 打印到控制台
print("列级血缘解析结果:")
for lineage in column_lineages:
upstream = ', '.join([str(col) for col in lineage[:-1]])
downstream = str(lineage[-1])
print(f"下游列: {downstream} <-- 上游列: {upstream}")
# 3. 写入 Excel(增量追加)
# sql_source 可传入 SQL 文件路径或标识,这里用固定字符串示例
write_lineage_to_excel(column_lineages, excel_file, filename)
else:
print(f"\n--- {filename} 读取失败 ---")
print("处理完成")
路径D:\JetBrains\PyCharm_project\pythonProject\sql_texts下文件如图:

运行结果:

excel文件内容:

更多推荐


所有评论(0)