快速回答
WebReaderAPI 错误码是什么意思,哪些应该重试?
WebReaderAPI 错误分为三组,且都不会计费。请求问题(invalid_url、invalid_api_key、insufficient_credits)需要你在自己这边修复。站点决定(blocked_by_robots、blocked_by_site_policy、target_refused)是最终结果。临时故障(fetch_failed、fetch_timeout、concurrency_limit)可以用退避策略重试。积分起步于 US$0.07/千页,使用加密货币支付,无需 KYC。
API 可以返回哪些错误码?
响应体始终是 {"error": {"code", "message"}},如果拒绝来自站点,还会包含 target_status。
| HTTP | code | 原因 | 重试? |
|---|---|---|---|
| 400 | invalid_url | 不是 http 或 https、网址过长,或是私有、本地或云元数据地址 | 不重试:修正 URL |
| 400 | invalid_parameter | 某个参数的值超出了其可选列表或范围 | 不重试:修正请求 |
| 401 | invalid_api_key | 密钥缺失、错误或已被撤销 | 不重试:检查 API 密钥 |
| 402 | insufficient_credits | 你的余额已无可用积分 | 购买积分包后可重试 |
| 403 | blocked_by_robots | 该站点的 robots.txt 禁止 WebReaderAPI 或所有机器人访问该页面 | 不重试:最终结果 |
| 403 | blocked_by_site_policy | 站点声明了 Content-Signal: ai-input=no | 不重试:最终结果 |
| 422 | unsupported_content | 我们不读取的类型(视频、压缩包)或文件过大 | 否 |
| 429 | concurrency_limit | 同一时间运行的请求过多 | 可以,退避后重试 |
| 502 | target_refused | 站点拒绝:403、429、验证挑战页或登录墙;其状态码在 target_status 中 | 不重试:最终结果 |
| 502 | fetch_failed | DNS、TLS 或连接错误,或站点返回 404 或 410 | 重试一两次,并退避 |
| 504 | fetch_timeout | 站点在 timeout 内未响应 | 可以,退避后重试或延长超时 |
为什么 robots.txt 和站点拒绝属于最终结果?
因为这是站点所有者的决定,而 WebReaderAPI 的设计就是遵循它。我们遵守 robots.txt:我们的爬虫以 WebReaderAPI 标识自身,遵循专为其或为所有爬虫编写的规则,不读取网站禁止的页面。被拒绝的页面不扣费。如果网站在其 robots.txt 中声明 Content-Signal: ai-input=no,我们不会为你读取其页面。
当站点以其他方式拒绝请求,例如 403、429、验证挑战页或登录表单时,你会得到 target_refused,并在 target_status 中看到站点自己的状态码。如果网站拒绝我们的请求,我们会返回其答复:我们从不破解 CAPTCHA、不登录账户,也不试图绕过封锁或付费墙。一分钟后重试同一 URL 通常得到相同结果,因此请把这些状态码当作结果而非失败:记录后继续。
如何排查页面为什么被拒绝?
打开站点的 /robots.txt,查找指名 WebReaderAPI 的分组,或适用于所有机器人的 * 分组。匹配该路径的 Disallow 行可解释 blocked_by_robots。诸如 Content-Signal: ai-input=no 的行则可解释 blocked_by_site_policy。
我们会保留每份 robots.txt 的副本,最长 24 小时。如果站点刚刚向机器人开放某个版块,变更最多需要一天才能生效。站点所有者可在我们的爬虫页面上了解我们爬虫的行为方式,以及如何允许或屏蔽它。
临时性失败应如何重试?
只重试 concurrency_limit、fetch_timeout,以及重试一两次 fetch_failed。每次重试前先等待,并让等待时间每次翻倍,再加一点随机抖动,避免并行 worker 同时重试。重试 4 或 5 次后停止。
对于 concurrency_limit,更好的办法是减少同时发送的请求数:每个账户都有同时读取页面数的上限;你的控制台会显示该上限,支持团队可将其提高。对于 fetch_timeout,在重试慢站点前将 timeout 提高到最多 60 秒。重试不产生费用,因为失败的尝试不计费。
import os
import random
import time
import requests
API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
RETRY = {"concurrency_limit", "fetch_timeout", "fetch_failed"}
FINAL = {"blocked_by_robots", "blocked_by_site_policy", "target_refused", "unsupported_content"}
def read_page(url, attempts=5, **options):
wait = 1.0
for attempt in range(1, attempts + 1):
resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
if resp.status_code == 200:
return resp.json()
error = resp.json().get("error", {})
code = error.get("code", "unknown")
if code in FINAL:
return {"url": url, "skipped": code, "target_status": error.get("target_status")}
if code not in RETRY or attempt == attempts or (code == "fetch_failed" and attempt >= 3):
raise RuntimeError(f"{code}: {error.get('message')}")
time.sleep(wait + random.uniform(0, wait / 2))
wait = min(wait * 2, 30)
raise RuntimeError("unreachable")
if __name__ == "__main__":
print(read_page("https://example.com/blog/first-post", max_chars=20000).get("title"))
const RETRY = new Set(['concurrency_limit', 'fetch_timeout', 'fetch_failed']);
const FINAL = new Set(['blocked_by_robots', 'blocked_by_site_policy', 'target_refused', 'unsupported_content']);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function readPage(url, options = {}, attempts = 5) {
let wait = 1000;
for (let attempt = 1; attempt <= attempts; attempt++) {
const params = new URLSearchParams({ url, ...options });
const res = await fetch(`https://webreaderapi.com/api/v1/read?${params}`, {
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
});
const body = await res.json();
if (res.ok) return body;
const code = body.error?.code ?? 'unknown';
if (FINAL.has(code)) return { url, skipped: code, target_status: body.error.target_status };
if (!RETRY.has(code) || attempt === attempts) throw new Error(`${code}: ${body.error?.message}`);
await sleep(wait + Math.random() * (wait / 2));
wait = Math.min(wait * 2, 30000);
}
}
批量或整站抓取 (crawl) 中的错误怎么办?
单个页面失败不会让整个任务停止。该页面会以 {url, error} 的形式出现在 results 中,并带有与上面相同的错误代码,且不产生任何费用。在一次整站抓取 (crawl) 中,如果某个分区遇到 blocked_by_robots,它会直接读取网站的其余部分。
任务结束时,统计错误代码。同一网站上出现大量 target_refused,说明该网站不接受自动化读取;出现大量 fetch_timeout,说明网站较慢,在任务选项中设置更长的 timeout 可能会有帮助。
错误:常见问题
被 robots.txt 阻止的页面会收费吗?
不会。1 积分 = 读取 1 个页面且无需浏览器;当页面需要浏览器渲染 JavaScript 时为 2 积分。失败的请求,或被网站 robots.txt 拒绝的请求,不扣费。本页上的其他所有错误同样如此。
如果我要求,你们能读取阻止你们爬虫的页面吗?
不能。我们尊重网站所有者的选择,且该政策对所有客户一致。如果你拥有该网站,请在 robots.txt 中为 WebReaderAPI 添加一条规则;爬虫页面说明了具体做法。
为什么一个存在的页面会返回 fetch_failed?
网站可能当时宕机、接受 TLS 连接缓慢,或对自动化客户端返回 404。请用退避策略重试一两次。如果持续失败,请在浏览器中检查该地址。
在被拒绝后,我的 agent 应该告诉用户什么?
简单说明即可,例如该网站不允许对该页面进行自动化读取。然后让模型使用其他来源。对大多数模型来说,把错误代码作为工具结果返回就足够了。