无需 KYC — 无身份证、无自拍、无证件。加密货币支付:BTC, USDT, USDC, ETH, SOL, LTC
语言: 简体中文
获取 API 密钥

文档 · Python

用 Python 将网页转为 Markdown

四个方案覆盖了大多数 Python 项目:用 requests 读取一个页面,用 httpx 并发读取多个页面,把列表交给批量任务,以及把 Markdown 切分成块以供向量存储使用。

最后更新:

快速回答

如何在 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 个字符以内,并打印这次调用的花费。

requests:一个页面
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 保持在上限以下比重试更简单。

httpx:最多 10 个页面同时进行
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,这样指向同一页面的两个地址就不会产生重复。

按标题切分 Markdown
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 秒可以为慢速站点和响应留出余量。

低至 US$0.07/千页 · 无需 KYC

获取 API 密钥