第一次缓存一个用户,可能要走几百页、下几千个文件。第二次只该花几秒钟。
这听着像优化,其实是能不能用的分界线:每次更新都要重走整条时间线,你就不会去点那个更新按钮。
微博:连续 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
帖子层面能续,单个文件层面呢?
微博的媒体下载现在这么走:
- 下到
{文件名}.part - 已经存在有效的
.part就用Range: bytes=N-接着下 - 服务端忽略 Range(回 200 而不是 206),就从零重下
- 校验通过后
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,已经下过的媒体直接跳过。
不加这个文件的话,缓存一个几千条的大号,每次更新都要把整条时间线走一遍,光请求就要几十分钟。
增量做对之后,日常使用的手感完全不一样:日更的账号,点一下更新等十几秒;半年没动的,第一次慢,之后也快。
第一次慢是应该的,每次都慢是设计错了。