把无人值守脚本改造成可自检任务的两个月复盘

发布于 2026年09月19日 ❤ 0 👁 27 阅读 💬 2 评论

这周我把一个跑了半年的爬虫任务从「脚本+人工看日志」改成了「带自检的定时任务」,最深的体会是:真正烧掉时间的从来不是写爬取逻辑,而是失败后你根本不知道它到底哪一步断了。这篇把重构前后的差异和踩过的坑完整写下来,给同样在维护无人值守脚本的人做个参考。

背景是这样:脚本每天凌晨跑一次,抓十几个目标站点,把结果写进 SQLite 再生成一份汇总。半年来基本能用,但有三类问题反复出现。第一类是静默失败,站点改版导致选择器返回空列表,脚本照样正常退出、状态码是 0,第二天看汇总才发现整天的数据是空的。第二类是部分失败被当成成功,十个站点里挂了三个,脚本继续往下走,最后写进库里的是一份不完整的数据,而且没有任何地方标记它是残缺的。第三类是重跑污染,发现问题后手动再跑一次,第一次的半截数据还留在表里,第二次又追加一遍,同一个日期出现两份记录,后面统计就彻底乱了。

重构的思路很朴素,分三步走,每一步都是为了让「失败」变得可见、让「重跑」变得安全。

第一步是给每个站点单独包一层结果记录,而不是让异常往上冒到整个脚本。具体做法是每个采集单元返回一个三元组:站点名、状态、条数或错误信息,全部收集到一个列表里,循环结束后统一落一张 runs 表。这一步最大的收益是区分了「零结果」和「没执行」——前者可能是站点真的没新内容,后者一定是出了问题。我在这里踩的第一个坑就是:一开始把空列表和异常都记成 fail,结果每天都有几个站点报错,查了半天发现是那几个站点确实没有新数据,天天误报。后来改成空结果单独记 empty,异常才记 fail,报警噪音立刻降下来了。

第二步是把「完整性」变成可校验的条件,而不是靠人眼去扫。每个任务结束时算一个覆盖率,也就是成功站点数除以预期站点数,低于阈值就整体标成 degraded 并在汇总里顶格提示。另外用当天日期做唯一约束,重跑时先删当天再写入,这样无论跑几次,库里永远只有一份当天的数据。这个 DELETE + INSERT 必须在同一个事务里,否则中途失败会留下一个既没旧数据也没新数据的空窗期,这一点我是被坑过才加上的。

第三步是让脚本自己汇报,而不是等人去翻日志。任务跑完最后打印一行结构化的总结:总数、成功、空、失败、耗时、状态。这行总结是唯一需要人看的东西,日志文件只在状态不是 ok 的时候才去翻。这样一来,每天早上的检查动作从「打开日志往上翻几十行」变成了「看一眼最后一行」,三十秒能判断清楚昨晚到底有没有问题。

几个关键结论,都是实际跑过之后才确认的。一是「空结果」和「失败」必须分开记,混在一起要么噪音太大没人看,要么漏掉真正的故障。二是任何会被重复执行的写入,都要先想清楚幂等怎么做,唯一约束加事务删除是成本最低的一种。三是脚本的最终输出应该是一行状态摘要,而不是一堆滚动的日志,前者能被人坚持看下去,后者迟早会被忽略。四是阈值报警比逐条报警有用得多,覆盖率低于某条线才喊人,比每个站点单独喊人要少掉九成噪音。

评论 (2)

Meicheng · 站长
Meicheng · 站长 2026年09月19日
这套「自检 + 幂等」的思路我深有体会,补两个同源但更隐蔽的坑:一是「成功退出但数据为空」这类静默失败,最好在脚本末尾加一道『数据量断言』——对比当天写入行数和近 7 天均值,低于阈值就主动告警,而不是等第二天人肉发现。二是重跑污染,除了清半截数据,更稳的是让每次运行都写入一个 run_id,统计时只认最新一次 run,旧数据自然失效。另外状态码 0 不代表成功,我会让关键步骤显式写『完成标记』到日志,缺了标记就按失败处理。这些说起来都不难,难在坚持每段逻辑都想一遍『它失败时怎么被我发现』。
Meicheng · 站长
Meicheng · 站长 2026年09月20日
针对「部分失败被当成成功」这个坑,我补一个具体做法:给每个子任务设独立状态位,最后汇总时不是简单看 exit code,而是校验「成功数 == 总数」。不等就标记成 partial,告警里直接列出挂了哪几个站点,别让残缺数据静默入库。

登录 后参与评论