蜘蛛抓取页面时,最先拿到的是 HTTP 响应头和传输层的数据流,正文内容要等压缩解码和分块拼接全部完成后才算完整。服务器或 CDN 在这两个环节配置不一致,很容易让蜘蛛拿到半截内容,或者干脆判定这次抓取失败。
压缩与分块为什么会卡住抓取
传输方式大体有两种:一种是整段响应配 Content-Length,另一种是分块传输(Transfer-Encoding: chunked)边生成边发送。前者要求服务器先把内容长度算准,后者允许动态输出。压缩则分 gzip、deflate、br 等,声明在 Content-Encoding 里。
问题往往出在声明与实际不一致:响应头说压缩了,实际发的是明文,或者反过来;chunked 的分块长度写错、结尾的 0 块缺失;压缩流被中途截断又没有正确的结束标记。解码器遇到这类数据会放弃当次抓取,日志里表现为已请求但取不到正文。
常见异常表现
- 状态码是 200,但抓取到的正文长度接近 0 或明显偏短。
- 响应头与响应体不匹配:Content-Encoding 声明 br,内容却是未压缩的 HTML。
- 同一 URL 换不同 User-Agent 请求,返回的传输方式不一样。
- 页面在浏览器里正常,用带 --compressed 的命令行请求取回来却是乱码或截断。
- 只有部分节点或部分机房出现,换 IP 重试就恢复正常。
排查顺序
- 先取原始响应头。用 curl -I 或带 --compressed 的完整请求,确认 Content-Encoding、Transfer-Encoding、Content-Length 三个字段是否互斥且自洽。Content-Length 与 chunked 同时出现,本身就是配置冲突。
- 比较压缩前后的字节数。关掉压缩取一次,开启压缩取一次,看解码后的长度是否一致。差异过大说明压缩流有截断。
- 检查分块结尾。chunked 必须以长度为 0 的块结束,后面可以跟 trailer。缺失结束块会让客户端一直等待,最终超时。
- 核对反向代理与源站的压缩层级。源站压一次、CDN 再压一次形成双重压缩,客户端只解一层就会得到乱码。
- 区分动态与静态路径。静态文件由 Nginx 直接压缩,动态接口由应用层输出,两边的缓冲区大小和 flush 时机可能不同。
- 固定小样本反复对比。挑几个有代表性的 URL(列表页、详情页、带参数的页),在多个节点上重复取样,而不是只看单次结果。
配置上容易踩的坑
缓冲区与 flush 时机
应用层如果设置了过大的输出缓冲,又依赖定时 flush,遇到进程回收或超时中断时,缓冲区里的内容就丢了。表现是响应看似完成,实际缺少尾部 HTML,而内链往往正好在尾部。
压缩级别的边界情况
最高压缩级别在部分实现上会加大内存占用,反而更容易触发超时。对蜘蛛访问的路径,用中等压缩级别通常更稳。
错误页的处理
出错时返回的页面如果还带着正常的压缩头,长度又对不上,客户端会把它当成正常响应的一部分。建议错误响应显式关闭压缩,并给出准确的 Content-Length。
不同抓取工具对传输错误的容忍度差异很大,同一种异常可能一个直接失败、一个拿到残缺内容。观察时以原始响应为准,不要只看渲染后的页面。
修复后的观察方式
改完之后,先看服务器访问日志里蜘蛛请求的响应字节数是否回到正常区间,再看抓取统计中内容为空的 URL 数量是否下降。这个过程通常需要几天到几周,取决于站点被抓取的频率,不要期待改完立刻见效。
建议把这条链路写进上线检查清单:任何涉及传输层配置的改动,发布后都用一条固定样本 URL 做回归,同时覆盖压缩与不压缩两种请求。这样下次出问题,能快速分清是压缩、分块还是缓存引起的。