蜘蛛来抓页面,表面上看是一次 HTTP 请求换回一份 HTML。但在这两者之间,还夹着一层容易被忽略的东西:传输编码。页面往往是先被压缩、再被切成若干块发出去的,蜘蛛要先完成解码,才能看到一个标签闭合完整的文档。这一层出问题,通常不会报错,只会让抓取变得比预想的更费劲。
蜘蛛在请求头里说的那句话
抓取请求一般会带上 Accept-Encoding: gzip, deflate, br 这类声明,意思是这些解码方式我都能处理。服务器可以选择压缩,也可以选择不压缩,压缩本身是可选项。但一旦响应头里声明了某种编码,正文就必须真的按那个格式发出来。
声明与实现不一致的后果
如果响应头写着 Content-Encoding: br,正文却是普通的 gzip 流或者干脆是明文,解码就会失败。蜘蛛拿到的是乱码,这一次抓取基本只能作废,等下次重访再来。麻烦在于,服务器日志里这次请求是成功的 200,传输也完成了,两边记录对不上,排查时最容易走偏。
压缩省下的是字节,不是请求数
压缩不改变蜘蛛发起的请求次数,它影响的是每趟能带回去多少内容。抓取资源有限时,单位时间内能拉取的页面数,和单页的字节数直接相关。
- 文本类页面经过 gzip 后,常见体积会降到原来的两三成,收益明显。
- 已经压过的资源,比如图片、视频、woff2 字体,再压几乎没有效果,还白耗 CPU。
- 压缩级别从最高档降到中档,体积差别通常不大,CPU 占用却会明显下降。
所以真正值得优化的是 HTML、CSS、JS 和接口返回的 JSON,而不是把所有响应无差别地压一遍。
分块传输:Content-Length 缺失意味着什么
开了动态压缩之后,响应长度在发送前是不确定的,服务器往往会改用 chunked 传输,不再给出 Content-Length。这对蜘蛛来说不算问题,它按块读取,直到读到结束标记为止。真正需要留意的是——块什么时候来。
有些后端会在渲染出一部分内容后就 flush 一次,如果中途某个环节卡住,蜘蛛会一直挂着这条连接等下一块。等不到,超时后这次抓取就白跑了。日志里表现为响应时间很长、状态码却仍是 200,单看状态码很难发现。
动态压缩与缓存:别让蜘蛛每次都触发一遍
每次请求都现场压缩,CPU 会上去,首字节时间也会变长。常见做法是在前置层做压缩,并让缓存键包含编码方式这一维度。
如果缓存只按 URL 存一份,压缩又按请求头临时决定,就可能把 br 编码的响应发给只支持 gzip 的客户端,或者反过来。前者直接解码失败,后者只是多传了些字节,影响程度完全不同。
几个容易踩的坑
- 双重压缩:源站已经压过一次,前置层又压了一次,解码出来仍是压缩流。
- 对 304 响应重新压缩,导致 ETag 与实际内容对不上,蜘蛛只能重新拉全量。
- 给小体积页面开最高压缩级别,收益有限,却拖长了首字节时间。
- 只在测试环境开了 br,线上没开,两边行为对不上,问题难以复现。
可以这样自查
- 用 curl --compressed 拉一次页面,看返回的是否为可读的 HTML。
- 对比压缩前后的字节数,确认压缩真的生效,而不是只写了响应头。
- 检查 Content-Encoding 与实际内容是否一致,尤其是有多层代理时。
- 看日志中同一批 URL 的响应时间分布,是否存在明显的长尾超时。
- 确认压缩发生在哪一层,缓存键是否包含编码方式。
传输编码不是抓取策略的核心,但它决定了蜘蛛每次来能带回去多少东西。压缩对得上、分块发得干净、首字节别拖太久,后面的抓取安排才有讨论的余地。
反过来也成立:页面结构、内链、Sitemap 都做得不错,却卡在传输这一环,抓取量上不去,排查时往往要绕很大一圈才能找到真正的原因。