数据源接入:结构化、半结构化与非结构化

数据源接入,说白了就是搞清楚你的数据从哪来、长什么样、怎么把它拽进 ELT 管道。我见过不少团队,一上来就急着写代码,结果被数据源的「脾气」折腾得够呛。这一章咱们就把三类数据源——结构化、半结构化、非结构化——挨个捋一遍。

结构化数据源:MySQL 与 PostgreSQL

结构化数据源,就是那种规规矩矩的表。行是行,列是列,主键、外键、索引一应俱全。MySQL 和 PostgreSQL 是咱们最常打交道的两个。

MySQL 接入要点

我个人习惯用 批量读取 + 增量同步 的方式接 MySQL。全量同步?那是小表才敢干的事。一张几千万行的订单表,你每周全量拉一次,管道迟早会崩。

核心策略:利用 MySQL 的 binlog 或自增 ID 做增量抽取。每次只拉取上次同步之后新增或变更的数据。

举个例子,假设订单表有个 update_time 字段:

-- 增量抽取 SQL
SELECT *
FROM orders
WHERE update_time > '2024-01-01 00:00:00'
  AND update_time <= '2024-01-02 00:00:00'
ORDER BY id;

嗯,这里要注意:时间字段一定要有索引。我在项目中遇到过,某次没加索引,全表扫描直接把源库 CPU 打满,业务方差点打电话过来骂人。

PostgreSQL 接入要点

PostgreSQL 比 MySQL 多了一些「高级玩具」,比如 逻辑复制 和 WAL 日志。如果你需要近乎实时的数据同步,逻辑复制是首选。

特性 MySQL PostgreSQL
增量方式 binlog / 自增 ID / 时间戳 逻辑复制 / WAL / 时间戳
批量读取 支持,但大表需分页 支持,游标更稳定
数据类型 基础类型为主 支持 JSON、数组、自定义类型
连接池 推荐 HikariCP 推荐 PgBouncer

你想想看,如果源表里存了 JSON 字段,PostgreSQL 可以直接在 SQL 里解析,省掉一层后处理。MySQL 8.0 虽然也支持 JSON,但性能上还是差一截。

小技巧:无论是 MySQL 还是 PostgreSQL,都建议在抽取时加上 WHERE 条件做分区裁剪。比如按日期分区,每次只拉一个分区,既快又稳。

半结构化数据源:JSON 与 XML

半结构化数据,说白了就是「有结构但不那么死板」。JSON 和 XML 是最常见的两种。它们不像数据库表那样有固定的 schema,但也不是纯文本。

JSON 接入

JSON 现在几乎是 API 接口的标配。我建议在 ELT 的 Extract 阶段就做一次 扁平化,把嵌套的 JSON 拍平成宽表。

举个例子,一个典型的用户行为日志:

{
  "user_id": 12345,
  "event": "click",
  "timestamp": "2024-01-15T10:30:00Z",
  "properties": {
    "page": "home",
    "button": "signup",
    "duration_ms": 2500
  }
}

扁平化之后,变成这样:

| user_id | event | timestamp           | page | button | duration_ms |
|---------|-------|---------------------|------|--------|-------------|
| 12345   | click | 2024-01-15 10:30:00 | home | signup | 2500        |

为什么要扁平化?因为大多数分析引擎(比如 ClickHouse、BigQuery)对宽表的查询效率远高于嵌套结构。我曾经接手过一个项目,原始 JSON 嵌套了 5 层,ETL 跑一次要 40 分钟。扁平化之后,15 分钟搞定。

避坑指南:JSON 里的数组要小心处理。比如 properties.tags 是个数组,你是拆成多行还是合并成字符串?这取决于下游怎么用。我建议先拆成多行,保留粒度,后面再聚合。

XML 接入

XML 现在用得少了,但在金融、医疗、政府系统里还是「老大哥」。XML 的解析比 JSON 重得多,因为它有命名空间、属性、CDATA 这些概念。

我个人习惯用 SAX 解析 而不是 DOM 解析。DOM 会把整个 XML 文档加载到内存,一个 500MB 的 XML 文件就能把 JVM 撑爆。SAX 是流式解析,边读边处理,内存占用小得多。

# Python 示例:使用 SAX 解析 XML
import xml.sax

class OrderHandler(xml.sax.ContentHandler):
    def startElement(self, name, attrs):
        if name == "order":
            self.current_order = {}
            self.current_order["id"] = attrs.get("id")

    def characters(self, content):
        if self.current_tag:
            self.current_order[self.current_tag] = content.strip()

    def endElement(self, name):
        if name == "order":
            # 写入目标表
            pass

你想想看,XML 的 schema 定义(XSD)其实是个好东西。它告诉你哪些字段是必填的、什么类型、取值范围。我建议在接入 XML 时,先拿 XSD 校验一遍,避免脏数据进管道。

非结构化数据源:日志文件与图片

非结构化数据,说白了就是「没固定格式」的数据。日志文件、图片、视频、PDF……这类数据没法直接塞进关系型数据库,得先做一层处理。

日志文件接入

日志文件是 ELT 里最常见的非结构化数据。Nginx 日志、应用日志、系统日志……每秒钟可能产生几百 MB。

我建议用 Filebeat + Kafka 的组合来收日志。Filebeat 负责采集和轻量处理,Kafka 负责缓冲和削峰。ELT 管道从 Kafka 消费数据,再做解析和结构化。

关键点:日志解析一定要在 Extract 阶段做,不要等到 Load 之后。把非结构化的日志行转成结构化的字段(时间戳、级别、来源、消息体),后面分析起来才顺手。

举个例子,一条 Nginx 日志:

192.168.1.1 - - [15/Jan/2024:10:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234

解析后:

| ip            | timestamp           | method | path       | status | size |
|---------------|---------------------|--------|------------|--------|------|
| 192.168.1.1   | 2024-01-15 10:30:00 | GET    | /api/users | 200    | 1234 |

嗯,这里要注意:日志格式可能随时变。我曾经遇到过,开发团队在日志里加了一个字段,没通知我们,结果解析器直接报错,管道停了两个小时。从那以后,我每次上线前都会加一个 schema 校验,格式变了就报警。

图片接入

图片接入 ELT 管道,通常不是为了分析图片本身,而是为了提取元数据或做后续的 AI 处理。

我建议的做法是:

  1. 存储路径:图片本身存在对象存储(S3、OSS)里,ELT 管道只存路径。
  2. 元数据提取:用 EXIF 工具提取拍摄时间、GPS 坐标、设备型号等信息。
  3. 特征向量:如果需要做图像识别,可以在 Extract 阶段调用预训练模型,生成特征向量,存入向量数据库。

小技巧:图片的元数据提取可以做成一个独立的微服务,ELT 管道通过消息队列异步调用。这样即使图片处理失败,也不会阻塞整个管道。

三类数据源接入对比

维度 结构化 半结构化 非结构化
典型代表 MySQL, PostgreSQL JSON, XML 日志文件, 图片
接入方式 JDBC/ODBC 批量读取 解析器 + 扁平化 采集代理 + 对象存储
增量策略 binlog / 时间戳 全量 + 增量标记 文件监听 + 偏移量
Schema 管理 固定,由 DDL 定义 灵活,需运行时推断 无,需提取阶段定义
常见坑点 大表全量扫描 嵌套层级过深 格式变化无通知

数据源接入的整体流程

下面这张图,是我自己画的数据源接入流程。你可以看到,不管数据源长什么样,最终都要经过 采集 → 解析 → 校验 → 暂存 这四个步骤,才能进入 ELT 的核心管道。

数据源接入流程 数据源接入整体流程 结构化数据源 MySQL / PostgreSQL 半结构化数据源 JSON / XML 非结构化数据源 日志 / 图片 采集层:批量读取 / 流式采集 / 文件监听 解析层:扁平化 / SAX解析 / 元数据提取 校验层:Schema校验 / 格式检查 / 异常报警 暂存层:原始数据写入暂存区(Staging Area) 暂存区是 ELT 的第一道防线,原始数据落地后再做清洗和转换

你看,不管数据源是 MySQL 还是日志文件,流程骨架是一样的。区别只在于采集和解析的具体实现。把这个流程想清楚,后面写代码的时候就不会跑偏。

避坑指南:我曾经在接入日志文件时,忘了处理文件滚动(log rotation)。结果 Filebeat 读到了已经归档的旧文件,重复消费了几百万条数据。从那以后,我每次都会确认文件采集的偏移量管理机制。

好了,数据源接入这块就聊到这儿。记住一句话:接入阶段多花点心思,后面清洗和转换阶段能省一半的力气。