克劳德网络爬虫:将可空的统计表转换为干净的JSON
Specialist in Anti-Bot Strategies
TL;DR:
- Claude 清晰地读取可空数据,通过实时运行证明了这一点。 在提供一个渲染的赛季统计表后,
anthropic/claude-haiku-4.5在一次调用中正确地返回了 25 个球队赛季记录,对于 1990-91 NHL 赛季中尚不存在的统计数据,正确地写入了null,而不是猜测一个值。 - API 是文本输入,文本输出——它从不触及网络。 渲染、会话和访问挑战属于获取层;本指南通过 Scrapeless 通用抓取 API 每页进行一次 POST 请求。
- JSON 模式有一个值得注意的怪癖,了解这一点可以避免解析错误。 通过 OpenRouter 的 Claude 有时会在结构化输出周围用
```json划分,即使response_format已经设置——下面的提取脚本防御性地剥离了它。 - 当前的廉价命名变化。 实时的 OpenRouter 模型目录将
claude-haiku-4.5的能力远高于较旧的claude-3-haiku系列,且每个 token 只需适度的额外费用;本指南直接检查目录,而不是依赖记忆中的名称。 - 还没有 Anthropic 密钥? 通过 OpenRouter 运行相同的请求。 本指南在
anthropic/claude-haiku-4.5上实时执行了它,并展示了捕获的输出。 - 从获取侧开始免费。 在 app.scrapeless.com 创建你的 Scrapeless API 密钥。
Claude 可以抓取网站吗?
Claude 解析数据;它不会抓取。给它页面文本和模式,它返回清晰、有类型的记录——包括更困难的情况,知道一个字段确实缺失而不是编造一个似是而非的零。它无法做的是检索特定的 URL、执行构建页面的 JavaScript、保持会话或清除抓取工作所需的访问挑战。每个有效的“Claude 网络抓取”设置都将模型与能够单独处理这些任务的获取层配对。
本网站早期的指南,与 Claude AI 的网络抓取,调查了将 Claude 与抓取器组合的十种不同方式。这个指南更窄,更具体:一个真实目标、一个实时执行的提取、一个模式、真实的捕获输出。如果更广泛的问题是使用哪种语言模型进行解析,LLM 抓取器解释 涉及该类别。
安装
Anthropic SDK 覆盖原生路径,openai 覆盖 OpenRouter 路径,requests 覆盖获取层:
bash
pip install "anthropic==0.120.0" "openai==2.48.0" requests
配置
bash
export ANTHROPIC_API_KEY="sk-ant-your_key"
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
获取一个字段有时缺失的页面
演示目标是一个公共体育统计沙盒:每个 NHL 球队一行赛季数据,包括一个统计数据——加时赛失利——联盟在 1990 年代初期之前并未追踪。早于该时间点的行留下空白,这使得该页面成为真正测试提取器是编造数据还是报告缺失数据的测试。一条 POST 请求到 通用抓取 API 返回页面,渲染和代理路由在服务器端处理:
python
# fetch_teams.py — 通过 Scrapeless 获取球队统计页面
import os
import requests
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
json={
"actor": "unlocker.webunlocker",
"input": {"url": "https://scrapethissite.com/pages/forms/", "method": "GET", "js_render": True},
},
timeout=120,
)
resp.raise_for_status()
html = resp.json().get("data", "")
print(f"获取 {len(html):,} 字符")
print("球队行:", html.count('<tr class="team">'))
with open("page.html", "w", encoding="utf-8") as f:
f.write(html)
运行结果打印沙盒的第一页结果中 25 行球队数据——这是一个由 公共网络抓取练习网站 维护的实时目标,与本网站其他 LLM 解析指南中使用的引用和书籍沙盒不同。
基本实现:Claude 作为提取器
原生 Messages API 通过 output_config 直接接受 JSON Schema,当前 API 参考文件将其记录为替代旧的基于预填充的 JSON 技巧——请参见 Anthropic 的结构化输出指南。当前最便宜的非遗留 Claude 等级是 claude-haiku-4-5;请注意,原生模型 ID 使用连字符,与 OpenRouter 列出的 claude-haiku-4.5 标识不同。
注意:此块需要一个具有信用的
ANTHROPIC_API_KEY—— 此指南未假定的唯一前提条件。下一部分通过 OpenRouter 运行相同的提取,同时捕获输出。
python
# extract_claude.py — 原生 Claude 提取(需要 ANTHROPIC_API_KEY)
import json
from anthropic import Anthropic
client = Anthropic() # 从环境中读取 ANTHROPIC_API_KEY
page_html = open("page.html", encoding="utf-8").read()
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=6000,
system="从此表中提取每个团队赛季行。ot_losses 在单元格为空时为 null。",
messages=[{"role": "user", "content": page_html}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"teams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"year": {"type": "integer"},
"wins": {"type": "integer"},
"losses": {"type": "integer"},
"ot_losses": {"type": ["integer", "null"]},
"win_pct": {"type": "number"},
"goals_for": {"type": "integer"},
"goals_against": {"type": "integer"},
},
"required": ["name", "year", "wins", "losses", "ot_losses", "win_pct", "goals_for", "goals_against"],
"additionalProperties": False,
},
}
},
"required": ["teams"],
"additionalProperties": False,
},
}
},
)
text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)
print(f"提取到 {len(data['teams'])} 支球队")
output_config.format 确保响应解析为 JSON 符合模式 —— 无需删除外框,无需预填充变通。
没有 Anthropic 密钥?通过 OpenRouter 运行
OpenRouter 在与 OpenAI 兼容的聊天完成表面下提供 Claude。这是本指南实际执行的版本,以一个自包含的脚本进行抓取和提取:
python
# extract_openrouter.py — 通过 OpenRouter 执行相同的提取
import json
import os
import requests
from openai import OpenAI
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
json={
"actor": "unlocker.webunlocker",
"input": {"url": "https://scrapethissite.com/pages/forms/", "method": "GET", "js_render": True},
},
timeout=120,
)
resp.raise_for_status()
page_html = resp.json().get("data", "")
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
completion = client.chat.completions.create(
model="anthropic/claude-haiku-4.5",
temperature=0,
max_tokens=6000,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": '从此表中提取每个团队赛季行。仅回复 JSON: '
'{"teams":[{"name":str,"year":int,"wins":int,"losses":int,"ot_losses":int|null,'
'"win_pct":number,"goals_for":int,"goals_against":int}]}. '
"ot_losses 在单元格为空时为 null。",
},
{"role": "user", "content": page_html},
],
)
raw = completion.choices[0].message.content.strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
data = json.loads(raw)
print(f"提取到 {len(data['teams'])} 支球队")
print(json.dumps(data["teams"][0], ensure_ascii=False))
现场运行提取了所有 25 支团队赛季,第一条记录:
text
提取到 25 支球队
{"name": "波士顿棕熊", "year": 1990, "wins": 44, "losses": 24, "ot_losses": null, "win_pct": 0.55, "goals_for": 299, "goals_against": 264}
ot_losses 返回 null 对于 1990-91 赛季 — 这一年的数据早于 NHL 对该统计数据的跟踪 — 并且所有 25 行都按声明的类型解析,没有任何后续强制转换。去掉围栏是关键:没有它,json.loads 在这个模型的 OpenRouter 输出上直接失败,即使 response_format 已设置。整个调用:13,365 个令牌。
在免费计划中获取您的 API 密钥:app.scrapeless.com
高级模式
- 防范聚合路径上的围栏输出。 原生
output_config.format路径强制执行无围栏的符合模式的 JSON;OpenRouter 对话完成路径在模型之间没有同样的保证,因此在解析之前,无论你要路由哪个模型,都要去掉前导的```块。 - 明确命名可为空性。 仅靠模式(
"ot_losses": {"type": ["integer", "null"]})加上系统提示中的一句话就能产生正确的空值,而不是模型发明的0。如果去掉其中任何一个,重新测试后再信任该字段。 - 每次调用保持一页。 页边界是自然的记录边界;将几页合并到一个提示中会模糊一个团队的统计数据结束的地方和下一个开始的地方。
- 在扩展之前测量令牌。 上面的运行花费了 13,365 个令牌用于一页 25 行 — 在提交页面体积之前,先乘以实际页面和字段数量,而不是在账单到达后。
故障排除
- 在 OpenRouter 响应上
json.loads失败,即使设置了 JSON 模式。 检查是否有前导的```json围栏并在解析之前去掉 — 本指南的实时运行恰好需要这个修复。 - 一个有时应该缺失的字段总是返回
0或占位符。 在模式和提示中明确说明字段可为空,以及什么条件使其为空;除非告诉模型不要填补空缺,否则它会默认填补。 - 从一个有数十行的页面中得到零行或少数几行。 首先检查获取的 HTML 数量,就像上面的获取脚本所做的那样 — 学习获取的稀薄是一个渲染或访问问题,而不是提取提示能解决的问题。
- 输出在相同运行之间漂移。 在 OpenRouter 路径上设置
temperature=0;提取是一项转录任务,任何高于零的温度都邀请改写。
结论
Claude 的结构化输出路径消除了从获取类型 JSON 中的猜测 — 原生 API 上的 output_config.format 完全跳过了预填充技巧,即使是更宽松的 OpenRouter 对话完成表面也能从真实页面抽取出 25 行团队赛季数据,并正确处理了空值字段,一旦响应得到了防范。Claude 不提供的是页面本身:每页一个服务器端 POST,通过一个为渲染和访问构建的获取层,使得记录得以存在并供提取。
准备好给 Claude 提供真实页面了吗?
这里的获取层是 通用抓取 API — 计划和请求量在 定价页面 上,所有的 unlocker.webunlocker 参数都在 开发者文档 中。创建一个免费的计划密钥在 app.scrapeless.com,两个脚本可以按原样运行。
常见问题
问:Claude 能否独立抓取网站?
不能。Claude API 是一个语言模型端点:它从您提供的文本中提取,并不能发出 HTTP 请求、渲染 JavaScript 或保持会话。每个有效的设置都将其与返回真实页面内容的获取层配对。
问:我该使用哪个 Claude 模型进行网络抓取提取?
在原生 API 上使用 claude-haiku-4-5(在 OpenRouter 上为 claude-haiku-4.5) — 本指南中的实时运行使用了它,并返回所有 25 条记录,并正确处理了空值字段。它是当前最便宜的非遗留 Claude 级别;较早的 claude-3-haiku 名称是先前版本,仍然列出但不是当前推荐。
问:为什么即使在 JSON 模式下,我的 Claude JSON 输出也无法解析?
在 OpenRouter 聚合路径上,即使将 response_format 设置为 json_object,Claude 也可能会将其输出包装在 ```json markdown 围栏中。调用 json.loads 之前去掉前导围栏,或者使用原生 API 的 output_config.format,它强制执行无该故障模式的符合模式的 JSON。
问:Claude如何处理源页面中有时缺失的字段?
正确地,按照架构和提示的说明。当该指南的架构将ot_losses标记为["integer", "null"],且系统提示声明了空值条件时;实时运行在一个早于该统计数据的赛季返回了null,而不是伪造一个值。
问:使用Claude进行抓取是否合法?
提取层不改变收集规则。仅提取公共页面,尊重网站条款和按照机器人排除协议标准化的机器人指令,保持数量有限,并根据适用法律处理任何个人数据——此外,还需遵循Anthropic在模型方面的使用政策。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



