快速回答
如何使用 WebReaderAPI 将网站整站抓取 (crawl) 为 Markdown?
将起始 URL 连同页面 limit 发送到 POST https://webreaderapi.com/api/v1/crawl。WebReaderAPI 在后台读取网站,仅限同一域名,遵循其 sitemap、robots.txt 和 Crawl-delay,每个任务最多 10,000 页。从任务中收集 Markdown 页面,一次一页结果。每次页面读取花费 1 积分(使用浏览器为 2),自 2026 年 10 月起从 US$0.07/千页 起。无需 KYC;加密货币支付。
任务一览
| 批量 | 每个批量任务最多 100 个 URL:POST https://webreaderapi.com/api/v1/batch |
|---|---|
| 整站抓取 (crawl) | 每个任务抓取一个网站,最多 10,000 页,遵循其 sitemap、robots.txt 和 Crawl-delay:POST https://webreaderapi.com/api/v1/crawl |
| 结果 | GET https://webreaderapi.com/api/v1/jobs/{job_id},然后 ?cursor=… 获取下一页结果 |
| 计费 | 每次页面读取按 /read 调用计费;被 robots.txt 跳过的页面和失败的页面不收费 |
| 整站抓取 (crawl) 范围 | 一个域名;指向其他域名的链接会列出但不读取 |
| 取消任务 | v1 中不可用:将 limit 设置为你准备支付的最大页数 |
启动批量任务
POST/api/v1/batch
在后台使用相同选项读取最多 100 个 URL。选项使用 /read 的名称和值。答案是 job ID;值仅为示例。
认证:将 API 密钥放在 Authorization: Bearer API_KEY 请求头中。
请求体
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
urls | array of strings | 是 | 1 到 100 个 http 或 https URL。重复的只读取一次。 |
options | object | 否 | 除 url 外的任意 /read 参数:format、main_content、render、include_links、include_images、country、max_chars、timeout。 |
{
"urls": [
"https://example.com/blog/post-1",
"https://example.com/blog/post-2",
"https://example.org/news/launch"
],
"options": {
"format": "markdown",
"max_chars": 50000
}
}响应 202 Accepted
{
"job_id": "job_5c1e8a90d2"
}代码示例
curl -s "https://webreaderapi.com/api/v1/batch" \
-H "Authorization: Bearer $WEBREADER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com/blog/post-1", "https://example.com/blog/post-2"], "options": {"format": "markdown"}}'import os
import requests
resp = requests.post(
"https://webreaderapi.com/api/v1/batch",
headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
json={"urls": ["https://example.com/blog/post-1", "https://example.com/blog/post-2"], "options": {"format": "markdown"}},
timeout=30,
)
print(resp.json()["job_id"])const res = await fetch('https://webreaderapi.com/api/v1/batch', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ urls: ['https://example.com/blog/post-1', 'https://example.com/blog/post-2'], options: { format: 'markdown' } }),
});
console.log((await res.json()).job_id);启动一个整站抓取 (crawl) 任务
POST/api/v1/crawl
从一个起始 URL 开始,按广度优先读取一个网站。爬虫只会停留在起始 URL 的域名内,使用 robots.txt 中指定的站点地图或位于 /sitemap.xml 的站点地图,并遵守 robots.txt 和 Crawl-delay。以下数值仅为示例。
认证:将 API 密钥放在 Authorization: Bearer API_KEY 请求头中。
请求体
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
url | string | 是 | 起始页面,例如文档站点的首页。 |
limit | integer | 否 | 最多读取的页面数,上限为 10,000。任务达到该数量时停止。 |
max_depth | integer | 否 | 距起始 URL 最多的链接步数。 |
include_paths | array of strings | 否 | 要保留的简单路径模式,例如 /docs/*。 |
exclude_paths | array of strings | 否 | 要跳过的简单路径模式,例如 /blog/tag/*。 |
options | object | 否 | 除 url 外的任意 /read 参数,应用于每个页面。 |
{
"url": "https://docs.example.com/",
"limit": 500,
"max_depth": 3,
"include_paths": [
"/docs/*"
],
"exclude_paths": [
"/docs/archive/*"
],
"options": {
"format": "markdown"
}
}响应 202 Accepted
{
"job_id": "job_8b27f41c0e"
}代码示例
curl -s "https://webreaderapi.com/api/v1/crawl" \
-H "Authorization: Bearer $WEBREADER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://docs.example.com/", "limit": 500, "include_paths": ["/docs/*"]}'import os
import requests
resp = requests.post(
"https://webreaderapi.com/api/v1/crawl",
headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
json={"url": "https://docs.example.com/", "limit": 500, "include_paths": ["/docs/*"]},
timeout=30,
)
print(resp.json()["job_id"])const res = await fetch('https://webreaderapi.com/api/v1/crawl', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://docs.example.com/', limit: 500, include_paths: ['/docs/*'] }),
});
console.log((await res.json()).job_id);获取任务状态和结果
GET/api/v1/jobs/{job_id}
返回任务状态以及已就绪的下一批结果。results 中的每一项要么是完整的 /read 响应,要么是 {url, error},表示该页面无法读取。当出现 next_cursor 时,将其设为 cursor 再次调用;当它不存在时,表示暂无更多结果就绪。以下数值仅为示例。
认证:将 API 密钥放在 Authorization: Bearer API_KEY 请求头中。
参数
| 名称 | 位置 | 类型 | 必填 | 描述 |
|---|---|---|---|---|
job_id | path | string | 是 | 由 /batch 或 /crawl 返回的 ID。 |
cursor | query | string | 否 | 上一次调用返回的 next_cursor 值,用于获取后续结果。 |
响应 200
{
"job_id": "job_8b27f41c0e",
"status": "running",
"results": [
{
"url": "https://docs.example.com/docs/install",
"final_url": "https://docs.example.com/docs/install",
"status_code": 200,
"rendered": false,
"credits_used": 1,
"title": "Install",
"markdown": "# Install\n\nRun the installer...",
"word_count": 418
},
{
"url": "https://docs.example.com/docs/private",
"error": {
"code": "blocked_by_robots",
"message": "robots.txt disallows this page for WebReaderAPI."
}
}
],
"next_cursor": "c_000002",
"credits_used": 1
}代码示例
curl -sG "https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e" \
-H "Authorization: Bearer $WEBREADER_API_KEY" \
-d cursor=c_000002import os
import requests
resp = requests.get(
"https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e",
headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
params={"cursor": "c_000002"},
timeout=30,
)
job = resp.json()
print(job["status"], len(job["results"]), job.get("next_cursor"))const res = await fetch('https://webreaderapi.com/api/v1/jobs/job_8b27f41c0e?cursor=c_000002', {
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
});
const job = await res.json();
console.log(job.status, job.results.length, job.next_cursor);如何等待任务完成?
轮询 /jobs/{job_id} 并保留 cursor。处理拿到的结果,然后将 cursor 设为 next_cursor 再次调用。当 next_cursor 不存在且 status 为 done 时,说明已获取全部结果;当它不存在且任务仍为 running 时,等待后用同一个 cursor 再次查询。
开始时间隔 2 秒轮询一次,在没有新结果到达时每次将等待时间翻倍,最多 30 秒。遵守站点 Crawl-delay 的整站抓取 (crawl) 可能需要较长时间,因此等待久一点是正常的,且不产生费用。
import os
import pathlib
import re
import time
import requests
JOBS = "https://webreaderapi.com/api/v1/jobs"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
def save_job(job_id, folder="pages"):
out = pathlib.Path(folder)
out.mkdir(exist_ok=True)
cursor, wait, saved = None, 2, 0
while True:
params = {"cursor": cursor} if cursor else {}
job = requests.get(f"{JOBS}/{job_id}", headers=HEADERS, params=params, timeout=30).json()
for item in job["results"]:
if "error" in item:
print("skipped", item["url"], item["error"]["code"])
continue
name = re.sub(r"[^a-z0-9]+", "-", item["final_url"].lower()).strip("-")[:120]
(out / f"{name}.md").write_text(item["markdown"], encoding="utf-8")
saved += 1
if job.get("next_cursor"):
cursor, wait = job["next_cursor"], 2
continue
if job["status"] == "done":
return saved
time.sleep(wait)
wait = min(wait * 2, 30)
if __name__ == "__main__":
print(save_job("job_8b27f41c0e"), "pages saved")
整站抓取 (crawl) 会读取哪些页面?
整站抓取 (crawl) 从你的 URL 开始,按广度优先一次向外扩展一个链接步,最多到 max_depth。它还会读取站点的站点地图,地址来自 robots.txt 中给出的地址或 /sitemap.xml,以找到链接指向较少的页面。
- 仅限同一域名。 指向其他域名的链接在你请求
include_links时会列在结果中,但不会被读取。 - 路径过滤。
include_paths和exclude_paths接受/blog/*之类的简单模式。 - 每页只读一份。 当站点声明了 canonical URL 时,通过多个地址到达的同一页面只读取一次。
- 硬性停止。 任务在读取
limit个页面后结束。
整站抓取 (crawl) 如何遵守站点规则?
每个请求都带有 User-Agent WebReaderAPI/1.0 (+https://webreaderapi.com/bot/),爬虫会遵循适用于 WebReaderAPI 的 robots.txt 规则;若没有为其命名的规则,则遵循用于所有爬虫的规则。被禁止的页面会被跳过,并以错误 blocked_by_robots 出现在 results 中;这些页面不计费。
当 robots.txt 设置了 Crawl-delay 时,爬虫在对该站点的两次请求之间会等待那么久。如果网站在其 robots.txt 中声明 Content-Signal: ai-input=no,我们不会为你读取其页面。如果网站拒绝我们的请求,我们会返回其答复:我们从不破解 CAPTCHA、不登录账户,也不试图绕过封锁或付费墙。
整站抓取 (crawl) 的费用是多少?
每个读取的页面都按一次 /read 调用计费:1 积分,若页面需要浏览器则为 2 积分,使用 country 时再加 10 积分。被跳过和失败的页面免费。任务的 credits_used 会累计截至目前已计费的金额。
一个 500 页的文档站点在不使用浏览器的情况下读取约消耗 500 积分,10,000 个网页 套餐(US$3.75)可以覆盖很多次。由于 v1 中任务无法取消,limit 就是你的支出上限:把它设为你能承受付费的最大页面数。
批处理与整站抓取:常见问题
应该用 /batch 还是多次调用 /read?
当你已经拥有 URL 且不需要立即拿到每个结果时,用 /batch。它免去了你管理并发和重试的麻烦。当 agent 需要立即获取一个页面时,用 /read。
任务结果会保留多久?
结果一就绪就尽快收集。结果会等待你获取;一旦交付,就不再保留页面内容的副本,所以请在你自己这边保存所需内容。
整站抓取 (crawl) 会跟随指向其他网站的链接吗?
不会。一次整站抓取 (crawl) 只读取一个域名。其他域名会在你请求时作为链接出现,如果你希望读取这些 URL,可以把它们发给 /batch。
可以停止已经开始的整站抓取 (crawl) 吗?
v1 中不行。任务会在 limit 个页面时停止,所以请选择与预算相符的上限。已读取的页面仍会计费;从未读取的页面不产生费用。