爬取
Yozh Crawler 从单个种子 URL 开始爬取网站,并通过 SSE 流式传输发现的每一页。它负责发现过程——边界探索、去重、范围控制、链接提取、重试和礼貌请求——而每个页面的抓取任务则通过 HTTP 委托给 Yozh Scraper 处理。 整个系统仅需维护一套 Playwright 技术栈,且所有抓取器功能(代理、隐身模式、会话管理、提取规则)同样适用于被爬取的页面。
- 范围谓词 —
same-domain/subdomains/all/regex,包含“包含”与“排除”模式以及硬max_depth/max_pages上限。 - 指纹去重——先对 URL 进行规范化处理,然后进行哈希(SHA1)运算,这样,顺序不同的查询和带尾斜杠的变体都会被归为一次访问。
- 按域轮询机制——一个运行缓慢或体积庞大的主机不会导致其他主机无法获得工作进程。
- 自适应速率限制器——一个跨作业共享的、按域划分的全局令牌桶(带抖动);一个
429在冷却窗口期间将 RPS 减半。 - 会话健康状况——Crawlee式评分法:
401/403/429终止会话时,错误会提高其评分,成功则降低评分;当评分≥3或使用次数≥50时进行轮换。 - SSE 流式处理——每个任务都会发出
stats,page,page_error,以及终端done/cancelled事件。在检测结果出现时立即采取行动。
:8000 ,爬虫则运行在 :8001.快速开始
爬虫通过配置连接到根节点 docker-compose.yml 并随抓取程序一同启动:
docker compose up --build # scraper → http://localhost:8000 # crawler → http://localhost:8001
或者在不使用 Docker 的情况下在本地运行它——将 SCRAPER_URL 指向一个正在运行的 scraper:
cd yozh-crawler pip install -r requirements.txt cp .env.example .env # edit SCRAPER_URL if needed python -m uvicorn src.main:app --reload --port 8001
基本用法
提交一个包含 seed_url 以及一个 scope。该调用会立即返回一个 job_id ;随后该爬取任务将在后台通过多个工作线程执行。
curl -X POST http://localhost:8001/api/v1/crawl
-H "Content-Type: application/json"
-d '{
"seed_url": "https://example.com",
"scope": {"mode": "same-domain", "max_depth": 2, "max_pages": 50,
"per_domain_rps": 1.0, "per_domain_concurrency": 1},
"scrape_options": {"proxy_type": "none"},
"enable_scraping": false
}'
响应内容仅为作业句柄:
{ "job_id": "crawl_abc123" }
探索与收获—— enable_scraping 切换
同一个爬行引擎支持双向运行。通过一个布尔值即可在低成本的发现模式和全负载采集模式之间进行切换——且每种模式都使用各自的代理配置,因此不会产生交叉影响。
探索地图
轻量级抓取。抓取工具仍会渲染每一页(因此能发现由 JavaScript 生成的链接),但爬虫仅保留页面骨架——这非常适合用于网站地图、链接审核和差异检测。
- 保持
url·parent_url·depth·status·took_ms - 支持上传原始HTML、截图和提取的数据
- 使用更便宜的
crawl_proxy - 最小有效载荷,最快爬行速度
完整采集
每个访问过的页面都会连同其完整的 ScrapeResponse — 原始 HTML、可选的截图以及您提取规则中的所有字段。一次爬取,一次结构化处理。
- 保留每页的完整
ScrapeResponse每页 -
raw_html·screenshot· 已提取data - 用途
scrape_options.proxy_* - 通过
extract规则以获取干净的 JSON
crawl_proxy 仅在 enable_scraping=false; scrape_options.proxy_* 仅在以下情况下使用 true。如果 crawl_proxy 是 null,则 scrape_options 无论处于何种模式,都会使用代理。爬网范围
范围限定了爬行行为:规定了爬行可以访问的范围,以及对每个主机的访问强度上限。它是 scope 请求中的对象。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode | same-domain · subdomains · all · regex | same-domain | 哪些链接属于范围之内。 regex 与……匹配 include_patterns. |
include_patterns | string[] | [] | URL 必须匹配的正则表达式模式才能被加入队列(由 regex mode / 作为白名单)。 |
exclude_patterns | string[] | [] | 即使 URL 原本在作用域内,也会将其剔除的正则表达式模式。 |
max_depth | int | 3 | 从种子节点开始的最大链接深度(种子节点深度为 0)。 |
max_pages | int | 500 | 在抓取完成前,对访问的页面数设置硬性上限。 |
per_domain_rps | float | 1.0 | 每个域的令牌桶补充速率(请求/秒)。 |
per_domain_concurrency | int | 1 | 对单个域的最大并发请求数。 |
subdomains 的作用域定义较为简单。它将最后两个标签作为可注册域,因此会对“公共后缀列表”(Public Suffix List)中的主机产生过度匹配,例如 github.io 或 co.uk等主机时,会发生过度匹配。建议优先使用 same-domain 或 regex 。流式处理结果 (SSE)
每个任务都会暴露一个服务器发送事件流。通过读取该流,可以在发现页面的一瞬间就对其采取行动,而无需等待抓取完成。
curl -N http://localhost:8001/api/v1/crawl/crawl_abc123/events
该流会发出五种事件类型:
| 事件 | 触发时机 |
|---|---|
stats | 定期进度快照(已访问 / 排队中 / 失败 / 去重跳过 / 超出范围 / 重试)。 |
page | 每个访问过的URL对应一个。在 ScrapeResponse 在采集模式下,在发现模式下仅包含精简元数据。 |
page_error | 一个在所有重试后仍失败的 URL。 |
done | 终端 — 抓取已正常完成。 |
cancelled | Terminal — 抓取已取消。 |
一个 page 发现模式下的事件如下所示:
{
"event": "page",
"url": "https://example.com/docs/intro",
"parent_url": "https://example.com/docs",
"depth": 2,
"status_code": 200,
"took_ms": 812,
"scrape_response": null
}
进展与结果
更喜欢轮询吗?任务记录可随时查询——包括在爬取过程中,此时可查询到迄今为止发现的页面。 /results 是该任务端点的别名,为与抓取工具保持一致而保留。
curl http://localhost:8001/api/v1/crawl/crawl_abc123 curl http://localhost:8001/api/v1/crawl/crawl_abc123/results
{
"job_id": "crawl_abc123",
"status": "running",
"stats": {
"visited": 47, "queued": 12, "failed": 0,
"dedup_skipped": 9, "out_of_scope": 31, "retries_total": 2
},
"pages": [
{ "url": "https://example.com/", "parent_url": null,
"depth": 0, "status_code": 200, "took_ms": 640 }
]
}
status 穿过 queued → running → done (或 failed / cancelled).取消抓取
使用 DELETE。软取消是默认操作,也是安全的选择。
# soft — stop scheduling, let in-flight requests drain curl -X DELETE "http://localhost:8001/api/v1/crawl/crawl_abc123?hard=false" # hard — abort in-flight asyncio tasks immediately curl -X DELETE "http://localhost:8001/api/v1/crawl/crawl_abc123?hard=true"
?hard=true 会中断爬虫正在处理的请求,但抓取程序无法得知这一点——它那边的页面渲染已经完成。除非必须立即停止,否则请优先使用软取消。MCP
fastapi-mcp 已部署在 /mcp 作为 Streamable HTTP 端点。将 Point Claude 或 Cursor 指向该端点,相关工具便会自动显示:
healthcreate_crawlget_crawlget_crawl_resultscancel_crawl
"open-crawler": {
"type": "http",
"url": "http://localhost:8001/mcp"
}
stream_crawl_events 端点被有意排除在 MCP 之外——流式响应并不适用于请求/响应工具。配置参考
请求 — CrawlRequest
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
seed_url | 字符串 (URL) | 必填 | 爬取操作的起始 URL。 |
scope | CrawlScope | defaults | 边界与礼貌——参见“爬行范围”。 |
scrape_options | ScrapeOptions | defaults | 按每页内容原样转发给抓取程序(url 被注入)。 |
crawl_proxy | ScrapeOptions · null | null | 在发现模式下使用的廉价代理。仅 proxy_type / proxy_pool_id / proxy_geo 被读取。 |
enable_scraping | bool | false | 保持每页 ScrapeResponse 内容(true) 还是仅保留检索元数据(false). |
每页 — ScrapeOptions (已选中)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
proxy_type | none · mobile · res_static · res_rotating · dc_static · … | none | 代理池,页面通过该代理池获取路由。 |
device | desktop · mobile | desktop | 视口 / 用户代理配置文件。 |
render | bool | true | 使用浏览器渲染(JS生成的链接需要此操作)。 |
stealth | bool | true | 应用反检测加固措施。 |
wait_until | domcontentloaded · networkidle | domcontentloaded | 当页面被视为准备就绪时。 |
screenshot | bool | false | 截取屏幕截图(采集模式)。 |
extract | ExtractRule · null | null | CSS/XPath 字段规则 → 结构化 data 按页面。 |
session_id | 字符串 · null | null | 重用已认证的爬虫会话——请参阅“已认证的爬取”。 |
环境变量
| 变量 | 默认值 | 备注 |
|---|---|---|
SCRAPER_URL | http://web-scraper:8000 | 上游抓取器(在 Compose 网络中使用 Docker 服务名称)。 |
WORKERS | 2 | 并行爬取任务的数量。 |
QUEUE_MAXSIZE | 200 | 待处理任务队列的深度。 |
JOB_TIMEOUT_MS | 3_600_000 | 对一个爬取任务设置Wall-clock时间限制。 |
SCRAPER_JOB_TIMEOUT_MS | 120_000 | 每页抓取请求超时。 |
MAX_RETRIES | 3 | 发生临时故障时,每个请求的重试次数。 |
RETRY_HTTP_CODES | [408,429,500,502,503,504] | 会触发重试的状态码。 |
RETRY_BACKOFF_MAX | 30.0 | 指数退避的上界(秒)。 |
SESSION_MAX_ERROR_SCORE | 3.0 | 会话过期阈值。 |
SESSION_MAX_USAGE | 50 | 在 N 次使用后轮换会话。 |
SESSION_BLOCKED_CODES | [401,403,429] | 可立即结束会话的代码。 |
API 参考
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /api/v1/crawl | 创建一个作业。返回 {"job_id":"…"}. |
| GET | /api/v1/crawl/{id} | 任务记录——状态、统计数据、目前已浏览的页面。 |
| GET | /api/v1/crawl/{id}/results | 该工作记录的别名。 |
| GET | /api/v1/crawl/{id}/events | SSE 流 (stats/page/page_error/done/cancelled). |
| DELETE | /api/v1/crawl/{id}?hard=bool | 取消该作业(软取消或硬取消)。 |
| GET | /api/v1/health | 健康 + 抓取器可达性。 |
重要细节
robots.txt 不会进行查询。爬虫会遍历所有在范围内的 URL,无论 /robots.txt。对于您不拥有的目标网站,您有责任遵守机器人协议和网站条款——请使用 exclude_patterns 和速率限制,以保持礼貌。POST /sessions + POST /sessions/{id}/login),然后传递 scrape_options.session_id。抓取器会针对每个请求在客户端和服务器端传输 Cookie 及存储状态,因此每个被抓取的页面都能看到经过身份验证的状态。