📊 第5章 大数据的收集
📄 一句话概括

分页接口取一千条,实际发了 14 次请求,其中 4 次被限流。 没有断点续传,第 7 页一超时就得从第 1 页重来——每次都卡在同一页。 ETL 的命门是幂等:同一批数据加载三次,表里还是 1000 条,不是 3000 条。

📝 本章和前面几章的关系

上一篇讲文件和数据库。这一篇讲第三种来源:别人的接口。 教材 5.5、5.6 两节:API 数据收集与列编收集、批数据收集的开发方法。 接口取数是三种取法里最不稳定的——你不知道对方什么时候限流、什么时候改字段。

一、分页:一次只给一百条

大部分接口不会一次给你一万条,它一页给一百条,让你翻页。 演示里要取一千条,也就是翻十页。实际发了 14 次请求,4 次被限流。 因为模拟的接口每三次调用就返回一次 429。代码里遇到 429 就 continue 重发同一页。

这里有个关键认识:被限流不是失败,是常态。 接口收数的代码必须假设「会失败」。 正确的做法是:失败重试(带退避)、有最大重试次数、记录失败页。 没有上限的无限重试会把对方打挂,也把自己卡死。 还要注意:重试要有间隔。立即重试大概率还是被拒,而且会加重对方负担。

二、断点续传:别每次都从头来

演示第二段列了两种情况的对比。 没有断点:第 7 页超时,从第 1 页重来;程序崩了,已取的 600 条全丢。 有断点:第 7 页超时就从第 7 页接着要;崩了下次从第 7 页开始。

实现很简单:把「已经取到哪儿了」存下来。可以是页码,也可以是最后一个 id。 用 id 比用页码可靠。 因为翻页期间数据可能在变,第 7 页的内容会漂移; 用「id 大于 xxx」就没这个问题。 这也解释了为什么很多接口提供 since_id 这种参数——它就是为了让你能稳稳地接着取。

三、ETL:抽、转、装

ETL 是 Extract、Transform、Load 三个词:抽出来、转成要的样子、装进去。 演示里做了一次最简单的:把取到的一千条装进 SQLite,连装三次。 三次之后表里还是 1000 条,不是 3000 条。这叫幂等。

靠的是 primary key 加 insert or replace——同一个主键再进来就覆盖,不新增。 如果没有主键,重跑一次数据就翻倍。 这是数据管道最常见的事故,而且很难发现:报表数字突然大了一倍,大家先怀疑业务, 最后发现是任务重跑了。 所以做任何加载,第一件事是问:主键是什么?重跑会怎样?

这两个问题答不上来,这套管道迟早出事。

四、用工具还是自己写

教材 5.6 节把两条路并列:用现成的 ETL 工具,或者自己写代码。 工具的好处是快:界面点一点,连接器现成的,调度和监控都带。 工具的坏处是受限:需求一特殊就抓瞎,出问题日志是黑盒,而且通常按数据量或连接数收费。

自己写的好处是灵活:什么逻辑都能加,日志自己打,排查清楚。 自己写的坏处是全都得自己维护:调度、重试、监控、告警、权限,一样不能少。 选择标准和第 3.4 篇一样:看团队有没有人能维护。

但有一条经验值得记住:先把流程想清楚,再上工具。流程没想清楚就上工具,只会把混乱自动化, 而且自动化得更快。

五、动手 Demo:翻十页、被限流四次、重跑三遍不多一条

模拟一个每页一百条、每三次调用限流一次的接口。 翻页取完一千条,看重试了几次。 再把结果连装三次,验证幂等。

_CALLS = [0]

def fake_api(page, size=100, total=1000):
    _CALLS[0] += 1
    if _CALLS[0] % 3 == 0:
        raise RuntimeError('429 too many requests')
    start = (page - 1) * size
    return [{'id': i, 'v': i % 97}
            for i in range(start, min(start + size, total))]

got, page = [], 1
while len(got) < 1000:
    try:
        got.extend(fake_api(page))
        page += 1
    except RuntimeError:
        continue          # 被限流就重发同一页

# 幂等加载:主键 + replace
con.executemany('insert or replace into t values(?,?)', pairs)

实跑输出:

--------------------------------------------------------------------
翻页、限流、以及重跑三遍不会多一条
--------------------------------------------------------------------

  ① 分页接口:一次只给一百条
  --------------------------------------------------------------------
  目标:取完 1000 条,每页 100 条,也就是要翻 10 页。
  发出了 14 次请求,其中 4 次被限流
  拿到 1000 条
  注意那个 continue:被限流不算失败,等一会儿重发同一页就行。
  接口收数必须假设「会失败」,不能假设「一次就成」。

  ② 没有断点续传会怎样
  --------------------------------------------------------------------

| 情况 | 没有断点 | 有断点 |
|---|---|---|
| 第 7 页超时 | 从第 1 页重来 | 从第 7 页接着要 |
| 程序崩了 | 已取的 600 条全丢 | 下次从第 7 页开始 |

  分页收数看起来简单,但一个不稳的接口配上没有断点的代码,结果是每次都卡在同一页。
  工程上的做法是记一个游标:把「已经取到哪儿了」存下来,下次接着跑。
  第 5.4 篇的队列也是同样的思路。

  ③ ETL:抽、转、装
  --------------------------------------------------------------------
  同一批 1000 条,连续加载三次:
  第一次之后 1000 条,第二次 1000 条,第三次 1000 条
  三次都是 1000 条,没有翻倍。这叫幂等:同样的输入跑多少遍,结果一样。
  靠的是 primary key 加 insert or replace。
  没有主键的话,重跑一次数据就变成两倍——这是数据管道最常见的事故。
  所以做任何数据加载,第一件事是问:主键是什么?重跑会怎样?

  ④ 那要不要用现成的 ETL 工具
  --------------------------------------------------------------------

|  | ETL 工具 | 自己写 |
|---|---|---|
| 上手 | 快,界面点一点 | 要写代码 |
| 改需求 | 受工具限制 | 想怎么改就怎么改 |
| 排错 | 黑盒,日志难查 | 自己打的日志,清楚 |
| 适合 | 标准化的搬运 | 有特殊业务逻辑 |

  教材 5.6 节把两条路并列,选择标准和第 3.4 篇一样:看团队有没有人能维护。
  不过有一条经验:先把流程想清楚,再用工具。流程没想清楚就上工具,只会把混乱自动化。

第一段那个 continue 是最小可用的重试。 真项目里还要加退避(等 1 秒、2 秒、4 秒)和最大重试次数,否则一个坏接口能把程序卡死。 十四次请求里四次被限流,比例接近三成。 这不算高——真实接口限流更凶,所以「翻页取数」这件事比想象中慢得多。 第三段那个幂等演示只有三条输出,但它是本篇最重要的结论:主键决定了重跑安不安全。 最后那张工具对比表没有绝对答案。但如果团队里没人能维护自研管道,老老实实用工具更稳。

30 秒实验

  1. 把限流逻辑改成「每五次调用限流一次」,看总请求数变多少。
  2. 把 insert or replace 改成 insert,重跑三次,看报什么错。
  3. 给重试加一个计数器,超过 10 次就放弃,看代码要怎么改。

常见误区

  1. 「接口返回 429 是接口有问题。」429 是「你请求太快」,通常是你自己的问题,要退避。
  2. 「重试一下就好了。」没有上限的重试会把对方打挂,也把自己卡死。必须设最大次数和间隔。
  3. 「ETL 跑失败重跑一遍就行。」没有主键的话,重跑就是翻倍。幂等不是可选项。
自测三问(点开对答案) 1. 为什么接口收数必须假设「会失败」? —— 因为限流、超时、网络抖动是常态。代码如果假设一次成功,遇到 429 就会崩或者死循环。 2. 断点用页码好还是用 id 好? —— 用 id 好。翻页期间数据可能新增或删除,页码对应的内容会漂移; 「id 大于上一次的最后一个」就不会漏也不会重。 3. 什么叫幂等?为什么它这么重要? —— 同样的输入跑多少遍,结果一样。 数据管道经常失败重跑,不幂等的话重跑一次数据就翻倍,而且很难发现。

下一篇讲流数据怎么落地:队列、消费者、以及积压。

相关笔记