Skip to main content

scrape-mcp

⚠️ 这是 scrape-mcp 的官方源仓库(upstream)。 若在别处看到同名/相似项目并带不同作者署名,即为转载或派生,非原作者发布。

反爬感知抓取 + 极省 Token 内容精简的 MCP Server —— 让 AI 读真实网页时,只花 1%~5% 的 token 就能拿到干净、真实、免登录的正文。

自动化分三层应对:默认 curl_cffi(伪装 TLS/JA3 + HTTP2)低成本打头阵,命中 Cloudflare/验证码/WAF 才升级 Playwright 真浏览器渲染;遇到"登录才给内容"的站,先弹窗手动登录一次、登录态落盘,之后自动带上、过期会提示重登。

License: MIT PyPI Python Release CI

简体中文 | English


⚡ 60 秒入门

方式 A:一键安装(推荐,零配置)

pip install mcp-scrape
# 或从源码:cd scrape-mcp && pip install -e .

然后本机任意目录即可:

scrape-mcp --version          # 确认装好(应输出 0.x)
claude                        # 在支持 MCP 的客户端里把它当服务接入

不再需要 clone、手动设 PYTHONPATH 或 python -m;scrape-mcp 命令在任意目录可用, 默认 stdio 直接被 MCP 客户端接管。想用 HTTP 直连(Postman/curl),跑 scrape-mcp --transport streamable-http --port 8000。

方式 B:一键演示(拉源仓库跑 web_fetch + web_extract,看省 token 效果)

git clone https://github.com/guanyuyan/scrape-mcp.git
cd scrape-mcp
python -m venv .venv && .venv\Scripts\pip install -e .
.venv\Scripts\python scripts\demo.py

预计输出(对不同站点自动选 L1/L2 分级,给出省 token 压缩比):

=== 文档站 | https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
  ok=True tier=l1 status=200 html=260128B → tokens=3730 (约 5.7% 压缩)
  标题: 跨源资源共享(CORS) - HTTP | MDN
  ok=True tier=l1 data={"heading": "跨源资源共享(CORS)", "h2_count": 10, "links": 665}

第 2 步:接入 MCP 客户端(Claude / Cursor 等,一条命令写好配置)

scrape-mcp init --client cursor --write     # claude / vscode / json 同理

默认只打印 diff 预览,确认后加 --write 落盘;重启客户端即可调用 web_fetch(url=...)。

MDN 整页约 260KB,这里只花几千 token 就拿到干净正文;拦截/登录页会自动升级或如实上报,不会把验证码当正文。


功能说明

它解决什么:模型读网页不是在读正文,而是在读整棵 DOM(脚本、导航、版权、链接一应尽收),又贵又脏。这个工具把页面剪成"只有正文"再喂给 AI。

核心价值(三点)

  1. 极省 token —— 属性剥离 + 模板块丢弃 + 站外链接降级 + 表格转 CSV + 预算截断。实测 MDN 整页 214KB → 数百 token,压缩率约 1%。
  2. 自动分级反爬 —— 85% 的站只走最轻的 L1(快、并发高),拦截/JS 骨架页才升级 L2 真浏览器 + stealth 伪装,绝不暴力逐站上浏览器。
  3. 登录墙处理 + 如实上报 —— 判定登录/拦截时返回 login_required/blocked,引导登录而非硬撞,从不让模型把验证码当正文。

怎么用(一行)

web_fetch(url="https://xxx/article/1") → JSON:{ok, content(极省正文), token_estimate, tier, blocked?, login_required?, cached?}

覆盖的站点类型:文章页 / 文档站 / 门户与列表页 / 需要 JS 渲染的 SPA / 需要登录才给正文的站(如知乎)。

真实站点实测(一键 scripts/demo.py 可复现):

站点 类型 分级 原始 抓到正文 说明
MDN CORS 文档 文档站 L1 260KB ~3.7k token(约 5.7%) 纯静态,最短路径直达
腾讯新闻首页 列表/门户 L2 338KB ~1.4k token(约 1.6%) 动态页升级浏览器渲染
极客邦首页 SPA/动态 L2 12KB ~1.4k token JS 骨架页,需渲染才有正文

形态:本地 MCP Server(stdio/HTTP 均可),隐私可控;支持单页 web_fetch、批量 web_batch、结构化抽取 web_extract、登录持久化 login,缓存默认开启。


特性

  • 分级抓取:L1 curl_cffi → L2 Playwright,拦截才升级,不误伤正常页面。
  • L1 连接复用:同一指纹模板在进程内共享连接池,省掉重复的 TLS 握手与 DNS 解析;换事件循环自动重建,进程退出或 aclose() 统一释放。
  • L2 浏览器池:同站同会话复用 context(各带独立登录态),超出上限按最久未用淘汰;同时打开的页面数受限,超出排队;空闲超过 L2_IDLE_TIMEOUT 秒自动关掉浏览器进程,下次用到再启动。
  • 内容密度选根:从 article/main/div 中挑出"恰好装住正文的最紧容器",避开导航/侧栏。
  • compact 精简:属性剥离、模板块丢弃、站外链接降级、表格转 CSV(合并单元格自动补齐空位)、 代码块带语言标记、figure 输出图片地址与图注,压缩率可达 1~20% 数量级(如 MDN 214KB → 数百 token)。
  • 拦截如实上报:不把挑战页/空壳页当正文喂给模型,返回 blocked/block_reason。
  • 稳定错误码 + 可诊断:失败返回 error_code 与 retryable,debug=true 还能看到分层决策、选择器命中数与 L2 交互结果。
  • 登录态持久化 + 多会话:login 工具手动登录并落盘,web_fetch 自动带上;session="work" 可按身份隔离登录态,session_export/session_import 在机器之间搬运 cookie。
  • 非 HTML 内容:web_fetch 直接读纯文本;装 [pdf] extra 后按页抽取 PDF 文本,扫描件(无文字层)如实返回 empty,不伪造正文。
  • 两层缓存:HTTP 响应按 TTL 落盘,过期后带 ETag/Last-Modified 做条件请求;正文(compact 后)与字段抽取结果另有一层结果缓存,二次调用直接返回,bypass_cache=true 同时绕过两层。web_batch 并发抓取一批 URL。
  • 轻量爬取:web_crawl 支持两种模式——按 depth/limit 发现站内链接,或按 next_selector 顺序翻页(max_pages 封顶、自动去重、不跳站外);静态资源自动跳过。
  • 总预算与降级:web_fetch / web_extract / web_batch / web_crawl 都支持 deadline_ms。预算不够时按"压缩单步超时 → 跳过重试 → 不升级 L2 → 直接返回 timeout"逐级降级,已拿到的结果照常返回;降级结果不写入缓存,避免后续预算充足的调用拿到残缺内容。预算同样覆盖合规检查(限流等待与 robots.txt 抓取),不够就提前返回 timeout,不把时间耗在准备阶段。
  • 结构化抽取:web_extract 支持 text/html/attr/count/list/object/jsonld,配 regex、abs_url、convert(int/float/date)与 required 必填校验,缺字段直接返回 missing_required。
  • 站点 recipe:把某站的渲染方式、正文选择器、抽取 schema、翻页规则写成一份 JSON 落到本地,之后同一站点免参数复用,单次调用仍可覆盖。
  • 输出格式可选:format=markdown|text|json,正文可按段落数组直接入库。
  • 同域并发闸门:同一站点默认最多 2 个并发请求,跨站仍可并行,避免批量抓取时把单站打穿。
  • Stealth 加固:Wipe webdriver、UA 数据、Canvas/WebGL 确定性指纹噪声、时区对齐。

安装

pip install mcp-scrape                          # 核心:L1 抓取 + 精简 + 抽取
pip install "mcp-scrape[pdf]"                   # 可选:让 web_fetch 直接读 PDF
# L2 渲染所需浏览器(国内可用镜像)
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://cdn.npmmirror.com/binaries/playwright"
pip install "playwright>=1.49"
playwright install chromium

# 或复用系统浏览器,无需下载
$env:SCRAPE_MCP_L2_CHANNEL="chrome"   # 或 msedge

启动(MCP,stdio)

scrape-mcp                          # 默认 stdio,被 MCP 客户端接管
scrape-mcp --transport streamable-http --port 8000   # 切 HTTP 直连
scrape-mcp --version / --help       # 版本 / 用法

不传参默认 stdio。传 --transport/--port 由 CLI 显式启动,优先级高于 .env/环境变量。

环境变量(前缀 SCRAPE_MCP_)

全部配置可用环境变量(SCRAPE_MCP_ 前缀)设置,也可集中放到项目根目录 .env 文件里。

# 项目根目录执行
cp .env.example .env    # 生成配置模板
# 编辑 .env 后无需改代码,直接启动即可

优先级:实际进程环境变量 > .env > 默认值。.env 被 gitignore,不入库;.env.example 是随仓库提交的模板。

变量 默认 说明
IMPERSONATE chrome curl_cffi 指纹模板
REQUEST_TIMEOUT 20 L1 超时(秒)
PROXY — 代理,L1/L2 共用
L2_ENABLED true 是否启用 Playwright 升级链
L2_CHANNEL — 关联浏览器:空=自带 Chromium,chrome/msedge=系统
L2_SETTLE_TIMEOUT 8 挑战页等待窗口(秒)
L2_SCROLL_STEPS 5 L2 滚动步数(触发懒加载)
L2_SCROLL_DELAY_MS 400 L2 每步滚动间隔(毫秒)
L2_MAX_PAGES 3 L2 同时打开的页面上限,超出排队
L2_MAX_CONTEXTS 8 L2 常驻 context 上限(按 host + session 复用),超出淘汰最久未用
L2_IDLE_TIMEOUT 120 L2 浏览器空闲回收阈值(秒),0 常驻到进程退出
L2_TIMEZONE Asia/Shanghai 隐身加固时区
DATA_DIR ~/.scrape_mcp 登录态/缓存放盘目录
RECIPES_DIR <DATA_DIR>/recipes 站点 recipe 目录;留空用默认路径
PDF_MAX_PAGES 200 PDF 单次最多解析页数(超出部分不抽取,pages 仍返回总页数)
CACHE_TTL 300 两层缓存 TTL(秒),0 全关;HTTP 层过期后带 ETag/Last-Modified 做条件请求,正文/抽取结果层命中即直接返回
RETRY_TIMES 2 网络异常/5xx 重试次数(429/403 不重试)
RETRY_BACKOFF 0.5 重试退避基数(秒,指数增长)
RETRY_AFTER_MAX_WAIT 0 429/503 带 Retry-After 时最多等待秒数;0 只上报不等待
LOG_LEVEL WARNING 日志级别,JSON 行写 stderr(stdout 留给 MCP 协议)
BATCH_MAX_CONCURRENCY 4 web_batch 并发上限
BATCH_MAX_URLS 20 web_batch 单批上限
HOST_MAX_CONCURRENCY 2 同一主机的并发上限(web_batch/web_crawl 共用)
CRAWL_MAX_PAGES 20 web_crawl 单次最多抓取页数硬上限
RESPECT_ROBOTS true 默认遵循目标站 robots.txt;false 忽略(仅建议自用合规场景关闭)
ALLOWED_HOSTS — 主机白名单(逗号分隔);非空时仅允许名单内主机
DENIED_HOSTS — 主机黑名单(逗号分隔),优先于白名单
MAX_QPS 10 全网抓取限流(每秒请求数);0 不限速

HTTP 模式(Postman / curl 直连)

默认 stdio,改环境变量即可切到 HTTP,无需改代码:

$env:SCRAPE_MCP_TRANSPORT="streamable-http"
$env:SCRAPE_MCP_HTTP_PORT="8000"
python -m scrape_mcp.server

以 json_response=1(默认)启动,请求/响应都是普通 JSON(非事件流)。stateless 模式无需先 initialize,直接发 tools/call 即可。 健康检查:GET /healthz 返回版本与关键开关,可用于容器探针/网关巡检。

本地统计:GET /stats 返回进程内计数,只接受回环地址访问(其它来源返回 403):

curl http://127.0.0.1:8000/stats
# {"ok":true,"uptime_s":42,"requests":7,"fetches":5,"result_cache_hits":2,
#  "http_cache_hits":1,"cache_hit_rate":0.286,"by_tier":{"l1":4,"l2":1},
#  "by_code":{"ok":5},"by_kind":{"html":5},"tokens_returned":8120,"bytes_in":1048576}

by_tier 是实际生效的层级分布,by_code 复用工具返回的错误码(成功为 ok),cache_hit_rate 是结果缓存命中数 ÷ 总请求数。统计只记计数与字节数,不记 URL、不记页面内容,进程重启后归零。

必需项:请求头 MCP-Method、Mcp-Name;body 内 params 需带 _meta(protocolVersion + clientCapabilities)。完整示例见下。

接入 MCP 客户端(Claude / Cursor 等)

先 pip install mcp-scrape(或用项目 .venv 跑 pip install -e .),使 scrape-mcp 命令全局可用。

一条命令生成配置(推荐)

scrape-mcp init --client cursor --write     # 或 claude / vscode / json
  • 默认只打印 diff 预览,加 --write 才落盘;目标文件里其它服务配置原样保留
  • --client json 直接打印 JSON(不落盘),适合贴到任意客户端或自定义路径
  • 已有同名 scrape-mcp 条目时不覆盖,确需更新加 --force
  • --scope global|project 控制写全局还是项目内(.cursor/mcp.json、.vscode/mcp.json)
  • --env KEY=VALUE 可把 SCRAPE_MCP_PROXY、SCRAPE_MCP_L2_CHANNEL 等写进客户端配置
  • --command 可指定启动命令;缺省优先用已安装的 scrape-mcp,找不到则退回当前解释器 + -m scrape_mcp.server

生成的结构(Claude / Cursor 用 mcpServers,VS Code 用 servers 且带 "type": "stdio"):

{
  "mcpServers": {
    "scrape-mcp": {
      "command": "scrape-mcp",
      "args": []
    }
  }
}
  • 若复用系统浏览器(免下载):设环境变量 SCRAPE_MCP_L2_CHANNEL 为 chrome 或 msedge。
  • 配对成功后重启客户端,即可在会话里直接调用 web_fetch / web_extract / web_batch / web_crawl。
  • 想先看工具定义(名称、说明、入参 schema):scrape-mcp schema --tool web_fetch,或 --output tools.json 导出全量。

MCP 工具

  • web_fetch(url, max_tokens=4000, link_policy="internal", format="markdown", ...) → 状态 + 极省正文
    • link_policy: internal(站内相对链接+站外降级)/ all / none
    • format: markdown(默认,保留标题/列表/链接)/ text(剥掉标记的纯文本)/ json(content 为段落数组)
    • 抓取参数:headers / cookies(自定义请求,不走共享缓存)、timeout、bypass_cache
    • 总预算:deadline_ms(毫秒,可选)限制整次调用;不够时逐级降级,见"错误码与诊断"中的 deadline:* 说明
    • 登录态:session(默认 default),与 login 时的会话名一致即可自动带上对应登录态
    • 渲染参数(仅 L2 生效):wait_for(等 CSS 选择器)、scroll(滚动触发懒加载)、click(点"加载更多")
    • 非 HTML:PDF 返回结构化文本(装 [pdf] extra;kind="pdf" 且带 pages/pages_parsed),纯文本原样返回(kind="text")
    • 诊断参数:debug=true 时返回 diagnostics(决策轨迹、选择器命中数、L2 交互结果),不含页面内容
  • web_extract(url, schema=None, ...) → 按字段 schema 抽取结构化 JSON(可入库)
    • schema 可省略:命中站点 recipe 时直接用 recipe 里的 schema,否则返回 invalid_argument
    • 字段类型:text(默认)/ html / attr(需配 attr)/ count / list / object / jsonld(需配 path)
    • 嵌套:list 配 fields 返回对象数组,object 在匹配节点内部继续抽取
    • 后处理:regex + group(正则提取,未命中回落 default)、abs_url(相对链接转绝对)、strip
    • 类型转换:convert 可设 int / float / date(去千分位与货币符号;日期兼容 ISO 与 2026年10月20日 这类写法)
    • 必填校验:字段设 required: true,缺失时整次调用 ok=false、error_code=missing_required,并在 required_missing 列出字段名
    • jsonld 用点号路径从 <script type="application/ld+json"> 取值(多个 script / @graph 自动聚合,数组自动展开、数字段视为下标)
    • 抓取/渲染参数与 web_fetch 相同(不含 link_policy/format);非 HTML(PDF/纯文本)返回 unsupported_content,请改用 web_fetch
  • web_batch(urls, max_tokens=3000, link_policy="internal", format="markdown", ...) → 并发抓取一批,逐 URL 返回;单条异常不影响整批
    • 支持 headers / cookies / timeout / bypass_cache / debug / session(对该批所有 URL 生效)
    • deadline_ms 为整批共用的预算:耗尽后还没开始的子任务直接返回 timeout,不再占用网络与浏览器
    • 客户端请求进度(传 progressToken)时,每完成一条上报 已抓取 n/N 个 URL
  • web_crawl(url, depth=1, limit=10, same_host=True, max_tokens=2000, ...) → 从一个入口逐页抓取,返回带 mode 标明模式
    • mode=links(默认):按 depth/limit 发现站内链接逐层抓取;depth 最大 3,limit 受 CRAWL_MAX_PAGES 约束
    • mode=paginate:传 next_selector 即启用分页模式,按"下一页"链接顺序翻页,max_pages 封顶(默认 5),已访问 URL 自动去重,默认不跟随站外链接(same_host=false 可放开)
    • 单页失败只影响该页;图片/压缩包等静态资源链接自动跳过;session 对该次所有页面生效
    • deadline_ms 为整次爬取的预算:耗尽后不再抓新页,已抓到的页面照常返回
    • 客户端请求进度时逐页上报(分页模式为 已翻到第 n/N 页,链接模式为 已抓取 n/N 页)
  • login(url, timeout=180, session="default") → 打开带界面浏览器手动登录并持久化登录态
    • 多账号场景用不同 session 名分别登录,web_fetch 时传同一个名字即可,互不覆盖
  • session_list() → 列出本地已保存的登录态(session + host + updated_at),不含 cookie 内容
  • session_export(url, session="default", path=None) → 导出该站 cookie({host, session, cookies});传 path 时同时写入本地文件
  • session_import(url, path, session="default", replace=False) → 从本地 JSON 导入 cookie
    • 兼容 session_export 产物、Playwright storage_state 或裸 cookie 数组
    • 默认按 (name, domain, path) 合并(同名以导入值为准),replace=true 整体替换;只读本地文件,不发网络请求

返回为 JSON 字符串(落在 content[0].text,请解析文本,勿依赖 structured_content)。 失败时带稳定 error_code 与 retryable;传输失败/5xx 会自动指数退避重试,attempts 为实际尝试次数;L2 交互未命中(如选择器不存在)只在 notes 中提示,不判失败。

错误码与诊断

error_code 是协议的一部分(只增不改),retryable 直接告诉你"过一会儿重试是否有意义":

error_code retryable 含义 / 下一步
timeout / dns / tls / connection / unknown ✅ 网络层问题,稍后重试或检查代理
http_5xx ✅ 目标服务端异常,可退避重试
http_4xx ❌ 404/410 等,页面本身无效
blocked ❌ 被反爬拦截(Cloudflare/WAF/限流),换时段或人工确认
login_required ❌ 需要登录态,先调用 login
denied / robots_disallowed ❌ 黑白名单或 robots.txt 拒绝
missing_required ❌ 必填字段缺失,见 required_missing;补齐 schema 或放宽 required
unsupported_content ❌ 内容形态不支持当前工具(如对 PDF 用 web_extract);改用 web_fetch,PDF 需装 [pdf] extra
invalid_url / invalid_argument ❌ 参数问题

失败时默认带精简 diagnostics.decisions(如 ["retry:5xx", "escalate_l2:cloudflare", "l2_ok"]); 传了 deadline_ms 且触发降级时,会看到 deadline:skip_retry(预算不够再等一次退避)、 deadline:skip_l2(不够启动浏览器,返回 L1 结果)或 deadline:exceeded(没来得及发起请求)。 传 debug=true 会展开更多计数:

{"diagnostics": {
  "decisions": ["escalate_l2:shell", "l2_ok"],
  "attempts": 1,
  "html_bytes": 348210,
  "root": "div",
  "selector_hits": {"article": 0, "main": 1, "div": 812},
  "content_chars": 5820,
  "l2": {"wait_for_hit": null, "clicked": null, "scroll_rounds": 0, "error": null}
}}

429/503 带 Retry-After 时返回 retry_after_s;配置 RETRY_AFTER_MAX_WAIT 后可在上限内自动补一次重试。

web_extract 示例

schema='{"fields": {
  "title":     {"selector": "h1", "required": true},
  "price":     {"selector": ".price", "regex": "¥([\\d,.]+)", "group": 1,
                "convert": "float", "required": true},
  "published": {"selector": "time", "type": "attr", "attr": "datetime", "convert": "date"},
  "main_link": {"selector": "a", "type": "attr", "attr": "href", "abs_url": true},
  "tag_count": {"selector": ".tag", "type": "count"},
  "tags":      {"selector": ".tag", "type": "list", "list_key": "text"},
  "author":    {"type": "jsonld", "path": "author.name"},
  "items":     {"selector": ".item", "type": "list", "fields": {
    "name": {"selector": ".name"},
    "url":  {"selector": "a", "type": "attr", "attr": "href", "abs_url": true}
  }}
}}'
# → {"ok":true, "data": {"title":"…","price":299.0,"published":"2026-10-20",
#    "main_link":"https://…","tag_count":3,"tags":["a","b","c"],
#    "author":"…","items":[{"name":"…","url":"https://…"}]},
#    "missing":[], "required_missing":[]}

必填字段缺失时返回 ok=false + error_code=missing_required,required_missing 里是缺失的字段名:

{"ok": false, "error_code": "missing_required", "error": "必填字段缺失: title, price",
 "retryable": false, "required_missing": ["title", "price"], "data": {"title": null, ...}}

站点 recipe

把"这个站怎么抓、取哪块、抽哪些字段、怎么翻页"写成一份 JSON 放进 RECIPES_DIR (默认 ~/.scrape_mcp/recipes),之后同一站点免参数复用。文件名即主机名, 也可用 host 字段覆盖;匹配按最长后缀(m.example.com 能命中 example.com), 单次调用的显式参数优先于 recipe。

~/.scrape_mcp/recipes/example.com.json:

{
  "host": "example.com",
  "wait_for": "main article",
  "scroll": true,
  "root_selector": "main article",
  "extract": {
    "fields": {
      "title": {"selector": "h1", "required": true},
      "author": {"type": "jsonld", "path": "author.name"},
      "published": {"selector": "time", "type": "attr", "attr": "datetime", "convert": "date"},
      "items": {
        "selector": ".item",
        "type": "list",
        "fields": {
          "name": {"selector": ".name"},
          "url": {"selector": "a", "type": "attr", "attr": "href", "abs_url": true}
        }
      }
    }
  },
  "crawl": {"next_selector": "a.next", "max_pages": 5}
}
字段 作用 生效范围
host 匹配的主机(缺省用文件名) 全部
wait_for / scroll / click L2 渲染交互,缺省则用调用参数 web_fetch / web_extract / web_batch / web_crawl
root_selector 指定正文根,命中失败自动回落密度选根 同上
extract web_extract 省略 schema 时的默认 schema web_extract
crawl.next_selector / crawl.max_pages 分页翻页规则,next_selector 一旦生效即进入 mode=paginate web_crawl
note 备注,仅供人读 —

改完 JSON 无需重启:按文件 mtime 自动重载。写坏的 JSON 会被跳过,不影响其他 recipe。

开箱示例(真实站点)

examples/recipes/ 里有三份可直接使用的 recipe,复制到 RECIPES_DIR 即可:

mkdir -p ~/.scrape_mcp/recipes
cp examples/recipes/*.json ~/.scrape_mcp/recipes/
文件 站点 演示的 recipe 能力
docs.python.org.json Python 官方文档 root_selector 取 div.body;crawl.next_selector 用 accesskey="N" 顺序翻页;extract 取标题/版本/章节数/下一章链接
github.com.json GitHub Trending type=list 一次抽整张榜单(仓库名、URL、今日 star、语言、简介)
ruanyifeng.com.json 阮一峰的网络日志 正文根 #main-content + regex 提取发布日期;host 用 ruanyifeng.com 以覆盖 www. 子域
# 按 recipe 抽取(不传 schema)
web_extract(url="https://docs.python.org/3/library/asyncio.html")
web_extract(url="https://github.com/trending")
web_extract(url="https://www.ruanyifeng.com/blog/2026/10/weekly-issue-415.html")

# 按 recipe 翻页(next_selector 生效即进入 mode=paginate)
web_crawl(url="https://docs.python.org/3/library/asyncio.html", max_pages=3)

示例选择器于 2026-10 实测通过;站点改版后失效属正常现象,改一行 selector 即可恢复。

单次覆盖:显式参数优先于 recipe,例如 web_fetch(url="...", wait_for=".comments")。

Python SDK

不接 MCP 也能直接用同一套实现(参数、返回值、错误码与工具完全一致):

import asyncio

from scrape_mcp.sdk import aclose, batch, crawl, extract, fetch


async def main() -> None:
    page = await fetch("https://docs.python.org/3/library/asyncio.html", max_tokens=2000)
    print(page["ok"], page["token_estimate"], page["tier"])

    # 按字段抽取;不传 schema 时套用该站 recipe
    data = await extract(
        "https://docs.python.org/3/library/asyncio.html",
        {
            "fields": {
                "title": {"selector": "h1"},
                "sections": {"selector": "section", "type": "count"},
            }
        },
    )
    print(data["data"])

    pages = await crawl(
        "https://docs.python.org/3/library/asyncio.html",
        next_selector='div.related a[accesskey="N"]',
        max_pages=3,
    )
    print(pages["mode"], pages["pages_fetched"])

    many = await batch(["https://example.com", "https://example.org"])
    print(many["ok_count"], "/", many["total"])

    await aclose()  # 触发过 L2 时释放浏览器子进程;不调用也能跑,只是退出时可能有清理告警


asyncio.run(main())
  • 返回的就是 MCP 工具的 JSON 载荷(已解析为 dict),失败同样带 error_code / retryable
  • SDK 放在 scrape_mcp.sdk,是为了不和内部 scrape_mcp.extract 子包重名

示例脚本

examples/ 下按场景提供 5 个可直接运行的脚本(默认参数就能跑通,需要代理时先设 SCRAPE_MCP_PROXY):

脚本 场景 常用参数
01_fetch_article.py 长文压成 token 预算内的正文 --max-tokens --out
02_extract_list.py 列表页抽结构化数据(不取整页正文) --json --csv
03_crawl_docs.py 按"下一页"翻页并合并成一份 markdown --selector --max-pages --out
04_pdf_to_text.py PDF 直接读成文本(需 mcp-scrape[pdf]) --out --max-tokens
05_batch_check.py 批量巡检,输出层级/状态/token/错误码,失败即非零退出 位置参数传 URL
python examples/01_fetch_article.py https://ruanyifeng.com/blog/ --max-tokens 1200
python examples/02_extract_list.py --csv trending.csv
python examples/03_crawl_docs.py --max-pages 5 --out docs.md

另有 examples/example_client.py(MCP 客户端调用示例)与 examples/recipes/*.json(站点 recipe)。

基准

python scripts/bench.py                              # 冷/热对照
python scripts/bench.py --l2 https://news.qq.com/    # 额外看浏览器开销

"冷" = bypass_cache=true(走网络 + 精简),"热" = 普通调用(命中正文缓存)。 2026-10 实测(Windows 11 / 家宽直连,rounds=5,max_tokens=4000):

页面 层级 冷 p50 热 p50 提升 token
example.com L2 495 ms 1.4 ms ~360× 330
docs.python.org(asyncio-task) L1 681 ms 1.0 ms ~650× 3588

L2 浏览器:进程内首次调用 ≈3.5 s(含启动),复用 p50 ≈1.8 s。 网络与目标站状态会明显影响绝对值,重点看同一台机器上的冷热对比。

Docker

# 仅 L1(镜像小,覆盖大多数页面)
docker build -t scrape-mcp .

# 带 L2 真浏览器(Cloudflare/验证码/SPA)
docker build --build-arg INSTALL_BROWSER=1 -t scrape-mcp:l2 .

docker run -p 8002:8002 scrape-mcp
curl http://127.0.0.1:8002/healthz     # {"ok":true,"version":"…"}

目录结构

src/scrape_mcp
├── server.py          # MCP 入口 + 工具(web_fetch/web_extract/web_batch/web_crawl/login/session_*)
├── cli.py             # 子命令:init(客户端配置)/ schema(导出工具定义)
├── sdk.py             # Python SDK 薄封装(fetch/extract/crawl/batch/aclose)
├── config.py          # 环境变量配置
├── tokenizer.py       # tiktoken 计数 + 预算截断
├── core/
│   ├── http_client.py # L1 curl_cffi + 磁盘缓存
│   ├── fetcher.py     # 分级调度(L1→L2),对工具层返回 FetchOutcome
│   ├── detector.py    # 拦截/骨架页判定
│   ├── browser.py     # L2 Playwright + stealth
│   ├── session.py     # 登录态持久化(多会话 + cookie 导入导出)
│   ├── recipes.py     # 站点 recipe(按 host 最长后缀匹配、mtime 重载)
│   ├── cache.py       # HTTP 响应缓存 + 正文/抽取结果缓存
│   ├── stats.py       # 进程内计数(/stats 端点数据源)
│   ├── errors.py      # 统一错误码
│   └── hostlimit.py   # 同域并发闸门
└── extract/
    ├── compact.py     # DOM 剪枝 → 极省 token 正文
    ├── schema.py      # 结构化抽取(按字段 schema 出 JSON)
    ├── links.py       # 链接发现(web_crawl 用)
    ├── pdf.py         # PDF → 文本(可选依赖 pypdf)
    └── render.py      # markdown → 纯文本 / 分块

开发

pip install -e ".[dev]"
python -m pytest

Metadata

Release files for mcp-scrape 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcp-scrape 0.7.0
File Size Uploaded Provenance
mcp_scrape-0.7.0.tar.gz 160.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mcp-scrape 0.7.0
File Interpreter ABI Platform
mcp_scrape-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 252.5 kB

Release files / mcp_scrape-0.7.0.tar.gz

Download URL mcp_scrape-0.7.0.tar.gz
Size 160.4 kB
Tags Source
SHA-256 checksum
How to use checksums
312076a43a719342a670019cd9b95fdc73e5e149e9e7a7f415f9fbec294ec0a8
BLAKE2b-256 checksum
How to use checksums
0acb7004b2d2c8b87ebb64959f8ca5e9f5aebc5f24d03768e38950a230a77645
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / mcp_scrape-0.7.0-py3-none-any.whl

Download URL mcp_scrape-0.7.0-py3-none-any.whl
Size 92.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
009644ed34070a8488a5627f705a4b18dc1b7b17366ffb00cf968b37ac5d2e7a
BLAKE2b-256 checksum
How to use checksums
3d01c2ede395882bf97d586ae3a199f7b2df41f6b7937f5b6e853a522846dd18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for mcp-scrape 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mcp_scrape-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page