Browser Harness 实战:Wayback Machine CDX API 快照检索与抓取完全指南

原创2026-09-20 13:55:551,736 阅读
文章标签:AI 应用AI 技能浏览器控制GUI 自动化MCP 服务

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 能读到的东西就不要开浏览器"的设计原则。

两个必须刻进脑子的事实:

  1. output=json 必须显式声明——不传它返回的是空格分隔的文本,JSON 不是默认输出模式。
  2. 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&timestamp=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.

两个关键注意点:

  1. Wayback 工具条被注入到每一页归档 HTML 中,包裹在 <!-- BEGIN WAYBACK TOOLBAR INSERT --> 与 <!-- END WAYBACK TOOLBAR INSERT --> 注释之间。追求原始 HTML 保真度时必须剥离这段区间。
  2. 形如 /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)从"找历史快照"到"取档案全文"的完整数据链路。

登录后查看全文
browser-harness