Accept-Encoding 看起来是个“自动的事”,但它决定了你从服务器拿到什么、省多少流量、遇到压缩炸弹时会不会把内存撑爆。这篇在本机用 gzip、deflate、zstd、br 各传一遍同一个页面,再试试没装库的客户端和一个 64 MiB 的压缩炸弹。
没装 brotli:Accept-Encoding: gzip, deflate, zstd 装了 brotli:Accept-Encoding: gzip, deflate, br, zstd (zstd 不需要另外装:Python 3.14 标准库自带 compression.zstd;这里装的 brotli 版本 1.2.0)
requests 通过 urllib3 声明自己能解哪些压缩:没装 brotli 时是 gzip, deflate, zstd,装了 brotli 后多出 br。zstd 来自 Python 3.14 标准库的 compression.zstd。因此换一台机器、装一个依赖,爬虫发给服务器的请求头就变了,这对排查“为什么这台机器上拿到的页面不一样”很重要。
编码 线上字节 占原始比例 解压后字节 解压结果与原文一致 identity 291470 100.0% 291470 True gzip 21308 7.3% 291470 True deflate 21296 7.3% 291470 True zstd 14369 4.9% 291470 True br 9196 3.2% 291470 True
对 29 万字节的重复 HTML,gzip 和 deflate 都压到约 7.3%,zstd 4.9%,br 3.2%。这些数字依赖我选的压缩参数(gzip 6、zstd 默认、br quality 5)和这份高度重复的数据,不要当成通用比例。服务端实际用什么级别,取决于它自己的配置。
编码 线上字节 占原始比例 identity 291470 100.0% 一致=True gzip 291578 100.0% 一致=True deflate 291566 100.0% 一致=True zstd 291488 100.0% 一致=True br 291475 100.0% 一致=True
随机字节(也近似已经压缩过的图片、视频)用任何方式压缩都不会变小,反而多出几十到一百多字节的头尾。对文本页面启用压缩有价值,对图片下载没有。
没装 brotli:Content-Encoding=br 线上 9196 字节,r.content 长度 9196,与原文一致=False 装了 brotli:Content-Encoding=br 线上 9196 字节,r.content 长度 291470,与原文一致=True
服务器本不该发客户端没声明的编码,但如果发了,没装 brotli 的客户端不会抛异常,r.content 是原样的压缩字节(9196 字节),和原文不一致。如果你的解析代码直接处理这些字节,得到的只是乱码。检查 Content-Encoding 与实际内容长度是否匹配,可以尽早发现。
线上传输的字节数(压缩后)= 65250 普通 get:r.content 67108864 字节,耗时 49 ms,进程峰值内存 180 MiB stream + 读满 1 MiB 就停:读到 1048576 字节,耗时 2 ms,进程峰值内存 34 MiB
一个 65250 字节的 gzip 响应能解压成 64 MiB。普通 requests.get 把它全部放进内存,进程峰值 180 MiB;stream=True 读到上限就停,峰值只有 34 MiB。对不可信的站点,把读取量限制在合理范围内(这里只演示了按解压后的字节数截断,没有测别的防护方式)。