872 字
4 分钟
增量归档:第二次跑得比第一次快

第一次缓存一个用户,可能要走几百页、下几千个文件。第二次只该花几秒钟。

这听着像优化,其实是能不能用的分界线:每次更新都要重走整条时间线,你就不会去点那个更新按钮。

微博:连续 5 条命中就停#

微博的翻页接口是 m.weibo.cn/api/container/getIndex,containerid=107603{uid},靠 since_id 往前翻。

判断「已经存过了」最直接的办法是看这条的原帖 id 在不在本地。但直接拿这个当停止条件会出事:

  • 置顶帖永远在第一条,会误判成”到头了”
  • 转发的 id 和内容归属不一致,容易算错
  • 帖子标题在,但媒体只下了一半,也算不上完整

所以停止条件是:连续 5 条「已缓存且媒体完整」的非置顶原创。连续,不是累计——中间夹一条新的就清零重算。

5 这个数字是试出来的。1 条太容易误判,10 条在白翻好几页。

另有一条保底:翻到 200 页就停,并明确告诉用户「更早的内容没拉到」。这个提示很重要,否则界面显示”完成”,你会以为全在了。

断点:别让中断等于白干#

微博的翻页有风控(后面单独写),跑到一半断掉是常态。

所以 _profile.json 里记三个值:lastSinceId、lastPage、fetchStatus。fetchStatus 只有三种:complete / partial / page_limit。

下次点「更新」,从 lastSinceId 继续,不用从头来。

换日期范围不会重置断点,只按新范围过滤。这条是为了避免”改个日期就重新爬一遍”。

媒体:.part 加 Range#

帖子层面能续,单个文件层面呢?

微博的媒体下载现在这么走:

  1. 下到 {文件名}.part
  2. 已经存在有效的 .part 就用 Range: bytes=N- 接着下
  3. 服务端忽略 Range(回 200 而不是 206),就从零重下
  4. 校验通过后 os.replace() 改名成正式文件

关键在于最后那步用 os.replace 而不是 rename 加判断——同一个文件系统内它是原子的,要么旧的要么新的,不会出现半个文件。

进度条也分两种:知道总大小的显示百分比,不知道的(gallery-dl 的很多场景)显示「已完成 N 个」加一个不确定态的动画条。不伪造百分比,转了半天的 99% 比没有进度更让人焦虑。

原子写入#

不只是媒体,JSON 也一样。

断电、强杀进程、写到一半磁盘满——都会留下一个语法不完整的 settings.json,下次启动直接崩。

所有元数据写入都改成:写临时文件 → flush() → os.fsync() → os.replace()。

fsync 那步不能省。flush 只是把数据从 Python 的缓冲区交给操作系统,fsync 才是真正落盘。省掉它,断电的时候文件系统里可能只有一个空文件。

X 和 Instagram:交给 download-archive#

这两个走 gallery-dl,增量不用自己实现——它在存档根目录维护 .download-archive.sqlite,已经下过的媒体直接跳过。

不加这个文件的话,缓存一个几千条的大号,每次更新都要把整条时间线走一遍,光请求就要几十分钟。


增量做对之后,日常使用的手感完全不一样:日更的账号,点一下更新等十几秒;半年没动的,第一次慢,之后也快。

第一次慢是应该的,每次都慢是设计错了。

增量归档:第二次跑得比第一次快
https://mssworld.me/posts/incremental-archive/
作者
m.s.s
发布于
2026-10-19
许可协议
CC BY-NC-SA 4.0