Browser Harness 实战:Wayback Machine CDX API 快照检索与抓取完全指南
Browser Harness 实战:Wayback Machine CDX API 快照检索与抓取完全指南
本文是 browser-harness 项目内 agent-workspace/domain-skills/wayback-machine/scraping.md 的完整技术展开。它面向需要在浏览器之外完成历史网页快照查询与抓取的 Agent 与开发者:核心场景是回溯任意 URL 的历史版本、定位某个时间点最近的一次归档、按时间线去重枚举站点页面,以及批量分页拉取全部快照。读完本文,你将掌握基于 https://web.archive.org CDX API 的整套无浏览器抓取方案——全部数据公开、无需鉴权与 API Key,每条查询都是纯 http_get,配合 browser-harness 内置的 HTTP 助手即可直接落地。
定位说明:本仓库另有覆盖面更广的 agent-workspace/domain-skills/archive-org/scraping.md(涵盖 CDX、item 元数据与 advancedsearch 搜索)。本文按原文档定位,聚焦于 Wayback Machine 快照场景的 CDX 优先速查与实战。
从哪里开始:CDX API 是唯一入口
CDX(Capture/Crawl Index,抓取/爬行索引)API 是查询 Wayback Machine 索引最快捷的方式。它返回结构化 JSON,原生支持过滤(filter)、去重折叠(collapse)、分页(resumeKey/page)与最近日期查找(closest + sort=closest)。
最小的起始查询——找出一个 URL 的全部快照:
import json
# Find all snapshots of a URL — the minimal starting query
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json&limit=10"
"&fl=timestamp,original,statuscode,mimetype,length",
timeout=40.0 # CDX is slow — never use less than 40s
)
rows = json.loads(r)
# rows[0] is ALWAYS the header row — slice rows[1:] for data
for ts, orig, status, mime, length in rows[1:]:
print(f"https://web.archive.org/web/{ts}/{orig} [{status}]")
其中 http_get 来自 browser-harness 的核心助手 src/browser_harness/helpers.py,它是一条纯 HTTP 通道、完全不启动浏览器:默认用 urllib 直连并自动处理 gzip 解压;当设置了 BROWSER_USE_API_KEY 环境变量时,会改走 fetch-use 代理(负责机器人检测、住宅代理与重试)。这正是本文所有代码的运行前提——Wayback 的数据都在公开 HTTP 层,不需要动用 CDP 浏览器能力,符合 skills/browser-harness/SKILL.md 中"纯 HTTP 能读到的东西就不要开浏览器"的设计原则。
两个必须刻进脑子的事实:
output=json必须显式声明——不传它返回的是空格分隔的文本,JSON 不是默认输出模式。- CDX 返回的所有值都是字符串,包括数字:
statuscode='200'而非200,length='4821'而非4821。需要数值时必须显式int()/float()转换。
核心 CDX 查询模式
定位目标日期最近的快照(nearest snapshot)
用 closest 指定一个目标时间戳、sort=closest 要求按接近度排序、limit=1 只取最近的一条:
import json
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json&limit=1"
"&fl=timestamp,original,statuscode"
"&closest=20230601120000&sort=closest",
timeout=60.0
)
rows = json.loads(r)
ts, orig, status = rows[1] # rows[0] is header
snap_url = f"https://web.archive.org/web/{ts}/{orig}"
- 时间戳格式恒为 14 位
YYYYMMDDHHMMSS。 - 支持前缀简写:
20230601(天)、202306(月)、2023(年),CDX 会自动匹配。 - 这是已确认可用的"取某时刻归档快照"方案(详见下文 Availability API 的弃用说明)。
每月一条快照(collapse 折叠)
collapse=timestamp:N 按时间戳前 N 位去重,取每个周期内的第一条捕获:
import json
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json"
"&collapse=timestamp:6" # :6 = one per YYYYMM
"&from=20220101&to=20230101"
"&fl=timestamp,original,statuscode",
timeout=60.0
)
rows = json.loads(r)
for ts, orig, status in rows[1:]:
print(f"{ts[:4]}-{ts[4:6]} https://web.archive.org/web/{ts}/{orig}")
# collapse=timestamp:N — collapse by first N timestamp digits:
# :4 = one per year
# :6 = one per month (most common)
# :10 = one per hour
# Keeps the FIRST capture of each period — not the last.
折叠粒度速查::4 每年一条、:6 每月一条(最常用)、:8 每天一条、:10 每小时一条。注意文档明确标注:保留的是每个周期的第一条而非最近一条,需要"周期内最后一条"时必须自行反转或客户端过滤。
列出域名/路径下的所有页面
matchType 控制 URL 匹配范围:
import json
# matchType=prefix — all URLs starting with the given path
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com/blog/&matchType=prefix&output=json"
"&limit=20&fl=timestamp,original,statuscode"
"&filter=statuscode:200", # only successful captures
timeout=60.0
)
rows = json.loads(r)
for row in rows[1:]:
print(row)
# matchType options:
# exact (default) — this URL only
# prefix — URL + all subpaths
# host — all subdomains of the host
# domain — host + all subdomains (broadest)
四种取值:exact(默认,仅该 URL)、prefix(URL 加所有子路径)、host(该主机全部子域名)、domain(主机加全部子域名,范围最广)。
按状态码 / MIME 类型过滤
import json
# Only successful HTML captures — combine multiple filters
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json"
"&filter=statuscode:200"
"&filter=mimetype:text/html"
"&fl=timestamp,original,length"
"&limit=10",
timeout=40.0
)
rows = json.loads(r)
# filter= uses regex. Examples:
# &filter=statuscode:200 exact match
# &filter=!statuscode:200 negation (all non-200)
# &filter=statuscode:[23].. 2xx and 3xx only
# &filter=mimetype:text/html HTML only
# &filter=original:.*\\.pdf URLs ending in .pdf
# Multiple &filter= params are ANDed together.
filter 参数基于正则语义:! 前缀表示取反;[23].. 这类字符类可用于 2xx/3xx 区间匹配;original 字段支持 URL 形态的正则(如匹配 .pdf 结尾)。多个 filter 参数之间是 AND 关系。
CDX 字段参考
| Field | Description | Example value |
|---|---|---|
urlkey |
SURT-format URL(域名反转) | com,example)/ |
timestamp |
捕获时间,14 位 YYYYMMDDHHMMSS |
20230601114925 |
original |
被抓取的原始 URL(非标准端口会保留) | https://example.com/ |
mimetype |
抓取时的 Content-Type | text/html |
statuscode |
抓取时的 HTTP 状态码(字符串) | 200 |
digest |
响应体 SHA-1,base32 编码 | I4YBMQ6PHPWE2TD6TIXNWHZB6MXRNTSR |
length |
内容字节数(字符串) | 4821 |
省略 fl= 时默认返回以上全部 7 个字段,且按此顺序。urlkey 是 SURT 格式(域名反转、路径置于括号内),便于对同域 URL 排序归组;digest 则可用于内容级去重(见后文高级用法)。
Availability API:确认已损坏,不要作为主通道
原文明确标注:此 API 已损坏——对确实存在于归档中的 URL 也会返回空的
archived_snapshots,确认日期 2026-04-18。请改用CDX ?sort=closest&limit=1。
import json
# WARNING: This API is BROKEN — returns empty archived_snapshots
# for URLs that ARE in the archive. Confirmed broken 2026-04-18.
# Use CDX with ?sort=closest&limit=1 instead (see above).
# Left here for reference only — do not rely on it:
r = http_get(
"https://archive.org/wayback/available"
"?url=example.com×tamp=20240101",
timeout=20.0
)
data = json.loads(r)
# Returns: {"url": "example.com", "archived_snapshots": {}}
# Even for well-archived URLs. Do not trust empty results.
# CORRECT replacement:
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json&limit=1"
"&fl=timestamp,original,statuscode"
"&closest=20240101000000&sort=closest",
timeout=60.0
)
rows = json.loads(r)
if len(rows) > 1:
ts, orig, status = rows[1]
snap_url = f"https://web.archive.org/web/{ts}/{orig}"
这段代码同时给出了"坑"与"正解"的对照:任何依赖 /wayback/available 空结果做"未归档"判定的逻辑都是错误的,判定归档状态必须回落到 CDX 查询。这正是仓库内 agent-workspace/domain-skills/archive-org/scraping.md 中"Do this first"一节反复强调的结论,两处文档相互印证。
分页拉取大数据集:resumeKey 模式
大站点动辄数十万条记录,CDX 提供了两种分页方式:基于 resumeKey 的光标分页(推荐,可无遗漏遍历)与基于 page= 的页码分页。以下为 resumeKey 完整实现:
import json
from urllib.parse import quote
def cdx_all_snapshots(url, fl="timestamp,original,statuscode", page_size=500):
"""Yield all CDX rows for a URL, page by page."""
base = (
"https://web.archive.org/cdx/search/cdx"
f"?url={quote(url, safe='')}&output=json"
f"&fl={fl}&limit={page_size}&showResumeKey=true"
)
resume_key = None
while True:
endpoint = base if resume_key is None else f"{base}&resumeKey={quote(resume_key)}"
rows = json.loads(http_get(endpoint, timeout=60.0))
# With showResumeKey=true, last two rows are [] and ['<key>']
has_resume = len(rows) >= 2 and rows[-2] == [] and rows[-1] != []
data_rows = rows[1:-2] if has_resume else rows[1:]
for row in data_rows:
yield row
if not has_resume:
break
resume_key = rows[-1][0]
for ts, orig, status in cdx_all_snapshots("example.com"):
snap_url = f"https://web.archive.org/web/{ts}/{orig}"
# process...
要点:
url与resumeKey都必须先经urllib.parse.quote编码(safe=''表示全部字符都编码),避免路径与查询符号破坏 URL 结构。showResumeKey=true会在结果末尾追加两行哨兵行:倒数第二行是[](空分隔符),最后一行是['<resume_key_string>']。- 判断是否存在下一页:
rows[-2] == [] and rows[-1] != [];有续页时数据行切片为rows[1:-2],无续页时为rows[1:]。
抓取归档页面本体
拿到时间戳与原始 URL 后,直接构造快照播放 URL 即可取回历史页面:
# Direct snapshot URL: /web/{14-digit-timestamp}/{original-url}
snap_url = "https://web.archive.org/web/20230601114925/https://example.com/"
content = http_get(snap_url, timeout=30.0)
# Returns archived HTML with Wayback toolbar injected inside:
# <!-- BEGIN WAYBACK TOOLBAR INSERT --> ... <!-- END WAYBACK TOOLBAR INSERT -->
# Strip those comments + their contents if you want the original HTML.
# Canonical form for "get latest available" — use 14 zeros:
latest = "https://web.archive.org/web/20240101000000*/example.com"
# The * suffix returns a calendar page (HTML), not the archived page itself.
# Use CDX to find the real timestamp, then fetch the direct URL.
两个关键注意点:
- Wayback 工具条被注入到每一页归档 HTML 中,包裹在
<!-- BEGIN WAYBACK TOOLBAR INSERT -->与<!-- END WAYBACK TOOLBAR INSERT -->注释之间。追求原始 HTML 保真度时必须剥离这段区间。 - 形如
/web/20240101000000*/example.com的规范"最新可用"地址,*后缀返回的是日历页(HTML)而非归档页面本身——正确姿势始终是:先用 CDX 查到真实时间戳,再按/web/{ts}/{orig}直取。
高级 CDX:按内容摘要去重,检测真实变化
collapse=digest 让每个唯一的内容哈希只保留一条记录,从而识别"页面是否真的变了":
import json
# Find only snapshots where the content CHANGED — dedup by SHA-1 digest
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&output=json"
"&collapse=digest" # one capture per unique body hash
"&fl=timestamp,original,digest,length"
"&filter=statuscode:200",
timeout=60.0
)
rows = json.loads(r)
# rows[1:] are unique content versions across all time
# Useful for detecting when a page actually changed, vs. being re-crawled identically
原理即 CDX 字段表中的 digest:响应体 SHA-1 的 base32 编码。同一页面被反复重爬但内容没变时 digest 相同,collapse=digest 会折叠掉这些重复,返回的是跨时间唯一的内容版本序列——这是做网页演变分析(何时改版、改过几次)最直接的手段。
计数查询:先估规模再全量拉取
import json
# showNumPages=true returns total page count, not records
# Use for estimating result size before a full fetch
r = http_get(
"https://web.archive.org/cdx/search/cdx"
"?url=example.com&matchType=prefix"
"&showNumPages=true",
timeout=30.0
)
page_count = int(r.strip()) # returns plain integer, not JSON
# 1 page ~ 150,000 records by default
# Combine with &page=N for manual page-based pagination:
# ?url=...&output=json&page=0, ?url=...&output=json&page=1, etc.
showNumPages=true返回的是纯整数页码数(不是 JSON,直接int(r.strip())),而不是记录条数;用它估算结果规模,避免对热门站点发起无界查询。- 每页默认约 150,000 条记录;如需手动页码分页,配合
&page=0、&page=1……依次请求。
速率限制与超时预算
| API | Typical latency | Safe timeout | Notes |
|---|---|---|---|
| CDX search | 5–40s | 60s | 间歇性缓慢;超时需重试 |
Snapshot fetch (/web/) |
2–10s | 30s | 可靠 |
Metadata (/metadata/) |
<1s | 20s | 快速稳定 |
| Advanced search | <1s | 20s | 快速稳定 |
- 无需 API Key,也没有文档化的速率限制。Archive.org 的实际要求是"保持礼貌":循环中在 CDX 调用之间加
time.sleep(1)。 - 实测经验(来自仓库文档):约 3 次连续的快速 CDX 调用(约 10 秒内)可正常完成;10 次以上快速调用会产生超时。CDX 的慢是服务端侧且不可预测的,超时后应重试而不是盲目降低超时值。
- 反观 browser-harness 的
http_get默认超时只有 20 秒(src/browser_harness/helpers.py),而 CDX 最坏情况可达 40 秒以上——这正是原文档坚持"CDX 调用timeout至少 40s、推荐 60s"的原因,所有示例都显式传入了timeout。
Gotchas 清单:CDX 实战避坑全集
- CDX 很慢,永远给 CDX 调用设
timeout=60.0。40s 是下限,60s 是推荐值。Metadata 与 search API 用 20s 即可。慢是服务端问题,不可预测。 - Availability API(
/wayback/available)已损坏。即使对拥有数千条捕获的 URL 也返回{"archived_snapshots": {}},测试日期 2026-04-18,请勿使用。替代方案:CDX 加?sort=closest&limit=1。 output=json时rows[0]永远是表头。数据一律从rows[1:]切起。忘了这点的典型症状是解构时拿到列名字符串,产生静默类型错误。output=json必须显式声明。省略会返回空格分隔文本,不存在默认 JSON 模式。- CDX 所有值都是字符串。
statuscode='200'而非200,length='4821'而非4821。需要时显式int(row[4])、int(row[6])。 original保留非标准端口。老爬虫会捕获http://www.example.com:80/,:80是original字段的一部分。回放 URL 必须原样拼接:f"https://web.archive.org/web/{ts}/{orig}"带端口也能正确工作。from=/to=边界是排他的。to=20231231等价于to=20231231000000,会排除 12 月 31 日当天的捕获。要包含整个 2023 年应写to=20240101。collapse=timestamp:6保留每个周期的第一条捕获,不是最近一条。需要"周期内最后"就反转结果集或客户端过滤。matchType=domain对热门站点可能返回数百万行。先加&limit=或先用&showNumPages=true估算规模。showResumeKey=true会追加两行哨兵。倒数第二行是[](空分隔),最后一行是['<resume_key_string>'];存在续页键时数据行切片为rows[1:-2]。- Wayback 工具条会注入到每一页归档 HTML。注入内容包裹在
<!-- BEGIN WAYBACK TOOLBAR INSERT -->/<!-- END WAYBACK TOOLBAR INSERT -->注释中,需要原始 HTML 保真度时剥离即可。
在 browser-harness 中启用与组织这些技能
本文件位于 agent-workspace/domain-skills/wayback-machine/scraping.md,属于 browser-harness 的领域技能(domain skills)体系。按 skills/browser-harness/SKILL.md 的约定:
- 领域技能默认关闭,需设置环境变量
BH_DOMAIN_SKILLS=1启用; - 启用后,Agent 在处理特定站点任务前应先通读
$BH_AGENT_WORKSPACE/domain-skills/<site>/目录下的每个文件,而不是自行发明方案——本文即wayback-machine站点的标准操作手册; - 任务级辅助原语可以追加到 agent-workspace/agent_helpers.py(该文件会被核心助手自动加载)。
最后回到本文的立身之本:Wayback Machine 的全部公开数据都走纯 http_get,完全不需要浏览器。这与 browser-harness 的核心原则一致——"纯 HTTP 能读到的东西,就别开浏览器"。把这套 CDX 快查方案与 archive-org 综合技能(item 元数据、advancedsearch 搜索、文件下载)配合使用,即可覆盖互联网档案馆(Internet Archive)从"找历史快照"到"取档案全文"的完整数据链路。