在网页采集中,HTTP 条件请求的核心做法是:第一次抓取页面时保存服务器返回的 ETag 或 Last-Modified,下一次请求通过 If-None-Match 或 If-Modified-Since 告诉服务器“只有内容发生变化时才返回正文”。如果页面没有变化,服务器通常返回 304 Not Modified,采集程序即可复用本地内容,减少带宽、响应时间和服务器压力。

什么是 HTTP 条件请求
HTTP 条件请求是一种基于请求头和响应头的缓存协商机制。客户端根据上一次响应获得的资源版本信息发起下一次请求,服务器再判断资源是否变化。
常用请求头与响应头
| 响应头 | 对应请求头 | 作用 |
|---|---|---|
ETag | If-None-Match | 使用资源版本标识判断内容是否变化,通常比时间判断更可靠。 |
Last-Modified | If-Modified-Since | 使用资源最后修改时间判断内容是否变化。 |
对于普通的 GET 请求,资源未变化时一般返回 304 Not Modified,响应中通常不包含网页正文。资源发生变化时,服务器返回 200 OK 和最新内容。部分接口或写入操作还可能使用 If-Match,但网页采集通常以 If-None-Match 和 If-Modified-Since 为主。
实施前的准备
确认目标站点是否提供校验信息
先查看目标 URL 的响应头,重点确认是否存在 ETag、Last-Modified 和 Cache-Control。可以使用以下命令:
curl -I -L https://example.com/article/1
如果响应头中没有这两个字段,仍然可以采集,但无法依靠标准条件请求准确判断内容是否变化。此时应考虑站点提供的更新时间、接口版本号、内容哈希或 RSS 更新标记。
确定采集边界和请求频率
条件请求能够减少响应正文传输,但每次判断仍然会产生一次 HTTP 请求。因此它不能替代合理的访问频率控制。实施前应明确:
- 只采集允许访问的页面,并遵守站点的 robots.txt、服务条款和适用法律法规。
- 为每个域名设置并发数、请求间隔、超时和重试上限。
- 优先采集列表页或接口提供的更新时间,避免无差别扫描整个站点。
- 对登录、付费、个人信息和受访问控制保护的内容进行权限审查。
步骤一:首次请求并保存校验信息
首次请求时不携带条件请求头。收到成功响应后,保存 URL、响应状态、正文、ETag、Last-Modified、抓取时间和内容摘要。建议以 URL 的规范化结果作为缓存键,并将重定向后的最终 URL单独记录。

import hashlib
import requests
url = "https://example.com/article/1"
response = requests.get(
url,
timeout=15,
headers={"User-Agent": "ResearchBot/1.0; contact: admin@example.com"}
)
response.raise_for_status()
record = {
"url": url,
"status": response.status_code,
"body": response.text,
"etag": response.headers.get("ETag"),
"last_modified": response.headers.get("Last-Modified"),
"fetched_at": response.headers.get("Date"),
"content_hash": hashlib.sha256(response.content).hexdigest()
}
实际项目中不要只保存校验头。服务器可能错误地复用 ETag,或者中间缓存配置不一致,因此同时保存正文哈希有助于排查异常和进行数据审计。
步骤二:构造条件请求
第二次及后续请求时,优先使用 ETag。若没有 ETag,再使用 Last-Modified;如果两者都存在,可以同时发送,但不同服务器的实现存在差异,测试后再决定是否启用组合方式。
headers = {
"User-Agent": "ResearchBot/1.0; contact: admin@example.com",
"Accept": "text/html,application/xhtml+xml"
}
if record.get("etag"):
headers["If-None-Match"] = record["etag"]
elif record.get("last_modified"):
headers["If-Modified-Since"] = record["last_modified"]
response = requests.get(url, headers=headers, timeout=15)
不要自行修改 ETag 的引号、大小写或弱校验前缀。服务器可能返回带双引号的强 ETag,也可能返回 W/ 开头的弱 ETag,应按原值保存和发送。
步骤三:根据响应状态处理页面
处理 304 Not Modified
收到 304 时,说明服务器认为资源没有变化。程序应保留本地正文,并更新本次检查时间、响应头和监控指标,而不是把空响应体覆盖到数据库中。
if response.status_code == 304:
result = {
"changed": False,
"body": record["body"],
"checked_at": "now"
}
处理 200 OK
收到 200 时,应读取新正文并更新缓存元数据。建议重新计算内容哈希,即使 ETag 发生变化,也可以通过哈希确认正文确实发生了变化。
elif response.status_code == 200:
new_body = response.text
new_record = {
"body": new_body,
"etag": response.headers.get("ETag"),
"last_modified": response.headers.get("Last-Modified"),
"content_hash": hashlib.sha256(response.content).hexdigest(),
"checked_at": "now"
}
处理其他状态码
301、302、307、308:记录重定向链,确认是否需要更新规范 URL。不要盲目把重定向地址当作原页面内容。403、401:检查权限和访问规则,不要通过更换大量 User-Agent 或绕过验证来规避限制。404:记录页面可能已删除,但不要立即删除本地数据,通常应经过多次确认。429:读取Retry-After,降低访问频率并执行退避。500、502、503、504:区分临时故障和页面变化,按有限次数重试。
步骤四:设计可用的缓存策略
缓存记录建议
最小可用的数据结构可以包含以下字段:
| 字段 | 用途 |
|---|---|
url | 规范化后的资源地址。 |
body 或正文存储地址 | 收到 304 时复用的原始内容。 |
etag | 下一次发送给服务器的版本标识。 |
last_modified | 没有 ETag 时使用的时间标识。 |
content_hash | 验证正文是否真正变化。 |
last_checked_at | 最近一次向服务器发起验证请求的时间。 |
last_changed_at | 最近一次确认正文变化的时间。 |
status | 记录最近的 HTTP 状态和错误原因。 |
设置检查周期
根据页面变化频率分级:新闻和库存页面可以按分钟或小时检查,稳定的帮助文档和归档页面可以按天或周检查。检查周期应结合业务价值、站点负载和响应头中的缓存策略决定。Cache-Control: max-age 可以作为参考,但不能替代业务上的更新判断。
加入超时、退避和限流
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"]
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
重试只适用于可恢复的网络错误或临时服务端错误。对于 401、403 和明确的业务拒绝,不应无休止重试。生产环境还应设置每域名并发上限、全局请求预算和熔断机制。
步骤五:验证条件请求是否生效
使用 curl 做最小验证
先取得响应头中的 ETag:
curl -sS -D headers.txt -o page.html https://example.com/article/1
grep -i '^etag:' headers.txt
再携带原始 ETag 发起验证:
curl -i -H 'If-None-Match: "保存的ETag值"' https://example.com/article/1
如果页面未变化,预期结果是 304 Not Modified。如果返回 200,可能是内容确实变化,也可能是服务器、CDN 或代理没有实现条件请求。
验证 Last-Modified
curl -i \
-H 'If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT' \
https://example.com/article/1
HTTP 日期应使用 RFC 7231 规定的格式,例如 Wed, 21 Oct 2015 07:28:00 GMT。客户端不应使用本地时区字符串代替标准 GMT 日期。
验证采集程序的业务结果
- 首次请求是否保存了正文和至少一种校验信息。
- 未变化页面收到 304 后,正文是否仍然可读取。
- 页面变化后是否获得 200、新正文和新的校验信息。
- 304 的比例、200 的比例、错误率和平均响应体大小是否符合预期。
- 服务器不返回校验头时,程序是否能降级为正常抓取或采用内容哈希比较。
- 网络中断、超时、429 和 5xx 后,任务是否能重试且不会重复写入错误内容。
注意事项
不要把 304 当成空页面
304 表示“使用已有缓存”,不是“页面为空”。如果程序直接读取 304 的响应体并覆盖本地内容,最终会丢失页面数据。
条件请求不能保证服务器一定返回 304
服务器可能不生成 ETag,动态页面可能每次都返回变化的标识,CDN 也可能重新计算缓存信息。因此应把条件请求视为优化手段,而不是采集正确性的唯一依据。
区分页面内容变化和无关字段变化
广告、推荐列表、时间戳或随机令牌变化,可能导致正文哈希或 ETag 变化,但业务上并不代表目标内容发生变化。解析后可以对标题、正文、价格、库存等业务字段生成规范化摘要,再决定是否触发下游更新。
谨慎使用 HEAD 请求
HEAD 理论上只返回响应头,但部分站点对 HEAD 支持不完整,返回的 ETag 或 Last-Modified 可能与 GET 不一致。因此应以实际 GET 行为为准,并先对目标站点进行测试。
注意压缩和内容编码
ETag 可能与压缩方式有关。使用 gzip、Brotli 或不同代理链时,服务器可能返回不同的 ETag。缓存正文时应同时记录 Content-Encoding、Content-Type 和字符集,避免解码错误或重复压缩。
推荐的完整处理流程
- 规范化 URL,并从缓存中读取历史记录。
- 有 ETag 时发送
If-None-Match,否则发送If-Modified-Since。 - 设置合理的 User-Agent、超时、限流和重试策略。
- 收到 304 时保留旧正文,只更新检查时间和响应状态。
- 收到 200 时保存新正文、ETag、Last-Modified 和内容哈希。
- 对 429 和 5xx 执行有限退避,对权限错误停止重试。
- 记录指标并定期抽样对比正文与解析结果。
常见问题
网页没有 ETag,还能使用条件请求吗?
可以检查是否有 Last-Modified。如果两个响应头都没有,只能采用站点提供的更新时间、接口版本号或本地内容哈希等替代方案;不能凭空生成 ETag 并期待服务器识别。
收到 304 后是否需要重新下载网页?
不需要。304 表示资源未变化,程序应直接复用本地缓存正文,并更新本次检查时间。
为什么携带 If-None-Match 后仍然返回 200?
常见原因包括 ETag 已变化、请求经过的 CDN 与首次请求不同、服务器不支持条件请求、请求头被代理删除,或资源本身是动态内容。应对比响应头、最终 URL、缓存链路和内容哈希。
ETag 和 Last-Modified 应该优先使用哪个?
通常优先使用 ETag,因为它表示资源版本,精度一般高于按秒记录的修改时间。没有 ETag 时再使用 Last-Modified。
条件请求能否降低网页采集对服务器的影响?
能减少未变化资源的响应正文和带宽消耗,但验证请求本身仍会到达服务器。因此仍需遵守访问规则,控制频率、并发和重试次数。
动态网页适合使用 HTTP 条件请求吗?
取决于动态程度。如果服务器为稳定资源提供可靠的 ETag 或 Last-Modified,仍然适用;如果每次响应都包含随机字段或时间戳,应结合解析后的业务字段摘要,避免无意义地触发更新。
总结
HTTP 条件请求的实操闭环是:首次 GET 保存正文与 ETag 或 Last-Modified,后续 GET 携带 If-None-Match 或 If-Modified-Since,收到 304 时复用缓存,收到 200 时更新内容和元数据,再通过状态码、内容哈希、监控指标与异常场景测试完成验证。它适合降低重复采集的带宽和存储开销,但不能替代限流、重试、缓存设计和合规审查。