先给有条件的结论:如果静态响应里已经有正文文本,而脚本渲染后反而多出或替换了内容,通常应优先怀疑“服务端输出被前端覆盖”,而不是爬虫没有执行脚本。这个判断只在一种前提下成立——你比较的是同一个 URL、同一组请求头、同一时刻的两次抓取,且没有登录态、地域或 A/B 分流干扰。一旦样本扩大到多个页面,这个结论就可能失效,因为不同模板、不同缓存状态和不同渲染入口会各自产生例外。
定位差异的第一步不是看代码,而是让两次观察可比。用同一 URL 分别取原始 HTML 和渲染后的 DOM,记录请求头里的 User-Agent、Accept-Language、Cookie 是否存在,以及响应状态码和重定向链。如果原始请求被 301 到带斜杠的版本,而渲染请求停在无斜杠版本,你看到的“内容不同”其实来自两个不同资源,不是渲染造成的。
一个可执行动作是:对同一 URL 连续取三份快照——原始 HTML、禁用脚本后的 DOM、启用脚本后的 DOM。把三份文本按段落切分后做差集。如果差集只出现在脚本启用后的版本,说明内容由前端注入;如果差集在禁用脚本时已经存在,说明差异来自服务端或缓存层。这个动作的结果决定下一步:前者去查数据接口和渲染依赖,后者去查缓存键和模板分支。
静态与渲染结果不同,通常落在三类成因里,每类的验证方式不一样。
这三类的区分点是:差异出现在脚本执行前还是执行后,以及差异是“多出内容”还是“丢失内容”。多出内容偏向注入,丢失内容偏向依赖失败,两边都有增删则优先查缓存版本。
假设你在一个商品详情页上验证:静态 HTML 有标题和价格,渲染后价格被接口更新。你据此判断“价格由前端注入”,并准备只针对价格字段调整输出。这个判断在单页上成立,但规模化后可能失效。
反例条件是:站点对部分页面做了服务端预渲染,对另一部分页面走纯客户端渲染,而两类页面共用同一套模板路径。此时你抽到的样本恰好属于纯客户端渲染,于是得出“全部由前端注入”的结论;但预渲染那批页面的静态响应里价格已经是最终值,渲染只是补充推荐位。若照搬单页结论去改输出,预渲染页面会被重复处理,而纯客户端页面仍未解决。
使结论失效的边界很清楚:当页面存在多种渲染模式且无法从 URL 结构直接区分时,单页结论不能外推。要打破这个边界,需要按模板或路由分组抽样,而不是按页面随机抽样。分组后如果每组内部差异模式一致,才可以对每组分别下结论。
页面文本差异只是表象,交叉验证才能定位到具体环节。可用的证据包括:
Content-Length 与渲染后 DOM 序列化长度是否同量级。差距过大说明内容主体不在初始响应里。这里要避免一个误判:抓取量或某接口请求量归零,不能单独证明你定位正确。归零还可能来自限流、请求头被拒、资源被合并或统计口径变化。要排除这些解释,至少需要同时看到响应状态和响应体结构。
定位完成后,动作应落到具体分组上。对确认由前端注入的页面,检查接口是否可被爬虫请求、是否需要特定请求头;对确认依赖失败的页面,检查被拦截的资源是否属于渲染必需项;对缓存错位的页面,统一缓存键或发布顺序。
修复后复验必须回到最初的三份快照口径:同一 URL、同一请求头、同一时刻。只有当静态响应与渲染结果的差集缩小到预期范围,且分组内不再出现反向差异,才能认为该组处理有效。若复验中某组仍出现“静态有、渲染无”,说明依赖问题未解决,应回到网络请求记录继续查,而不是调整内容输出。