资讯动态

Python 爬虫 ETag 实战:正确处理 304 与本地缓存,附 5 个可运行验证

发布时间:2026/9/15 9:00:53 来源:尧图企业网站定制
定时采集同一个接口时即使内容没有更新客户端也可能每次下载正文、解析 JSON再重复写入。服务端支持 ETag 时可以通过条件请求减少重复传输但代码如果只保存 ETag没有保存对应正文收到 304 后反而无法交付数据。本文把这个问题缩到一个本地 JSON 接口首次下载、内容未变、内容变化、缓存丢失以及新正文损坏。示例仅使用 Python 标准库2026-09-08 在 Python 3.9.6 下执行通过服务绑定 127.0.0.1 随机端口不访问外站也不需要第三方账号。1. ETag、If-None-Match 与 304 分别负责什么ETag 是服务端给当前表示的验证标记。客户端保存它下一次 GET 时原样放入 If-None-Match条件匹配时服务端可以返回 304客户端复用已有正文。304 本身没有正文所以不能把它的空响应覆盖进缓存。ETag 也不保证是文件 MD5不要自行去掉引号或重新计算后替换服务端值。参见 RFC 9110ETag、If-None-Match 与 304。这个实验的缓存条目包含两项服务端返回的 ETag以及通过业务校验的原始正文。二者必须属于同一次成功响应。只更新标记、不更新正文会把旧数据伪装成新版本。2. 先把实验契约写清楚代码只有一个固定 GET /doc固定 JSON 表示无登录态、无重定向、无压缩和语言协商。服务端用正文的 SHA-256 生成强 ETag客户端只回传这个单一标记服务端中的字符串比较只覆盖此实验输入。完整 HTTP 的 If-None-Match 需要弱比较还支持标记列表和星号。本例没有实现这些分支也不应把 Handler 拿去当通用条件请求服务。相关语义见 RFC 9110 第 13.1.2 节。另外客户端每次都联系本地服务器。它没有实现新鲜度判断、304 响应头合并、Vary、no-store 等缓存规则通用 HTTP 缓存需要遵循 RFC 9111。这里的重点是应用内一个已知端点的“正文与验证标记一起更新”。3. 完整本地示例保存为 etag_demo.py运行python3 etag_demo.py。代码使用 assert 验证结果请不要加-O。Local ETag experiment, not a general-purpose HTTP cache.importhashlibimporthttp.clientimportjsonimportthreadingfromdataclassesimportdataclassfromhttp.serverimportBaseHTTPRequestHandler,HTTPServerdataclass(frozenTrue)classEntry:etag:strbody:bytesstate{body:b{version:1},force304:False}classHandler(BaseHTTPRequestHandler):deflog_message(self,*args):passdefdo_GET(self):bodystate[body]etaghashlib.sha256(body).hexdigest()# This fixture accepts one unchanged strong tag from our own client.unchangedself.headers.get(If-None-Match)etag status304ifunchangedorstate[force304]else200self.send_response(status)self.send_header(ETag,etag)self.send_header(Cache-Control,no-cache)ifstatus200:self.send_header(Content-Type,application/json)self.send_header(Content-Length,str(len(body)))self.end_headers()ifstatus200:self.wfile.write(body)deffetch(port,oldNone):headers{Accept:application/json}ifoldisnotNone:headers[If-None-Match]old.etag connhttp.client.HTTPConnection(127.0.0.1,port,timeout3)try:conn.request(GET,/doc,headersheaders)responseconn.getresponse()status,etagresponse.status,response.getheader(ETag)bodyresponse.read()finally:conn.close()ifstatus304:ifoldisNone:raiseRuntimeError(304 without cached body)ifetag!old.etag:raiseRuntimeError(unexpected ETag in local fixture)returnold,304ifstatus!200ornotetag:raiseRuntimeError(expected 200 with ETag)# Validate before returning a replacement: failures keep the old entry.valuejson.loads(body)iftype(value.get(version))isnotint:raiseValueError(version must be an integer)returnEntry(etag,body),200defmain():serverHTTPServer((127.0.0.1,0),Handler)workerthreading.Thread(targetserver.serve_forever,daemonTrue)worker.start()portserver.server_address[1]cacheNonetry:cache,statusfetch(port,cache)assertstatus200andjson.loads(cache.body)[version]1print(PASS first: 200, version1)previouscache cache,statusfetch(port,cache)assertstatus304andcacheispreviousprint(PASS unchanged: 304, cached body reused)state[body]b{version:2}cache,statusfetch(port,cache)assertstatus200andcache.etag!previous.etagassertjson.loads(cache.body)[version]2print(PASS changed: 200, version2)state[force304]Truetry:fetch(port,None)exceptRuntimeErroraserror:assertstr(error)304 without cached bodyelse:raiseAssertionError(missing cache was accepted)state[force304]Falseprint(PASS missing cache: 304 rejected)previouscache state[body]bnot-jsontry:cache,statusfetch(port,cache)exceptjson.JSONDecodeError:passelse:raiseAssertionError(invalid JSON was accepted)assertcacheispreviousprint(PASS invalid 200: old ETag and body preserved)finally:server.shutdown()server.server_close()worker.join()if__name____main__:main()实际输出PASS first: 200, version1 PASS unchanged: 304, cached body reused PASS changed: 200, version2 PASS missing cache: 304 rejected PASS invalid 200: old ETag and body preserved4. 五个结果各自证明了什么首次请求没有条件头得到 200 和版本 1。第二次回传已保存的 ETag得到 304测试不只检查状态码还检查客户端返回的是之前那份缓存条目。把服务端正文改成版本 2 后旧 ETag 不再匹配。客户端读取新正文、解析并校验成功后才把新的 ETag 与正文一起交给调用方。第四个场景故意让测试服务器在客户端没有缓存时仍返回 304。这是异常注入不是正常协议示范。客户端明确抛错不把缺失数据伪装成成功。实际采集任务若发现只剩验证标记、正文已被清理应先清除这份失配状态再按有界策略发起不带条件头的获取。第五个场景返回 200但正文为无效 JSON。由于cache, status fetch(...)的右侧先求值函数抛错时左侧不会被替换原来的 ETag 和正文仍配对保留。保留旧缓存只是保存恢复材料本次请求依然失败不能因此在结果中声称拿到了最新数据。5. 接进采集任务时先补这三件事把状态保存成一个完整条目。本例用不可变 Entry 和单次变量赋值展示失败前后状态。持久化时可以在同一数据库事务中写入正文、ETag 和校验结果若正文进对象存储则需要先写正文再提交指向该对象的元数据并处理未引用对象。本文没有测试数据库、跨进程并发或断电恢复内存赋值不能证明这些性质。让缓存对应正确的请求上下文。本例只有一个端点所以没有缓存键映射。真实任务中的 URL、会话身份、语言等若会改变结果就必须隔离对应条目。不要把一个账户获取的 ETag 搭配另一份正文使用也不要把缓存清理任务只作用于正文、留下可继续发送的标记。分别记录网络结果与业务结果。200/304 比例可以帮助理解重复传输业务层还应记录解析成功、缓存缺失、更新失败和最后成功校验时间。304 只说明此次验证的表示可复用不能代替采集频率、数据完整性和下游更新的检查。本实验没有跑吞吐量或带宽基准不能据此给出固定的提速百分比。使用 AI 协助接入时可以让它补出状态分支和异常用例验收仍应落到“新正文不合格时旧正文与旧 ETag 是否一起保留”这类具体断言。先把一致性验证清楚再评估是否减少了下载与后续处理成本。示例客户端 API 说明可查 Python 官方 http.client 文档。本文文字和代码由 AI 辅助生成文中列出的五个场景已在本地实际运行未进行外站或生产环境验证。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价