Metadata-Version: 2.5
Name: mcp-scrape
Version: 0.2.1
Summary: 反爬感知抓取 + 极省 Token 内容精简的 MCP Server
Author: guanyuyan
License-Expression: MIT
License-File: LICENSE
Keywords: anti-bot,llm,mcp,scraping,token-efficiency,web-fetch
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Requires-Dist: curl-cffi>=0.7.0
Requires-Dist: mcp<3,>=2.0.0
Requires-Dist: playwright>=1.49
Requires-Dist: pydantic-settings>=2.2.0
Requires-Dist: selectolax>=0.4.11
Requires-Dist: tiktoken>=0.7.0
Provides-Extra: dev
Requires-Dist: pre-commit>=3.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6.0; extra == 'dev'
Description-Content-Type: text/markdown

# scrape-mcp

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

> **反爬感知抓取 + 极省 Token 内容精简的 MCP Server —— 让 AI 读真实网页时，只花 1%~5% 的 token 就能拿到干净、真实、免登录的正文。**
>
> 自动化分三层应对：默认 curl_cffi（伪装 TLS/JA3 + HTTP2）低成本打头阵，命中 Cloudflare/验证码/WAF 才升级 Playwright 真浏览器渲染；遇到"登录才给内容"的站，先弹窗手动登录一次、登录态落盘，之后自动带上、过期会提示重登。

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Release](https://img.shields.io/github/v/release/guanyuyan/scrape-mcp?color=blue&label=release)](https://github.com/guanyuyan/scrape-mcp/releases)
[![CI](https://github.com/guanyuyan/scrape-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/guanyuyan/scrape-mcp/actions)

---

## ⚡ 60 秒入门

**方式 A：一键安装（推荐，零配置）**

```bash
pip install mcp-scrape        # 已发布到 PyPI 后
# 或从源码：cd scrape-mcp && pip install -e .
```

然后本机任意目录即可：

```bash
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 效果）

```bash
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`）

```bash
scrape-mcp            # 已安装时；或 python -m scrape_mcp.server
```

在客户端里调用 `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，拦截才升级，不误伤正常页面。
- **内容密度选根**：从 article/main/div 中挑出"恰好装住正文的最紧容器"，避开导航/侧栏。
- **compact 精简**：属性剥离、模板块丢弃、站外链接降级、表格转 CSV、token 预算截断，
  压缩率可达 1~20% 数量级（如 MDN 214KB → 数百 token）。
- **拦截如实上报**：不把挑战页/空壳页当正文喂给模型，返回 `blocked`/`block_reason`。
- **登录态持久化**：`login` 工具手动登录并落盘，`web_fetch` 自动带上；过期会提示重登。
- **缓存与批量**：L1 明文响应按 TTL 磁盘缓存；`web_batch` 并发抓取一批 URL。
- **Stealth 加固**：Wipe `webdriver`、UA 数据、Canvas/WebGL 确定性指纹噪声、时区对齐。

## 安装

```bash
# 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）

```bash
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`** 文件里。

```bash
# 项目根目录执行
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_TIMEZONE` | `Asia/Shanghai` | 隐身加固时区 |
| `DATA_DIR` | `~/.scrape_mcp` | 登录态/缓存放盘目录 |
| `CACHE_TTL` | `300` | L1 缓存 TTL（秒），`0` 关闭 |
| `BATCH_MAX_CONCURRENCY` | `4` | `web_batch` 并发上限 |
| `BATCH_MAX_URLS` | `20` | `web_batch` 单批上限 |
| `RESPECT_ROBOTS` | `true` | 默认遵循目标站 robots.txt；`false` 忽略（仅建议自用合规场景关闭） |
| `ALLOWED_HOSTS` | — | 主机白名单（逗号分隔）；非空时仅允许名单内主机 |
| `DENIED_HOSTS` | — | 主机黑名单（逗号分隔），优先于白名单 |
| `MAX_QPS` | `10` | 全网抓取限流（每秒请求数）；`0` 不限速 |

## HTTP 模式（Postman / curl 直连）

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

```bash
$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` 即可。**

必需项：请求头 `MCP-Method`、`Mcp-Name`；body 内 `params` 需带 `_meta`（`protocolVersion` + `clientCapabilities`）。完整示例见下。

## 接入 MCP 客户端（Claude / Cursor 等）

先 `pip install mcp-scrape`（或用项目 `.venv` 跑 `pip install -e .`），使 `scrape-mcp` 命令全局可用；然后按标准 MCP 方式接入。

以 Claude Desktop 为例（`claude_desktop_config.json`）：

```json
{
  "mcpServers": {
    "scrape-mcp": {
      "command": "scrape-mcp"
    }
  }
}
```

- 用 `command: "scrape-mcp"`（已全局安装），免去 `PYTHONPATH` 与系统解释器导致的 `ModuleNotFoundError`。
- 若复用系统浏览器（免下载）：设环境变量 `SCRAPE_MCP_L2_CHANNEL` 为 `chrome` 或 `msedge`。
- 配对成功后重启客户端，即可在会话里直接调用 `web_fetch` / `web_extract` / `web_batch`。

## MCP 工具

- **`web_fetch(url, max_tokens=4000, link_policy="internal")`** → 状态 + 极省正文
  - `link_policy`: `internal`（站内相对链接+站外降级）/ `all` / `none`
- **`web_extract(url, schema={...})`** → 按字段 schema 抽取结构化 JSON（可入库）
  - 字段类型：`text`（默认）/ `attr`（需配 `attr` 取属性）/ `count` / `list`（`list_key` 取值）
- **`web_batch(urls, max_tokens=3000, link_policy="internal")`** → 并发抓取一批，逐 URL 返回；单条异常不影响整批
- **`login(url, timeout=180)`** → 打开带界面浏览器手动登录并持久化登录态

返回为 JSON 字符串（落在 `content[0].text`，请解析文本，勿依赖 `structured_content`）。

### web_extract 示例

```bash
schema='{"fields": {
  "title":    {"selector": "h1", "type": "text"},
  "main_link":{"selector": "a", "type": "attr", "attr": "href"},
  "tag_count":{"selector": ".tag", "type": "count"},
  "tags":     {"selector": ".tag", "type": "list", "list_key": "text"}
}}'
# → {"ok":true, "data": {"title":"…","main_link":"…","tag_count":3,"tags":["a","b","c"]}, "missing":[]}
```

## 目录结构

```
src/scrape_mcp
├── server.py          # MCP 入口 + 工具（web_fetch/web_batch/login）
├── 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     # 登录态持久化
│   └── cache.py       # HTTP 响应缓存
└── extract/
    ├── compact.py     # DOM 剪枝 → 极省 token 正文
    └── schema.py      # 结构化抽取（按字段 schema 出 JSON）
```

## 开发

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