better-crawler-4-agent
把 URL 变成干净正文的 MCP server。用真实浏览器渲染,知乎、CSDN、掘金这些"HTTP 抓回来是空壳"的站点也能拿到内容。
一个标准 stdio MCP server,不绑定任何客户端——Claude Code、Cursor、Windsurf、Cline、Codex、ZCode 都能用。
English summaryA vendor-neutral MCP server that turns URLs into clean article text using a real headless Chromium. Built for sites where plain HTTP fetching fails: JS-rendered pages and Chinese anti-bot sites like Zhihu, CSDN, and Juejin.
Scope: URL → text only. URL discovery is left to the caller, so the tool stays single-purpose and needs no search API keys.
Key features: real browser rendering, tiered fallback (browser → trafilatura → httpx+full browser fingerprint), font honeypot detection, fake-success detection (200 responses that are actually error pages), SSRF protection, and per-tier diagnostics in every response.
为什么需要它
大多数 Agent 自带的网页抓取是 HTTP 客户端 + 正文提取。对静态站点够用,但遇到下面三种情况会失败:
| 问题 | 表现 |
|---|---|
| JS 渲染 | 返回空壳页面,只有导航和页脚 |
| 反爬拦截 | 知乎返回 403,CSDN 返回 521 |
| 提取退化 | 页面上有内容,提取出来却是零散片段 |
本项目用真实 Chromium 渲染,并补齐完整浏览器指纹(UA / Sec-Ch-Ua / Sec-Fetch-* 全套一致)+ 导航器覆盖。
实测对比
同一组 6 个 URL,开浏览器 vs 关浏览器(本项目 Fetcher,2026-09 实测):
| 成功率 | 知乎 | CSDN 字符数 | 博客园字符数 | |
|---|---|---|---|---|
| 有浏览器 | 6/6 = 100% | 27069 | 57218 | 15360 |
| 无浏览器(仅静态路径) | 5/6 = 83% | 失败 | 13845(−76%) | 4627(−70%) |
反爬站点直接失败,其他站点正文完整度也大幅下降——没有 JS 渲染只能拿到首屏片段。
已实测可抓取的站点
知乎专栏、CSDN、博客园、掘金、豆瓣读书、少数派、腾讯云社区、React 官方文档、Python 官方文档等。单页耗时 2–5 秒(浏览器暖启动后)。
快速开始
git clone git@github.com:TC635807/better-crawler4agent.git
cd better-crawler4agent
python scripts/install.py
install.py 会自动:
- 在仓库内建
.venv并安装依赖 - 查找并复用系统中已有的 Playwright 浏览器(核对版本,避免重复下载 ~700MB)
- 打印各 agent 的接入配置,复制粘贴即可
验证安装:
python scripts/selfcheck.py # 依赖 + 浏览器 + 安全校验 + 真实抓取
python scripts/selfcheck.py --offline # 不联网
安装选项
python scripts/install.py --print-config # 只看配置,不安装
python scripts/install.py --reuse-env <path> # 复用已有 Python 环境
python scripts/install.py --browsers-path <p> # 指定已有浏览器目录
python scripts/install.py --no-browser # 稍后自己配浏览器
接入你的 Agent
通用 mcpServers 格式
Claude Code / Cursor / Windsurf / Cline 等通用:
{
"mcpServers": {
"better-crawler": {
"command": "D:\\better-crawler4agent\\.venv\\Scripts\\python.exe",
"args": ["D:\\better-crawler4agent\\scripts\\launch.py"],
"env": {
"BETTER_CRAWLER_BROWSERS_PATH": "D:\\better-crawler4agent\\.playwright"
}
}
}
}
路径用
python scripts/install.py --print-config生成,它会填好你机器上的绝对路径。
配置放置位置:
| 客户端 | 位置 |
|---|---|
| Claude Code | 项目级 <项目>/.mcp.json,或用户级 ~/.claude.json |
| Cursor | ~/.cursor/mcp.json |
| Windsurf / Cline | 各自的 MCP 设置里粘贴 JSON |
| ZCode | ~/.zcode/cli/config.json 的 mcp.servers 字段 |
| Codex | ~/.codex/config.toml → [mcp_servers.better-crawler] |
| DeepSeek Harness (dsh) | 用仓库自带的 dsh-plugin/,见下 |
仓库根目录带了一份 .mcp.json(相对路径),可直接引用。
DeepSeek Harness(dsh)
dsh 的插件形态和通用 MCP 配置不同:dsh 插件是一个声明了 dsh.bundle.patch 的包。仓库根目录的 package.json 就是这个声明——整个仓库即一个 dsh bundle,里面通过 @deepseek-ai/dsh-mcp-client 把本仓库的 MCP server 注册进 profile。
dsh plugin --profile web add github:TC635807/better-crawler4agent # 从 GitHub 装
./dsh-plugin/install.sh # 本地 clone 装(外加发布技能)
dsh-plugin/cordis.patch.yml 的启动器路径由 !!js 表达式在 profile 启动时算出,安装期不需要改写任何文件。装好后模型多出三个工具:mcp__better-crawler__fetch_url、mcp__better-crawler__fetch_urls、mcp__better-crawler__crawler_status。包不带 .venv,首次使用前先在有依赖的解释器里跑一次 python scripts/install.py;配置项、解释器与浏览器路径的解析方式见 dsh-plugin/README.md。
不用 MCP,直接当库调用
import sys
sys.path.insert(0, r"D:\better-crawler4agent\src")
from better_crawler import Fetcher
async def main():
f = Fetcher()
await f.start() # 预热浏览器
result = await f.fetch("https://example.com/article")
if result.ok:
print(result.content)
print(f"命中层级: {result.tier}, {result.content_length} 字符")
else:
print(f"失败: {result.error}")
await f.close()
install.py --print-config 会打印适合你环境的这段代码。
工具
| 工具 | 说明 |
|---|---|
fetch_url(url, max_chars) |
抓单个页面 |
fetch_urls(urls, max_chars, max_concurrent) |
并发抓取,逐条独立成败,最多 20 条 |
crawler_status() |
查浏览器状态,排查启动失败 |
返回结构
{
"url": "https://...",
"ok": true,
"title": "页面标题",
"content": "正文……",
"content_length": 13845,
"status": 200,
"tier": "browser",
"elapsed": 3.0,
"error": "",
"warning": "",
"truncated": false,
"attempts": [{"tier": "browser", "ok": true, "chars": 13845}]
}
设计要点是不只返回成功/失败:
status— 服务器返回的 HTTP 状态码,用来区分"页面不存在"(404)与"抓取被拦"(403/429)tier— 命中哪条路径:browser>trafilatura>httpxwarning— 非空表示拿到了内容但可能不完整(如异步接口多次失败),需要调用方判断content_length— 截断前的真实长度(truncated表示返回内容被max_chars截过)attempts— 每一层的具体结果与失败原因
失败时 error 会给出可操作的原因,而不是笼统的"抓取失败":
| error 内容 | 含义 | 该怎么做 |
|---|---|---|
服务器返回 HTTP 404(页面不存在) |
URL 失效 | 换 URL |
服务器返回 HTTP 403(服务器拒绝访问(可能触发反爬)) |
被拦截 | 可重试 |
服务器返回 HTTP 502(网关错误(服务端异常)) |
服务端异常 | 稍后重试 |
页面提示内容不存在或已被删除(服务端返回 200) |
软 404:状态码正常但页面是错误页 | 换 URL |
服务器返回 HTTP 200 但页面无有效正文 |
页面存在但没有正文(如纯图片/需登录) | 视情况 |
域名无法解析(DNS 问题) |
网络环境问题 | 检查 DNS/网络 |
最后两类值得注意:站点可能用 200 状态码渲染"页面不存在",或者页面确实没有文字内容。只看 HTTP 状态码会误判成成功,所以本工具会额外识别页面级错误特征。
抓取策略
分层降级,每层记录耗时与命中情况:
1. 浏览器渲染(主路径)
共享单例 Chromium + 信号量限并发 + 崩溃自动重建
冷启动 2–7s,之后单页 3–9s(含重试)
内容疑似未渲染时自动重试,最多 3 次
2. trafilatura 直取(快路径)
静态站约 1s;反爬站记录失败域名,10 分钟内跳过这层
3. httpx + 完整浏览器指纹(兜底)
补 Sec-Ch-Ua / Sec-Fetch-* / HTTP2,解决 CSDN 等站的 521
浏览器不可用时(未安装等)自动降级为后两层,crawler_status 会给出原因。
自动重试:应对概率性渲染失败
有些站点(检索结果页、SPA)的内容靠接口异步填充,而接口会概率性失败——同一个 URL 反复抓,页面会在"完整内容"和"只有导航的骨架"之间随机跳。
实测万方检索页:
| 状态 | HTML 大小 | 提取结果 |
|---|---|---|
| 接口失败 | 193 KB | 727 字符(仅导航) |
| 接口成功 | 481 KB | 5720 字符(459 条文献) |
关键是加长等待时间没用(实测等 5 秒照样失败),因为这不是渲染慢,是接口这一次没返回数据。
所以工具改成:识别骨架页 → 自动重试 → 取历次最完整的结果。
- 判据是"HTML 结构不小(>30KB)但正文极少(<1500 字符)"
- 确定性的失败(404、403)不重试,避免浪费时间
- 若重试后仍只拿到少量内容,返回
ok: true但带warning字段,提示内容可能不完整,由调用方判断
性能参考
实测 6 条混合 URL(含需重试的骨架页、确定性 404):
| 指标 | 数值 |
|---|---|
| 平均单条 | ~5s |
| 最坏单条 | ~9s(三次重试) |
| 6 条并发 3 | ~13s |
多数 MCP 客户端有 30 秒调用超时,因此 fetch_urls 建议一次不超过 8 条,更多请分批。
工程细节
这些是踩坑换来的,改动前请先读代码注释:
- 单例浏览器 + 信号量 — 每 URL 起一个 Chromium 会迅速吃满内存
asyncio.shield+done_callback— 超时取消会让 Playwright 内部 future 异常泄漏(TargetClosedError: future exception was never retrieved)- 崩溃检测 + 单例重置 — Chromium 挂掉后必须重建,否则后续全部失败
- 字体蜜罐识别 — 知乎会注入上万字符的
mmmmlli重复串,纯按长度取正文会误选它 - 假成功识别 — 200 响应可能是错误页、登录墙或文章已删除(知乎会渲染一张沙漠插画)
- 骨架页重试 — 内容靠异步接口填充的站点会概率性停在空壳状态,需重试而非加长等待
- 错误原因分类 — HTTP 状态码与 Chromium 网络错误映射成可读说明,让调用方能判断该重试还是换 URL
--headless=new— 缺了它,部分站点只返回空壳
配置
全部通过环境变量,都有合理默认值:
| 变量 | 默认 | 说明 |
|---|---|---|
BETTER_CRAWLER_PYTHON |
— | 指定解释器,覆盖自动探测 |
BETTER_CRAWLER_BROWSERS_PATH |
— | Chromium 所在目录 |
BETTER_CRAWLER_TIMEOUT |
25 |
单页抓取超时(秒) |
BETTER_CRAWLER_MAX_CHARS |
50000 |
默认正文返回上限 |
BETTER_CRAWLER_ALLOW_PRIVATE |
0 |
设为 1 放开内网限制(仅本地调试) |
BETTER_CRAWLER_LOG_LEVEL |
INFO |
日志级别(输出到 stderr) |
关于 Playwright 版本
requirements.txt 锁定 playwright==1.61.0。原因是 playwright 与浏览器 revision 强绑定(1.61 → rev 1228,1.62 → rev 1234),放宽版本会让"复用已有浏览器"因 revision 不匹配而静默失败。
升级 playwright 时必须同步重装浏览器:
python -m playwright install chromium
安全
- 仅允许
http/https,拒绝file://、raw://等协议 - 解析目标主机名,拒绝回环、内网、链路本地、云元数据地址(
127.0.0.1、192.168.*、169.254.169.254等) - 默认拒绝,本地调试需显式设
BETTER_CRAWLER_ALLOW_PRIVATE=1
一个"抓任意 URL"的工具本质上是本地网络请求原语。请只在可信环境启用。
项目结构
better-crawler4agent/
├── .mcp.json # 通用 MCP 配置(相对路径)
├── package.json # dsh bundle 声明:dsh.bundle.patch 指向 dsh-plugin/
├── dsh-plugin/ # dsh 插件:bundle patch + 启动器 + 安装脚本
├── skills/web-to-text/SKILL.md # 技能:何时用、如何选 URL、如何判断结果
├── src/better_crawler/
│ ├── browser.py # 共享 Chromium 单例、指纹、崩溃重建
│ ├── extract.py # 正文提取 + 蜜罐识别 + 错误页识别
│ ├── fetcher.py # 分层编排
│ ├── errors.py # HTTP 状态码与网络错误 → 可读说明
│ ├── safety.py # SSRF 防护
│ └── mcp_server.py # MCP 工具定义
├── scripts/
│ ├── launch.py # 启动器:解析解释器与浏览器路径
│ ├── install.py # 安装 + 打印各 agent 配置
│ └── selfcheck.py # 自检
└── tests/test_core.py # 单元测试(不联网)
关于 skill
skills/web-to-text/SKILL.md 是纯 Markdown,与厂商无关,内容涵盖:何时该抓取、如何挑选高质量 URL(避开 /tag/ /user/ /video/ 等聚合页)、如何判断结果是否可信。
支持 skill 的 agent(Claude Code、ZCode 等)可直接复制目录;不支持的,可把内容并入系统提示词。
开发
python -m pytest tests/ -q # 单元测试(34 项,不联网)
python scripts/selfcheck.py # 端到端自检(15 项,含真实抓取)
单元测试覆盖 SSRF 防护、蜜罐识别、正文提取、错误页识别。自检额外验证依赖可用性、浏览器启动与真实抓取(--offline 跳过抓取,为 12 项)。
合规
抓取请遵守目标站点的 robots.txt 与服务条款。本工具适合按需读取具体页面,不要用于高频批量抓取。
No comments yet. Be the first to write one.