打开网易新闻 查看精彩图片

凌晨3点,你的冲浪预警系统突然哑火。某个海浪数据网站把class名里的"heigth"改成了"height",你的CSS选择器瞬间失效。用户收不到警报,工程师被Slack吵醒,查日志、找问题、改代码、部署——2小时没了,而实际修复只需要5分钟。

这不是技术债,是摩擦税。一个Python sidecar系统正在消灭这种税:爬虫崩溃时自动唤醒本地大模型,现场诊断HTML,生成新选择器,验证通过直接入库。从报错到自愈,全程无人值守。

为什么选择器失效是"沉默的杀手"

为什么选择器失效是"沉默的杀手"

生产级爬虫有个隐藏脆弱性:它的CSS选择器和XPath表达式,是针对第三方网站某一时刻DOM结构的快照写的。对方改版、改名、重构表格——你的选择器要么返回空,要么返回错数据,而你的监控可能根本察觉不到。

冲浪预警系统要盯几十个预报源。一个拼写错误修复(heigth→height),就能让整条管道在凌晨停滞。工程师的修复流程高度仪式化:被叫醒、翻日志、定位选择器、写修复、走部署。核心工作5分钟,上下文切换2小时。

这套系统的作者把问题定性得很准:不是规模问题,是摩擦问题。修复本身是机械劳动——看新HTML、找元素、写选择器。难点在于把这个循环自动化,且保证安全。

自愈流程:从崩溃到修复的90秒

自愈流程:从崩溃到修复的90秒

Ruby爬虫提取字段失败时(比如wave_height返回nil),会向Redis队列抛出一个修复任务。Python sidecar——Self-Healer——消费这个任务,抓取当前HTML,裁剪到token预算内,向本地MLX运行的LLM发送定向提示词。

LLM返回带置信分数和推理过程的候选选择器。每个候选都要过两道筛子:先用BeautifulSoup和lxml对实时HTML做确定性验证,再用类型schema校验提取值(比如wave_height必须是0.1-20.0之间的浮点数)。通关的候选直接写入PostgreSQL的data_sources.selector_overrides表,无需重新部署。下一轮爬虫读取更新后的配置,继续运行。

系统设计的核心约束很克制:模型只提建议,代码决定生死。LLM输出被当作"实习生提交的PR":值得一看,但合并前必须审查。

这很重要,因为LLM错得自信。它可能生成一个看起来合理的XPath,实际匹配了表格的错误列——把表头文字"Wave Height (m)"当成数据提取出来。类型校验会拦截:"Wave Height (m)"无法通过float:0.1-20.0,候选被驳回。

为什么本地MLX、为什么零延迟验证

为什么本地MLX、为什么零延迟验证

候选选择器在进程内对抓取到的HTML做验证,使用与爬虫相同的解析库。没有预发环境,没有影子流量,没有A/B灰度。沙盒即闸门——不匹配当前HTML、或提取值不通过schema的选择器,进不了数据库。

选择本地MLX而非云端API,出于三个现实考量:数据不出境(抓取的目标HTML可能含敏感信息)、延迟可控(修复窗口期可能只有几分钟)、成本可预测(批量小任务云端计价不划算)。

token预算的裁剪策略也值得细说。原始HTML常超上下文窗口,系统保留语义骨架:标签层级、关键class/id、表格结构,丢弃样式属性和脚本块。这既省token,也减少LLM被噪声干扰的概率。

边界与未解决的问题

边界与未解决的问题

系统明确不处理两类场景:页面结构根本性重组(比如表格变卡片流,需要重新理解业务逻辑),以及反爬升级(验证码、IP封禁、行为检测)。这些是"需要人介入"的信号,系统选择让爬虫失败并告警,而非假装修复。

另一个设计取舍是schema的维护成本。每个字段需要人工定义类型约束(float范围、正则模式、枚举值),这是系统安全性的前提,也是接入新数据源时的固定开销。作者没有回避这一点,反而强调"schema即契约"——没有契约,自动修复就是自动犯错。

当前运行数据显示,约73%的选择器失效可在无人干预下自愈,平均修复时间从2小时降至4分钟。剩余27%的复杂变更,系统通过结构化日志和候选选择器历史,将人工排查时间压缩到15分钟以内。

这套系统的真正价值或许不在技术本身,而在它对"自动化边界"的清醒认知:让机器做它擅长的(模式匹配、快速迭代),让人做他擅长的(判断结构重组、承担最终责任)。

当你的爬虫下次在凌晨3点崩溃,你希望它自己爬起来,还是继续叫醒你?