bigywater.tech
Scrapy · 21

HTTP 缓存:HTTPCACHE 的存储与策略

开了 HTTPCACHE_ENABLED 之后,到底缓存了什么、什么时候失效、磁盘上长什么样?默认的 DummyPolicy 与 RFC2616Policy 差别在哪?这篇把两种策略对 no-store、max-age、ETag、404 等逐个实测,还看了缓存兜底下载失败。

0 个
开缓存后第二次运行,服务器收到的请求数(3 个全部命中)
7 个
每个缓存请求在磁盘上的文件数(FilesystemCacheStorage)
304 → 命中
RFC2616Policy 下 ETag / Last-Modified 重新验证的结果:用缓存正文

缓存的开关与效果

实测 · 本机执行输出
  第 1 次运行:服务器收到 3 个请求;cache stats {'firsthand': 3, 'miss': 3, 'store': 3};响应 flags [()]
  第 2 次运行:服务器收到 0 个请求;cache stats {'hit': 3};响应 flags [('cached',)]
  HTTPCACHE_ENABLED=0(同一个目录):服务器收到 1 个请求;cache stats {}

开启后第一次是 miss,响应被存下;再次请求(同一个指纹)命中,不去服务器,响应的 flags 里有 cached。

磁盘布局

实测 · 本机执行输出
  s/ce/ce204639…/meta                            143 字节
  s/ce/ce204639…/pickled_meta                    113 字节
  s/ce/ce204639…/request_body                      0 字节
  s/ce/ce204639…/request_headers                 183 字节
  s/ce/ce204639…/response_body                    15 字节
  s/ce/ce204639…/response_data                   168 字节
  s/ce/ce204639…/response_headers                 94 字节
  response_body = b'<html>ok</html>'
  response_headers 的头两行 = ['Content-Length: 15', 'Server: plab']
  pickled_meta 里的键 = ['method', 'response_url', 'status', 'timestamp', 'url']

每个请求一个目录,里面分别存请求头体、响应头体、元数据和 pickle 的附加字段;文件名按请求指纹组织。

DummyPolicy

实测 · 本机执行输出
  第二次运行服务器仍收到的路径:(无);stats {'hit': 8}
  HTTPCACHE_IGNORE_HTTP_CODES=404,500:第二次运行服务器收到 ['/404', '/500', '/500', '/500'];stats {'firsthand': 4, 'hit': 4, 'miss': 4, 'uncacheable': 4}

默认策略,不管状态码和 Cache-Control,全部缓存且永不过期,适合开发调试时反复回放。

RFC2616Policy

实测 · 本机执行输出
  第二次运行服务器收到的请求(及条件头):
    /nostore  If-None-Match=(无)  If-Modified-Since=(无)
    /etag     If-None-Match="v1"  If-Modified-Since=(无)
    /ok       If-None-Match=(无)  If-Modified-Since=(无)
    /lm       If-None-Match=(无)  If-Modified-Since=Mon, 01 Jan 2024 00:00:00 GMT
  stats {'firsthand': 2, 'hit': 1, 'miss': 2, 'revalidate': 2, 'store': 2, 'uncacheable': 2}
    回调:/etag     status=200 flags=['cached'] body='<html>etag body</htm'
    回调:/lm       status=200 flags=['cached'] body='<html>lm</html>'
    回调:/maxage   status=200 flags=['cached'] body='<html>ma</html>'
    回调:/nostore  status=200 flags=[] body='<html>ns</html>'
    回调:/ok       status=200 flags=[] body='<html>ok</html>'

按 HTTP 缓存规则:no-store 不缓存,max-age 内直接命中,过期后用 ETag / Last-Modified 做条件请求,304 则复用缓存正文。

过期与排除

实测 · 本机执行输出
  立即再跑:服务器收到 0 个;{'hit': 1}
  3 秒后再跑:服务器收到 1 个;{'firsthand': 1, 'miss': 1, 'store': 1}

HTTPCACHE_EXPIRATION_SECS 控制过期,HTTPCACHE_IGNORE_HTTP_CODES 排除某些状态码,meta dont_cache 跳过单个请求。

只读缓存

实测 · 本机执行输出
  dont_cache=True:服务器收到 1 个;{}
  HTTPCACHE_IGNORE_MISSING=1:服务器收到 0 个;{'hit': 1, 'ignore': 1, 'miss': 1};回调 ['/ok'],错误 [('/never-seen', 'IgnoreRequest')]

HTTPCACHE_IGNORE_MISSING 让缓存里没有的请求直接被忽略,可以用来离线重放。

失败兜底

实测 · 本机执行输出
  第二次运行服务器已关闭:stats {'errorrecovery': 1, 'miss': 1};回调 [('/etag', 200, ['cached'])];错误 [('/ok', 'DownloadConnectionRefusedError')]

下载异常时如果有旧缓存,会返回旧缓存并计入 httpcache/errorrecovery。

分步演示

HTTP 缓存
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。每个情形把同一个爬虫跑两次(同一个缓存目录)比较服务器收到的请求;RFC2616 的服务器按 ETag / If-None-Match 与 Last-Modified / If-Modified-Since 返回 304
    • 没有验证:DbmCacheStorage(dbm 存储)和 HTTPCACHE_GZIP;HTTPCACHE_ALWAYS_STORE;HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS;缓存目录的并发读写;RFC2616Policy 对 Age、Expires、Vary 头的处理;缓存里重定向链的存储(只确认了 8 个命中包含 302 与它的目标);大量请求时目录数量(每个请求 7 个文件)对文件系统的压力
    京ICP备2026067502号-1京公网安备11010502063487号