分页接口取一千条,实际发了 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 秒实验
- 把限流逻辑改成「每五次调用限流一次」,看总请求数变多少。
- 把
insert or replace改成insert,重跑三次,看报什么错。 - 给重试加一个计数器,超过 10 次就放弃,看代码要怎么改。
常见误区
- 「接口返回 429 是接口有问题。」429 是「你请求太快」,通常是你自己的问题,要退避。
- 「重试一下就好了。」没有上限的重试会把对方打挂,也把自己卡死。必须设最大次数和间隔。
- 「ETL 跑失败重跑一遍就行。」没有主键的话,重跑就是翻倍。幂等不是可选项。
自测三问(点开对答案)
1. 为什么接口收数必须假设「会失败」? —— 因为限流、超时、网络抖动是常态。代码如果假设一次成功,遇到 429 就会崩或者死循环。 2. 断点用页码好还是用 id 好? —— 用 id 好。翻页期间数据可能新增或删除,页码对应的内容会漂移; 「id 大于上一次的最后一个」就不会漏也不会重。 3. 什么叫幂等?为什么它这么重要? —— 同样的输入跑多少遍,结果一样。 数据管道经常失败重跑,不幂等的话重跑一次数据就翻倍,而且很难发现。下一篇讲流数据怎么落地:队列、消费者、以及积压。
- ☐ (本篇完,下一篇 5.4 流数据与分布式队列)