我们跑了15000次浏览器自动化:最危险的失败,你的监控完全看不见

2 阅读1分钟

原文:taprun.dev

YouTube 自动化有一半的运行返回 0 行数据。状态:ok。没有异常,没有错误日志。程序跑了 20 秒,静静地返回一个空数组。

我们直到看了 trace 日志才发现这件事。

没人发布的可靠性数据表

以下是真实数字,非 benchmark。硬错误率:抛出异常的运行比例。静默空结果率:状态为 ok 但返回 0 行的运行比例。

平台总运行次数硬错误率静默空结果率综合失败率平均耗时
Twitter / X1280%0%0%154 ms
GitHub4370%0.2%0.2%3,644 ms
Reddit68813.8%6.4%19.4%4,075 ms
小红书36115.8%6.6%21.5%9,054 ms
Bilibili25930.1%18.2%43.1%2,666 ms
微博3836.8%0%36.8%4,644 ms
YouTube4930.6%50.0%65.3%20,273 ms

GitHub 和 Twitter 几乎零失败。YouTube 则是三分之二的运行要么报错,要么返回空。

你没在追踪的失败模式

实际数字:

  • Element not found(显式 selector 失败):5 次
  • Cannot read properties of undefined (reading 'url')(隐式结构漂移):176 次

比例是 35:1,而你的监控抓不到那个 35。

这是结构漂移失败,不是 selector 失败。DOM 元素在,页面加载了,程序遍历了正确的节点,但这些节点返回的数据结构变了——一个一直存在的字段悄悄消失了。

受影响的站点:Bilibili、Algora、IssueHunt、抖音、知乎、X/Twitter、小红书、微博、百度、Hacker News、ProductHunt、TechCrunch、Ars Technica。

为什么你的监控看不见

发生这个失败时,基础设施层看到的是:

  • HTTP 响应:200
  • 页面加载成功:是
  • 进程退出码:0
  • 抛出异常:有——但只在 extraction 之后,下游代码访问格式错误的对象时才发生

更难的情况:对象确实有 url 字段——只是指向了错误的地方——相关推荐区、广告、分页链接。状态 ok,有行数据,没有异常,数据错了。Pydantic 通过。Prometheus 报告健康。OTel 没有任何异常。

平台可靠性规律

GitHub 和 Twitter 有稳定的公开 API,Web UI 反映的就是底层数据模型。

Bilibili、抖音、小红书、微博在渲染层做激进的 A/B 实验——有时同一个用户同一个页面刷两次就能拿到不同的 DOM 结构。url 字段可能在一个实验组里是 item.url,在另一个实验组里是 item.jumpUrl

YouTube 是另一种情况:反爬措施会返回空结果而不是拦截请求。没有登录的浏览器 session 得到的是 200 + 空内容容器。状态 ok。行数 0。20 秒的计算白跑了。

什么能抓到,什么抓不到

工具硬错误静默空结果数据错误(格式正确)
进程监控
Pydantic / 类型校验有时
行数阈值
健康合约(range + pattern + drift)
结构指纹(tap doctor)发现变化,不解释含义

如果重来会怎么做

把静默空结果当作一等公民失败。 min_rows 合约可以立刻发现。

在运行前做指纹检查,而不是运行后。 结构漂移在执行 extraction 前就可以在 DOM 里检测到。指纹检查比一次完整 tap 执行便宜得多。

把中文平台作为独立的可靠性层级。 A/B 实验频率本质上不同——Bilibili 的 tap 需要比 GitHub 的 tap 更短的漂移窗口和更频繁的健康检查。

耗时是信号。 YouTube tap 平均 20 秒,失败率 65%。那不是慢提取,是在等一个不会来的内容。8 秒超时合约可以提前拦截大部分。


15,455 次运行的 trace 数据,是对「浏览器自动化到底会在哪里坏掉」最诚实的回答:

静默的结构漂移,不是显式的 selector 失败。变化最快的站点失败最多。最重要的失败,看起来像成功。


Tap 构建——一次写好,永远运行的浏览器自动化程序。tap doctor 在运行前检测结构变化,health contracts 在运行后验证语义正确性。