快速回答
如何在 Python 中将网页转为 Markdown?
调用 WebReaderAPI,传入 requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...}) 和放在 Authorization 头中的密钥;JSON 结果会在 markdown 中给出页面的正文。对于多个页面,使用带 semaphore 或批量任务的 httpx.AsyncClient。每个页面消耗 1 个积分,截至 2026 年 10 月低至 US$0.07/千页。无需 KYC;使用加密货币支付。
我需要安装什么?
Python 3.9 或更高版本,以及一个 HTTP 客户端。requests 对一次读取一个页面的脚本来说就足够了;httpx 增加了对多个页面并行处理的异步支持。不需要我们的任何包:该 API 就是普通的 HTTPS 和 JSON。
不要把密钥写在代码里。像下面每个示例那样从 WEBREADER_API_KEY 环境变量中读取它,并为每个项目分配各自的密钥,这样你就能在不影响其他项目的情况下吊销其中一个。
pip install requests httpx
export WEBREADER_API_KEY="your-key"如何用 requests 读取一个页面?
把 url 和任何选项作为查询参数发送,在出现 HTTP 错误时抛出异常,并读取你需要的字段。示例会保留正文,为模型将其限制在 20,000 个字符以内,并打印这次调用的花费。
import os
import requests
API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
def read(url, **options):
resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
if resp.status_code != 200:
error = resp.json()["error"]
raise RuntimeError(error["code"] + ": " + error["message"])
return resp.json()
page = read("https://example.com/docs/getting-started", max_chars=20000)
print(page["title"], page["word_count"], "words", page["credits_used"], "credit(s)")
print(page["markdown"][:500])
如何用 httpx 一次读取多个页面?
使用一个 httpx.AsyncClient 和一个设置为你并发上限或以下的 asyncio.Semaphore。每个账户都有同时读取页面数的上限;你的控制台会显示该上限,支持团队可将其提高。某一个页面的错误不应中断其他页面,因此示例会把它们收集在结果旁边,而不是抛出异常。
如果你超过上限,API 会返回 429 concurrency_limit;不会计费,但该页面不会被读取。把 semaphore 保持在上限以下比重试更简单。
import asyncio
import os
import httpx
API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
async def read_one(client, sem, url):
async with sem:
resp = await client.get(API, params={"url": url, "max_chars": 50000})
body = resp.json()
if resp.status_code == 200:
return url, body, None
return url, None, body["error"]["code"]
async def read_many(urls, limit=10):
sem = asyncio.Semaphore(limit)
async with httpx.AsyncClient(headers=HEADERS, timeout=75) as client:
return await asyncio.gather(*(read_one(client, sem, u) for u in urls))
if __name__ == "__main__":
urls = [f"https://example.com/blog/post-{n}" for n in range(1, 21)]
for url, page, error in asyncio.run(read_many(urls)):
print(url, "->", error or page["title"])
什么时候应该改用批量任务?
当你有一个最多包含 100 个 URL 的列表,并且不需要每个页面一就绪就立刻拿到。批量任务在我们这边运行,所以你的脚本不用保持任何连接,也不做重试逻辑。用 POST https://webreaderapi.com/api/v1/batch 启动它,然后用batch and crawl 页面上展示的 cursor 循环从任务中收集结果。
如果要处理整个站点,就改为启动一次整站抓取 (crawl),让它跟随 sitemap 和链接;同一页面有一个完整的循环,会为每个页面写一个 .md 文件。
import os
import requests
urls = [f"https://example.com/changelog/{year}" for year in range(2020, 2027)]
resp = requests.post(
"https://webreaderapi.com/api/v1/batch",
headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
json={"urls": urls, "options": {"format": "markdown", "main_content": True}},
timeout=30,
)
resp.raise_for_status()
print("job:", resp.json()["job_id"])
如何把 Markdown 切分成块用于 RAG?
先按标题切分,再按大小切分。标题标记主题变化的位置,所以从标题开始的块更容易被检索器匹配。在每块的 metadata 中保留页面标题和标题路径:它们能帮模型引用正确的章节。
下面的函数不需要额外的库。它按 # 到 ### 级标题切分,然后把过长的章节按空行切分,以保持在字符预算之内。为每一块存储 final_url,这样指向同一页面的两个地址就不会产生重复。
import re
def chunk_markdown(page, max_chars=2000):
"""Split a /read response into chunks with a heading path and the source URL."""
chunks, path, buf = [], [], []
def flush():
text = "\n".join(buf).strip()
while text:
part = text[:max_chars]
cut = part.rfind("\n\n")
if len(text) > max_chars and cut > max_chars // 2:
part = part[:cut]
chunks.append({"text": part.strip(), "section": " > ".join(path), "title": page.get("title"), "source": page["final_url"]})
text = text[len(part):].strip()
buf.clear()
for line in page["markdown"].splitlines():
m = re.match(r"^(#{1,3}) (.+)", line)
if m:
flush()
level = len(m.group(1))
path[:] = path[: level - 1] + [m.group(2).strip()]
buf.append(line)
flush()
return chunks
一个 Python 任务要花多少钱?
按页面数计费,而不是按请求数。1 积分 = 读取 1 个页面且无需浏览器;当页面需要浏览器渲染 JavaScript 时为 2 积分。失败的请求,或被网站 robots.txt 拒绝的请求,不扣费。在没有浏览器的情况下读取 2,000 个文档页面大约用 2,000 积分,10,000 个网页(US$3.75)可以覆盖五倍于此的量。预付费积分永不过期:未使用的页面数会一直保留在你的账户中,直到你使用,所以每月运行一次的 pipeline 只消耗它实际读取的量。
Python:常见问题
有官方 Python 包吗?
没有。本页的代码就是你需要的全部:WebReaderAPI 是一个普通的 HTTPS API,requests 或 httpx 可以直接与它通信。
如何处理被 robots.txt 拒绝的页面?
检查错误码。blocked_by_robots 和 target_refused 是最终结果且免费;记录它们然后继续。错误页面上有一个重试辅助函数,只重试那些可能发生变化的错误码。
我可以在 Jupyter notebook 中使用这个 API 吗?
可以。requests 的写法可以直接使用。在 notebook 中,用 await read_many(urls) 替代 asyncio.run,因为 notebook 本身已经在运行事件循环。
我的客户端应该用多长的超时?
比 API 自身的超时略长一点:默认 30 秒,最大 60 秒,所以客户端设 75 秒可以为慢速站点和响应留出余量。