上个月团队接了一个内部数据治理的活,需要把公司里几十套系统的元数据统一管理起来。跑了一圈发现,情况比预想中麻烦得多:有的系统开放了接口可以直接拿元数据,有的只能在后台管理页面里一张表一张表地点着看,还有的系统连界面都登不进去,只能找 DBA 临时开个只读账号跑查询。我手工整理了两个星期,光是把表名、字段、类型、备注录到 Excel 里就花了大量时间,后来的增量变更更是让人崩溃。最后我干脆写了这套 Python 爬虫加统一建模的方案,把不同来源的系统元数据全部收进同一套模型里,跑一个命令就能完成全系统采集。
这篇文章把整个项目从头到尾拆开讲,包括模型怎么设计、不同源怎么适配、增量更新怎么做,以及我实际踩过的一堆坑。内容主要还是围绕系统元数据的统一建模跟多源抓取来展开,读者在搭建数据地图、数据资产管理或者做系统盘点时,可以直接把这套思路抄过去用。
1. 项目背景与整体设计思路
1.1 为什么需要一个“大一统”的元数据模型
系统元数据这个概念,大家平时说得比较多,指的是描述系统自身以及数据结构的数据,比如数据库里有哪些表、每张表有哪些字段、字段是什么类型、有没有索引、谁负责维护、数据从哪来。这些东西分散在各个系统里,平时没人注意,但是一到做数据地图、血缘分析、影响评估的时候就变成硬需求。
我见过不少团队的做法是:每个系统维护一份 Excel,内容由各自研发手动填写。结果也很典型,命名风格不统一,有的叫“用户信息表”,有的叫“t_user_info”,还有的字段类型一会儿写“varchar”,一会儿写“字符串”。当要跨系统做关联分析时,数据根本对不上,只能逐个系统人工核对。
所谓“大一统”建模,不是说要把所有系统的元数据强行揉成一个样,而是设计一个标准骨架,把系统信息、表信息、字段信息、依赖关系这些核心要素统一收纳。抓到原始数据之后,通过一层适配跟清洗,最终落到这个标准模型里。这样不管后端是 MySQL、Oracle、HTTP 接口还是 HTML 页面,进到统一模型之后的结构是完全一致的,后续查询、分析、展示都好办。
1.2 整体架构可以拆成哪几层
这个项目的架构不复杂,核心就五层,跟常见的爬虫采集管道很像。
抓取层是最靠近数据源的部分,主要做一件事:根据不同的来源类型,用对应的适配器去拿原始元数据。比如某系统有 OpenAPI,就用 API 适配器拉取;某系统只有管理后台页面,就用 HTML 适配器模拟登录再去解析;某个老系统连接口和页面都不方便,就用数据库适配器直连查询系统表。
抓取层之上是清洗标准化层。原始数据基本都不能直接用,比如类型大小写不统一、注释里混着 HTML 标签、字段名带不可见字符,这里要做统一的清洗转换。
再往上是统一模型层,也就是用 dataclass 或者数据表定义好的标准结构。抓取和清洗完的数据会实例化成这个模型。存储层负责把标准化之后的模型落到数据库里。调度层则负责定时触发采集任务、控制并发、记录日志和做失败重试。
整条链路下来,核心的工程量其实集中在两头:适配器要怎么写,模型要怎么设计。中间清洗和调度的逻辑反而相对固定,抽象好之后基本可以复用。
1.3 技术选型时的考量
选 Python 做这件事,坦白说不是因为它性能多强,而是生态确实顺。爬虫相关库 requests、BeautifulSoup、lxml 都很成熟,数据处理有 pandas,定时调度有 APScheduler,数据库操作有 SQLAlchemy,几乎所有环节都能找到现成的轮子。
具体到每个组件的选择,我简单梳理一下:
- 采集 HTTP 用 requests,操作简单,session 管理、cookie 保持都方便。
- HTML 解析用 lxml + BeautifulSoup,lxml 速度快,BeautifulSoup 的 API 对 XPath 和 CSS 选择器的兼容都不错。
- 数据模型定义阶段用 dataclass,先不急着上 ORM,方便在大内存里临时组装,等入库时再转成 SQLAlchemy ORM。
- 调度用 APScheduler 的 CronTrigger,可以精确控制每天几点跑、周期多长。
- 任务日志用标准 logging 模块,一看到文件就能定位问题。
存储层的方案我建议先用 MySQL 或者 SQLite 起步。SQLite 适合几十个系统以内的小规模场景,MySQL 适合有固定服务器、需要多人访问的环境。没必要一开始就上大数据平台,模型定得合理,后面迁移也容易。
2. 元数据建模:先设计好标准模型再动手抓数
2.1 统一模型需要覆盖哪些核心要素
建模是这项工作最有价值的部分,也是最容易返工的部分。我在设计时先把所有系统的元数据要素全部列了一遍,看了两天各个系统的接口文档和页面结构后,提炼出三组核心对象。
第一组是系统级元数据,描述系统本身的属性。我预留了系统编码、系统名称、访问入口地址、负责人、所属部门、技术栈、环境类型、采集方式、采集状态、最后采集时间这些字段。系统编码是一个系统在平台内的唯一标识,建议用拼音缩写或统一编号,比如订单中心用 odc,用户中心用 usr,避免直接套用业务名称。
第二组是表级元数据,描述数据库里的表和视图。字段包括表名、所属库名或模式名、表类型、存储引擎、行数估值、表注释、创建时间、更新时间、主键字段列表。表名不直接沿用原始库里的名字,而是通过适配器统一转成小写并去掉多余前缀,这样跨系统比较时不会因为大小写和前后缀产生误判。
第三组是字段级元数据,这一层最琐碎,也是最容易出问题的。字段名、数据类型、长度、精度、是否可空、是否主键、默认值、注释、枚举值、字段序号,基本都要存。其中数据类型和长度的标准化是重点,比如 varchar(100) 和 VARCHAR(100) 要归一化成 VARCHAR 和 100。枚举值信息在数据地图里非常有用,很多系统有 status 字段,0 1 2 分别代表什么含义,不记录下来后面分析全靠猜。
这三层之间有清晰的归属关系:一个系统包含多张表,一张表包含多个字段。把这个层次关系先在模型里定义好,后续爬虫抓回来才能有序组装。
2.2 用代码定义一个基础数据模型
开发时我没有直接用 ORM 来定义模型结构,而是先用 Python 标准库的 dataclass,配合类型和默认值,先把结构稳住。这个阶段的目标是让模型能在内存里方便组装,等到入库再转成 ORM 记录。
from dataclasses import dataclass, field from typing import List, Optional @dataclass class FieldMeta: table_id: str = "" field_name: str = "" field_type: str = "" field_length: Optional[int] = None field_scale: Optional[int] = None nullable: bool = True primary_key: bool = False default_value: Optional[str] = None comment: str = "" enum_values: str = "" ordinal: int = 0 @dataclass class TableMeta: system_code: str = "" table_name: str = "" schema_name: str = "" table_type: str = "BASE TABLE" engine: str = "" comment: str = "" primary_keys: str = "" fields: List[FieldMeta] = field(default_factory=list) @dataclass class SystemMeta: system_code: str = "" system_name: str = "" entry_url: str = "" owner: str = "" tech_stack: str = "" env_type: str = "prod" fetch_mode: str = "" fetch_status: str = "pending" last_fetch_time: Optional[str] = None tables: List[TableMeta] = field(default_factory=list)这套模型看起来简单,但实际跑数据时它会极大减少对接成本。所有的适配器抓完数据后都返回 SystemMeta 对象,其他模块不需要关心这个系统原来的元数据是什么格式,只管统一模型就行。后续在 MySQL 落表时,把 dataclass 映射成三张表就够了:sys_system、sys_table、sys_field。
2.3 模型扩展与多环境适配
建模时一定要给将来留余地。第一个版本我只设计了基础字段,后来发现实际使用中还需要追查数据血缘,于是补了字段来源、逻辑表映射、刷新策略;又因为安全部门介入,加了敏感级别、脱敏规则。这些字段如果一开始就全设计进去会显得过度设计,后续通过扩展字段 JSON 的方式兼容更灵活。
我的建议是:核心字段尽量稳定,扩展字段统一用一个 extra_json 来承载。任何时候爬虫抓到模型里还没有的属性,都可以塞进 extra_json 不丢数据,同时也不影响主表结构。
多环境适配也是建模时要考虑的事。生产环境、测试环境、预发环境里的表结构不完全一致,统一建模时最好把环境类型作为系统的一个属性,而不是体现在表名或字段名前缀上。如果某个系统分库分表特别多,同表名后缀有 0 到 32 个分片,采集时还需要在表级模型里记录分片信息。我在模型里加上 sharding_count 字段专门处理这种情况,这样统一模型在大多数场景下都不需要改结构。
3. 爬虫抓取模块的实现:多用适配器,少写重复代码
3.1 适配器模式为什么适合这个场景
系统来源形态各异,但采集流程是一致的:登录认证、访问目标、解析元数据、标准化、返回模型。如果不用适配器,每个系统写一套独立逻辑,主流程会被大量重复代码淹没,后面加一个新系统只能复制粘贴再改。所以这里选了适配器模式。
from abc import ABC, abstractmethod class BaseAdapter(ABC): source_type = "base" def __init__(self, system_meta: dict): self.system = system_meta @abstractmethod def fetch(self) -> SystemMeta: """返回标准化后的 SystemMeta 对象""" pass每个具体适配器只需要实现 fetch 方法,返回 SystemMeta 就可以。我在工程里维护一个注册表,通过系统配置里的 source_type 字段自动选择对应的适配器。
ADAPTER_REGISTRY = {} def register_adapter(source_type: str): def decorator(cls): ADAPTER_REGISTRY[source_type] = cls return cls return decorator @register_adapter("api") class ApiAdapter(BaseAdapter): def fetch(self) -> SystemMeta: # 访问 API 并解析 pass @register_adapter("html") class HtmlPageAdapter(BaseAdapter): def fetch(self) -> SystemMeta: # 模拟登录并解析页面 pass @register_adapter("db") class DbAdapter(BaseAdapter): def fetch(self) -> SystemMeta: # 直连数据库读取系统表 pass有了这个注册表,新增一个系统只需要在配置里指定 source_type 和对应参数,主程序不用动。我在做过一次扩展测试,一个只有 HTML 页面的老系统接入,只写了新的适配器类、注册一下,总共两个多小时。
3.2 三种主流抓取方式的实现要点
第一种是 API 适配器。现在不少系统提供了元数据查询接口,直接访问就行。实现时重点处理两件事:认证方式和返回结构解析。有些系统用 Token,有些用 Basic Auth,还有些需要签名。我一般把认证参数放在适配器构造函数里,避免污染主流程。
@register_adapter("api") class ApiAdapter(BaseAdapter): def fetch(self) -> SystemMeta: url = self.system["entry_url"].rstrip("/") + "/api/v1/meta/tables" headers = {"Authorization": f"Bearer {self.system['token']}"} resp = requests.get(url, headers=headers, timeout=30) resp.raise_for_status() data = resp.json() meta = SystemMeta(system_code=self.system["system_code"], system_name=self.system["system_name"]) for t in data.get("tables", []): table = TableMeta(table_name=t["name"], system_code=meta.system_code) for col in t.get("columns", []): field = FieldMeta( field_name=col["name"], field_type=col["type"], nullable=col.get("nullable", True), primary_key=col.get("primary_key", False), comment=col.get("comment", "") ) table.fields.append(field) meta.tables.append(table) return meta这里有个细节要说一下,接口返回的字段命名风格差异很大,有的下划线,有的是驼峰。Adapt 层里最好统一做一次 key 名映射,否则每个系统的 JSON 解析代码都会出现一堆col["columnName"]、col["column_name"]这样的重复逻辑。
第二种是 HTML 页面适配器。这个涉及模拟登录和页面解析,也是最容易出问题的。我的方案是先分析登录请求,用 requests.Session 保持 cookie,定位登录提交的 URL 和参数,再访问元数据列表页解析表格。
@register_adapter("html") class HtmlPageAdapter(BaseAdapter): def fetch(self) -> SystemMeta: session = requests.Session() login_url = self.system["login_url"] payload = { "username": self.system["username"], "password": self.system["password"], "remember": "1" } session.post(login_url, data=payload, timeout=30) tables_page_url = self.system["entry_url"] resp = session.get(tables_page_url, timeout=30) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") meta = SystemMeta(system_code=self.system["system_code"]) rows = soup.select("table tbody tr") for row in rows: cols = [c.get_text(strip=True) for c in row.find_all("td")] if len(cols) < 4: continue table = TableMeta(table_name=cols[1], schema_name=cols[0], comment=cols[3], system_code=meta.system_code) meta.tables.append(table) return meta页面适配器最怕的是系统前端框架更新导致 DOM 结构变化。我在实际项目中常规做法是先记住页面解析时的关键选择器,并做好找不到元素时的报错提示,这样页面一改版,报错能马上定位到具体系统,不至于默默抓回一堆空数据。
第三种是数据库适配器。有些系统连页面和接口都走不通,只能走数据库直连。这个方案需要的权限是只读,能查 information_schema 就够了。数据库适配器的代码核心就是拼接 SQL 去查表信息和列信息。
@register_adapter("db") class DbAdapter(BaseAdapter): def fetch(self) -> SystemMeta: engine = create_engine(self.system["db_uri"], pool_size=5) meta = SystemMeta(system_code=self.system["system_code"]) with engine.connect() as conn: tables = conn.execute(text( "SELECT table_name, table_schema, table_comment " "FROM information_schema.tables " "WHERE table_schema = :schema"), {"schema": self.system["schema"]}).fetchall() for t in tables: table = TableMeta(table_name=t[0], schema_name=t[1], comment=t[2], system_code=meta.system_code) columns = conn.execute(text( "SELECT column_name, data_type, character_maximum_length, " "is_nullable, column_key, column_comment, column_default " "FROM information_schema.columns " "WHERE table_schema=:schema AND table_name=:table"), {"schema": self.system["schema"], "table": t[0]}).fetchall() for c in columns: table.fields.append(FieldMeta( field_name=c[0], field_type=c[1], field_length=c[2], nullable=(c[3] == "YES"), comment=c[5], default_value=c[6] )) meta.tables.append(table) return meta用 database 适配器时务必在应用层加上只读约束。我见过有同事写库适配器时误把连接串指向了生产可写账号,好在只执行了 select,没出事。更稳妥的做法是给这个爬虫单独创建只读账号,权限只开放给 information_schema 和业务库的 SELECT。
3.3 数据清洗与标准化统一入口
从不同源抓回来的数据进入统一模型前,都要做一下清洗。我把清洗逻辑放在所有适配器之后、入库之前的入口处,统一走一个 standardize 函数。这个函数主要处理几个类型的脏数据。
第一是类型归一化。MySQL 的 int(11)、Oracle 的 NUMBER(10) 都要转成标准类型加长度的结构。我维护了类型映射表,比如 int、INTEGER、bigint 归成 INT,varchar、char、nvarchar 归成 STRING 并把长度放在单独字段。第二是空白字符清洗。字段注释里经常有出换行、缩进、全角空格,用正则去掉。第三是默认值和枚举值整理。默认值统一转字符串,枚举值从 0,1,2 的形式整理成可读的 0=待处理,1=已完成,2=失败。
def standardize_system_meta(system_meta: SystemMeta) -> SystemMeta: for table in system_meta.tables: table.table_name = table.table_name.strip().lower() table.comment = clean_comment(table.comment) for field in table.fields: field.field_name = field.field_name.strip().lower() field.field_type = normalize_type(field.field_type) field.field_length = extract_length(field.field_type) field.comment = clean_comment(field.comment) return system_meta清洗必须在适配器外面做。适配器只负责拿数据和初解析,统一清洗能保证即使不同系统的命名风格差异很大,最终出来的数据也相对规范。这是后面做字段关联和血缘分析的重要前提。
4. 自动化调度与增量更新策略
4.1 全量采集还是增量采集
元数据采集一开始可以先做全量,把已有系统的全部表、字段抓一遍。但是全量采集不能天天跑,每次光查询 information_schema 就要消耗数据库较多资源,如果系统表数量很大,还会占网络和 IO。我的方案是首次全量采集之后,后续默认走增量采集。
增量采集的核心思路比较朴素:每次采集前先查看每个系统距离上次采集后有没有变化。有表新增、表删除、字段新增、类型变化的都需要更新。对带 API 的系统,接口会返回最后更新时间;对 HTML 页面没有这种信息的,只能在解析时做对比。我把之前采集的结果存下来,本次抓到的表跟历史表做 diff,删除的表记录为已下线,新增的表正常入库,有变化的表更新全部字段。
def diff_and_update(table_dict, new_tables, system_code): current_names = {t.table_name for t in new_tables} old_names = set(table_dict.keys()) for name in old_names - current_names: mark_table_offline(system_code, name) for table in new_tables: if table.table_name not in old_names: insert_table_meta(table) elif table_dirty(table, table_dict[table.table_name]): update_table_meta(table)这个 diff 逻辑不需要太复杂,表指纹用字段名加类型加注释拼接,一旦拼接结果不同就认为有变化。字段变化频次不高,所以性能上不是问题。
4.2 调度配置与任务日志
调度我用了 APScheduler 的 定时触发。比如每天凌晨 2 点跑全量扫描,白天每隔 6 小时跑一次增量。凌晨跑能避开业务高峰,减少对系统库的影响。
from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger def job_full(): run_fetch(full_scan=True) def job_incremental(): run_fetch(full_scan=False) if __name__ == "__main__": scheduler = BlockingScheduler() scheduler.add_job(job_full, CronTrigger(hour=2, minute=0)) scheduler.add_job(job_incremental, CronTrigger(hour=8, minute=0)) scheduler.add_job(job_incremental, CronTrigger(hour=14, minute=0)) scheduler.add_job(job_incremental, CronTrigger(hour=20, minute=0)) scheduler.start()日志这块最好给每个系统单独记录采集状态,不要所有系统混在一起。我是用系统编码加时间戳生成日志文件名,里面记录采集开始时间、成功表数、失败表数、异常堆栈。这样排查问题时能直接看单个系统的采集历史,不用在全局日志里翻。
logging.basicConfig( filename=f"logs/{system_code}_{datetime.now():%Y%m%d}.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" )4.3 失败重试与异常隔离
元数据采集牵涉到多个弱依赖系统,某一个系统挂掉不应影响其他系统的采集。我在循环采集的主流程里用 try-except 把每个系统的处理包住,单个系统失败后记录下来继续跑下一个。
def run_fetch(full_scan: bool): for sys_conf in get_all_system_configs(): try: fetcher = ADAPTER_REGISTRY[sys_conf["source_type"]](sys_conf) raw_meta = fetcher.fetch() std_meta = standardize_system_meta(raw_meta) save_to_database(std_meta) log_info(sys_conf, "ok") except Exception as e: log_error(sys_conf, e) continue重试机制需要在例外累积时做退避。如果同一个系统连续 3 次失败,说明大概率是配置变更或系统下线了,应该直接告警人工处理。重试次数定义在适配器里,不要放在全局,因为不同系统的脆弱程度不一样,有的随便重试,有的要减少采集频率。
5. 常见问题与踩坑实录
5.1 字段歧义与同名不同义
不同系统里“createtime”这个字段,有的存的是字符串 yyyy-MM-dd HH:mm:ss,有的存的是时间戳,有的存的是8位数字 yyyyMMdd。统一模型里如果不区分,做跨系统分析时天数计算差一年都很正常。我的处理是在字段类型里区分 DATETIME、TIMESTAMP、STRING 三种常见形态,并在注释里保留原始系统说明。同时字段名标准化为 createtime 后,原始字段名仍然存在 extra_json 里,避免信息完全丢失。
类型映射的过程中还有一个容易踩的坑是 NUMBER 类型没有长度但有精度。Oracle 的 NUMBER(10,2) 转过来时精度是 2,很多初学者直接把长度按 10 处理,后面做精度换算时会差十倍。清洗函数里要区分长度和精度,长度可以去掉,但精度必须保留到单独的字段。
5.2 登录态过期和验证码
基于 HTML 的适配器最怕登录态失效。Cookie 的有效期短的可能只有半小时,长的一天。我一开始没有实现会话过期判断,第二天早上跑采集全失败了,因为 session 已经失效。后来在 HTML 适配器里加一个会话检查函数,请求完页面先判断页面有没有出现登录跳转关键词,如果有就直接重新登录再跑一遍。
验证码问题更麻烦。有些系统登录有动态验证码,这种自动化方案很容易被卡住。我的建议是,对这类系统尽量争取技术部门开一个服务账号,走免验证码的 API,或者人工在浏览器登录后导出 cookie,不适直接用 requests 传输这个动态凭证。cookie 导出方案有效期也不长,需要人工周期性更新,这个没法完全自动化,只能做成提醒机制。
5.3 HTML 编码与特殊字符
页面返回的编码千奇百怪,有的 GBK,有的 UTF-8,有的页面声明的编码跟实际编码不一致。我之前一股脑用 resp.encoding 拿到 header 里写的编码去解码,结果经常出现乱码。后面统一改用 resp.apparent_encoding,配合 chardet 自动探测,读取效果好很多。
特殊字符表现在两个地方:HTML 页面里字段注释经常包含 、&等实体,数据库注释里有全角数字、换行符。清洗函数里需要统一处理,先 html.unescape 转一次实体,再去掉换行和多余空格。否则这些符号进了数据库之后,下游做报表时非常痛苦,查数据会莫名多一串符号。
5.4 数据库连接耗尽和查询超时
数据库适配器最容易遇到的是连接池耗尽和查询超时。如果同时启动多个线程采集几十个系统的数据库,每个连接都占着一个连接池,数据库可能很快被打挂。我的做法是限制并发数到 3 个,并且数据库适配器统一用连接池 size=2,同时给 information_schema 查询加超时时间,超过 10 秒直接放弃这个系统。
information_schema 数据量大时查询很慢,一张大表的字段信息查询可能执行几秒甚至几十秒。优化方式是分批查询,不要逐表一层一层地查列信息,改成先把要采集的系统所有表名一次拿出,再用一个 IN 条件批量查列信息,最后在内存里按表分组。
tables_sql = "SELECT table_name FROM information_schema.tables WHERE table_schema=:schema" columns_sql = ("SELECT table_name, column_name, data_type, character_maximum_length, " "is_nullable, column_key, column_comment, column_default " "FROM information_schema.columns " "WHERE table_schema=:schema AND table_name IN :tables")这个优化能让采集时间从小时降到分钟级,在大库场景下非常明显。
5.5 常见问题速查表
这里整理一个速查表,方便排查时对照参考。
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 某个系统一直失败 | 配置里的 entry_url 变了或接口地址变更 | 检查系统配置和接口文档,更新 URL |
| 抓到的表全是空值 | 页面结构变了,选择器失效 | 打开页面检查 DOM,更新解析逻辑 |
| 字段类型全是大写字母 | 清洗映射表没有覆盖 | 补充类型映射规则 |
| 字段注释出现 HTML 标签 | 未转义实体标签 | 清洗时先用 html.unescape |
| 部分表总是缺失 | 表存在不同 schema 里 | 检查库名配置,确认 schema 范围 |
| 采集时间特别长 | 存在超大表,逐表查列 | 改用 IN 批量查列 |
| 登录成功后仍跳回登录页 | Cookie 过期或动态参数缺失 | 检查会话维持逻辑,更新 cookie |
| 密码错误但人工登录正常 | 密码里有特殊字符被 URL 编码截断 | 用 requests data 直接传原始字符串,不预编码 |
6. 项目落地效果与可扩展方向
6.1 落地之后的效果
这套方案跑起来之后,效果比较直观。原来人工整理一周才能汇总一次元数据,现在每天自动跑四轮,每次耗时在几分钟。数据地图、数据查询、影响分析终于有了一份相对准的底数。我对比过人工整理的物料和自动采集的结果,字段缺失率从大概 15% 降到 1% 以下,表的数量能全面覆盖到各个系统,不再靠记忆去补。
多个系统之间的元数据对比也变得简单很多。以前想做全局字段一致性检查,比如要找出哪些系统都叫 createtime 但类型不一致,基本是手工逐一排查。现在统一模型入库后,一条 SQL 就能把类型不一致的字段列出来。
我自己还追加一个使用技巧:不要只把元数据当静态档案存库,要在采集完成后跑一遍指标,比如系统数量、表数量、字段总量、本周变更次数。把这些指标发给相关同事,大家才有体感,也更愿意配合后续的数据治理工作。
6.2 可以继续扩展的方向
这套方案的扩展性还能继续做厚。第一是做数据血缘。通过解析每张表、每个字段的取数逻辑和上下游依赖,把元数据模型延伸出 lineage 关系,就能自动绘制全链路的数据流向图。第二是做权限和安全分级。在字段模型中加入敏感级别和脱敏规则,自动扫描识别手机号、身份证、银行卡等字段,打上标签。第三是做元数据开放 API,把统一模型通过服务层暴露给下游,其他平台要查表结构、字段注释时,不用再发邮件找 DBA,直接调接口就行。
这几个方向都是基于同一套统一建模和采集底座的,底座设计得够稳,扩展只是业务层的工作量。这也是我最初把所有精力压到建模和适配上的原因。
最后分享一点个人体会
做这种偏内部治理的项目,最怕的就是“重爬轻模型”。很多人一听到爬虫就想到反爬、验证码、代理池,但元数据抓取最耗精力的反而是模型设计和数据标准化。适配器花两个晚上能写完,但类型映射、字段去重、注释清洗这些脏活如果不提前想清楚,后面每次采集出来都是半成品,下游根本没法用。
我后来再做类似的项目,流程都固定成了先花一半时间建模和梳理字段口径,再写爬虫逻辑。这样做前期看起来慢,但后期几乎没有返工。另外一个技巧是给适配器加上版本标记,每次系统方改版,优先从版本异常里排查问题,能省不少时间。希望这篇文章能帮到正在头疼系统元数据治理的人,少走我走过的弯路。