bigywater.tech
Scrapy · 27

HttpCompressionMiddleware:压缩响应的解压

服务器发来压缩的响应,Scrapy 自己解压。支持哪些编码?叠加编码怎么处理?压缩体坏了、编码不认识会怎样?别人发一个 20 KB 的炸弹包要解压成 20 MB,能防住吗?这篇逐个实测。

5 种
2.19 能解的 Content-Encoding(gzip/x-gzip、deflate、br、zstd;外加叠加)
20,406 → 20,971,520
解压炸弹测试里压缩体与解压后的字节数
BadGzipFile
服务器声称 gzip 其实没压缩时的错误

支持的编码与异常情形

实测 · 本机执行输出
  服务器的编码                           传输字节   回调 body     与原文相同  Content-Encoding 头(解压后)  响应类 / 错误
  gzip                              116     12026         是  (无)                     HtmlResponse
  x-gzip                            116     12026         是  (无)                     HtmlResponse
  deflate(zlib 封装)                  104     12026         是  (无)                     HtmlResponse
  deflate(裸 deflate)                 98     12026         是  (无)                     HtmlResponse
  br                                 55     12026         是  (无)                     HtmlResponse
  zstd                               74     12026         是  (无)                     HtmlResponse
  叠加 gzip, br(先 gzip 再 br)          120     12026         是  (无)                     HtmlResponse
  不支持的 compress                      10        10         否  compress                Response
  声称 gzip 其实没压缩                   12026         -         -  BadGzipFile: Not a gzipped file (b'<h')
  截断的 gzip                           60        28         否  (无)                     HtmlResponse
  无压缩                             12026     12026         是  (无)                     HtmlResponse
  服务器看到的 Accept-Encoding:['gzip, deflate, br, zstd']
  stats:httpcompression/response_bytes=84220, httpcompression/response_count=9
  警告:[scrapy.downloadermiddlewares.httpcompression] WARNING: HttpCompressionMiddleware cannot decode the response for http://127.0.0.1:9960/x?k=%E4%B8%8D%E

请求里的 Accept-Encoding 写死 gzip, deflate, br, zstd。响应按 Content-Encoding 解压,叠加编码从最后一个往前解,裸 deflate 也能处理;解出的 body 替换原 body,Content-Encoding 头被删除,响应类重新判定。不认识的编码原样保留并警告;数据损坏会抛出对应的异常(如 BadGzipFile),由 errback 接住;被截断的流返回已解出的部分。

大小保护

实测 · 本机执行输出
  服务器发送 20,406 字节(gzip),解压后 20,971,520 字节
  默认(MAXSIZE 1 GiB、WARNSIZE 32 MiB):callback 20971520;日志 [];stats {'httpcompression/response_bytes': 20971520, 'httpcompression/response_count': 1}
  DOWNLOAD_WARNSIZE = 10 MiB:callback 20971520;日志 ['[scrapy.downloadermiddlewares.httpcompression] WARNING: <200 http://127.0.0.1:9960/x?k=BOMB> body size after decompression (20971520 B) is larger than'];stats {'httpcompression/response_bytes': 20971520, 'httpcompression/response_count': 1}
  DOWNLOAD_MAXSIZE = 10 MiB:errback IgnoreRequest: Ignored response <200 http://127.0.0.1:9960/x?k=BOMB> because its body;日志 ['[scrapy.downloadermiddlewares.httpcompression] WARNING: Ignored response <200 http://127.0.0.1:9960/x?k=BOMB> because its body (20406 B compressed, 10'];stats {}

解压按块进行,超过 DOWNLOAD_MAXSIZE 就放弃并丢弃响应;超过 DOWNLOAD_WARNSIZE 只记警告。默认上限 1 GiB、警告 32 MiB。

例外

实测 · 本机执行输出
  HEAD /x?k=gzip:回调 [{'n': 0, 'ce': 'gzip', 't': 139.4}]
  COMPRESSION_ENABLED = 0:服务器看到的 Accept-Encoding = None;服务器仍发了 gzip,回调拿到 116 字节(开头 1f8b = gzip 魔数 1f8b),Content-Encoding = gzip

HEAD 请求不解压;COMPRESSION_ENABLED = 0 时不发 Accept-Encoding 也不解压。

分步演示

压缩响应
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动(brotli 1.2.0,Python 3.14 自带 compression.zstd)。服务器用 gzip、zlib、brotli、compression.zstd 压缩同一段文本;炸弹是 20 MiB 的重复字节 gzip 后
    • 没有验证:Python < 3.14 时 zstd 需要 backports.zstd;brotlicffi 回退;流式下载大文件时的压缩(stream);Transfer-Encoding: chunked 与压缩的组合;aiohttp / httpx 下载处理器自带的解压是否与中间件重复;真实站点上 Content-Encoding 头大小写、多值头的各种写法
    京ICP备2026067502号-1京公网安备11010502063487号