# 用 Python 将网页转为 Markdown

> 在 Python 中将网页转为 Markdown：requests、带并发上限的异步 httpx、批量任务，以及按标题做 RAG 分块。低至 US$0.07/千页，无需 KYC。

- URL: https://webreaderapi.com/zh/docs/python/
- 最后更新: 2026-10-22
- 发布者: WebReaderAPI (https://webreaderapi.com)

四个方案覆盖了大多数 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` 环境变量中读取它，并为每个项目分配各自的密钥，这样你就能在不影响其他项目的情况下吊销其中一个。

*安装*

```bash
pip install requests httpx
export WEBREADER_API_KEY="your-key"
```

## 如何用 requests 读取一个页面？

把 `url` 和任何选项作为查询参数发送，在出现 HTTP 错误时抛出异常，并读取你需要的字段。示例会保留正文，为模型将其限制在 20,000 个字符以内，并打印这次调用的花费。

*requests：一个页面*

```python
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 个页面同时进行*

```python
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 页面](https://webreaderapi.com/zh/docs/crawl/)上展示的 cursor 循环从任务中收集结果。

如果要处理整个站点，就改为启动一次整站抓取 (crawl)，让它跟随 sitemap 和链接；同一页面有一个完整的循环，会为每个页面写一个 `.md` 文件。

*启动批量任务*

```python
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*

```python
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` 是最终结果且免费；记录它们然后继续。[错误页面](https://webreaderapi.com/zh/docs/errors/)上有一个重试辅助函数，只重试那些可能发生变化的错误码。

### 我可以在 Jupyter notebook 中使用这个 API 吗？

可以。`requests` 的写法可以直接使用。在 notebook 中，用 `await read_many(urls)` 替代 `asyncio.run`，因为 notebook 本身已经在运行事件循环。

### 我的客户端应该用多长的超时？

比 API 自身的超时略长一点：默认 30 秒，最大 60 秒，所以客户端设 75 秒可以为慢速站点和响应留出余量。

---

WebReaderAPI: WebReaderAPI 是一款面向 AI agents 的网页读取 API，可将任何公开网页转换为干净的 Markdown，用加密货币支付，无需上传身份证件。预付费积分低至 US$0.07/千页，永不过期，且遵守 robots.txt。无需 KYC — 无身份证、无自拍、无证件。加密货币支付。
