1. 网页抓取工具的定位
模型的知识有截止时间,网页是实时信息的来源:最新文档、产品价格、竞品动态、新闻事件。网页抓取 MCP 工具让模型「去网上查」——这是搜索工具之外的另一种获取实时信息的方式,尤其适合「打开具体某个页面看内容」的场景。
1.1 与搜索的区别
# 搜索工具: 给 query,返回候选链接与摘要
# 抓取工具: 给 URL,返回页面内容
# 组合使用
# 1) 模型先搜索找到候选 URL
# 2) 再调抓取工具打开具体页面
# 3) 或直接给已知 URL(用户提供的链接)
# 抓取是"点开链接看正文",搜索是"找链接"
1.2 抓取工具的复杂度
| 层级 | 内容 | 难度 |
|---|---|---|
| 静态抓取 | 纯 HTML 页面、读取 | 低 |
| 内容提取 | 去掉导航/广告,取正文 | 中 |
| 结构化输出 | 按 schema 抽取字段 | 中 |
| JS 渲染 | 需要执行 JavaScript | 高 |
| 会话/登录 | 需要 Cookie/Token | 高 |
| 反爬对抗 | 验证码/风控 | 极高(应避免) |
2. 抓取流程设计
一个抓取工具不是「拿个 HTML 就完事」,而是一条流水线:取回 → 清理 → 提取 → 结构化 → 返回。
2.1 流水线架构
# 抓取流水线
# 1) fetch: 下载页面(HTTP 客户端)
# 2) parse: 解析 HTML(DOM 树)
# 3) clean: 移除脚本/样式/导航/广告
# 4) extract: 提取正文(readability 算法)
# 5) structure: 按 schema 抽取字段(可选)
# 6) return: 按 token 预算返回
# 每一步独立可测,出问题可定位
2.2 抓取工具的参数
# 抓取工具参数设计
register_tool(
name="fetch_webpage",
description="抓取网页正文内容,可指定提取 schema",
parameters={
"url": str, # 目标 URL
"selector": str | None, # CSS 选择器(可选)
"schema": dict | None, # 结构化提取模板(可选)
"max_chars": int, # 返回内容上限(默认 5000)
"render_js": bool, # 是否渲染 JS(默认 false)
},
handler=fetch_webpage,
)
2.3 超时与大小限制
# 1) 抓取超时: 10-20 秒(含下载与渲染)
# 2) 响应大小限制: 默认 2MB(超限截断)
# 3) 返回内容截断: max_chars 限制进上下文的文本
# 4) 重定向跟随: 最多 3 跳,防重定向环
# 抓取是外部依赖,超时与大小限制是基本功
3. 内容提取
网页正文提取是把 HTML 变成「干净的文本」。目标是去掉噪音,保留模型真正需要的正文。
3.1 提取方法
# 提取正文的常用方法
# 1) Readability 算法: 基于文本密度打分找正文区(Mozilla 算法)
# 2) CSS 选择器: 已知目标站点结构时直接选
# 3) 语义标签: article/main/section 优先
# 4) 机器学习提取: 泛化好但重
# 推荐: readability + 语义标签兜底
3.2 噪音清理
# 清理清单
# 1) script/style/noscript: 全删
# 2) nav/header/footer/aside: 通常删
# 3) 广告 iframe/嵌入: 删
# 4) 重复内容(分页导航、评论模板): 去重
# 5) 空白与排版: 归一化
# 清理得越干净,token 用越省、答案越准
3.3 提取结果格式化
# 返回给模型的格式
# 1) 标题 + URL + 抓取时间
# 2) 正文文本(按 max_chars 截断)
# 3) 可选: 阅读时间估计
# 4) 失败信息: 404/超时/被拒 → 清晰报错
# 别返回原始 HTML——模型要的是"页面讲了什么"
4. 结构化输出
有些场景模型需要的是「数据」而非「文章」:商品价格、招聘信息、比赛比分。用 schema 让抓取工具直接输出结构化 JSON,比模型从大段文本里找字段省 token 且更准。
4.1 schema 驱动的提取
{
"url": "https://example.com/products/123",
"schema": {
"title": {"type": "text", "selector": "h1.product-title"},
"price": {"type": "number", "selector": "span.price", "transform": "strip_currency"},
"stock": {"type": "text", "selector": "div.stock-status"},
"specs": {"type": "table", "selector": "table.specs"}
}
}
4.2 提取引擎实现
async def extract_by_schema(doc, schema):
result = {}
for field, conf in schema.items():
nodes = doc.cssselect(conf["selector"])
if not nodes:
result[field] = None
continue
text = nodes[0].text_content().strip()
if conf.get("transform") == "strip_currency":
text = re.sub(r"[¥$€,\s]", "", text)
if conf["type"] == "number":
result[field] = float(text) if text else None
else:
result[field] = text
return result
4.3 schema 的边界
# 1) schema 由用户/调用方给定,模型可生成
# 2) 提取失败字段返回 null(诚实)
# 3) 页面结构变化 → 提取失败 → 提示"页面结构不匹配"
# 4) 无法匹配时回退全文提取
# 结构化提取"猜错字段"时诚实失败,别编数据
5. robots 与合规
抓取不是「能抓到就能抓」。合规边界包括 robots.txt、站点条款、频率控制。这是工程问题也是法律问题。
5.1 robots.txt 处理
# 抓取前检查 robots.txt
# 1) 解析 robots.txt(Allow/Disallow 规则)
# 2) 明确 User-agent(MCP 抓取器标识自己)
# 3) 被 Disallow 的路径不抓
# 4) 抓取日志记录 robots 决策(合规可审计)
# 遵守 robots.txt 是"礼貌"与"合法"的基础
# robots 检查最小实现
from urllib.robotparser import RobotFileParser
def robots_allowed(url: str, user_agent: str = "McpFetcher/1.0") -> bool:
rp = RobotFileParser()
rp.set_url(robots_url_for(url))
rp.read()
return rp.can_fetch(user_agent, url)
5.2 站点条款与授权
# 1) 公开文档/新闻: 一般可抓,仍遵守 robots
# 2) 有 API 的站点: 优先用 API(官方通道)
# 3) 登录内容/私有数据: 无授权不抓
# 4) 明确 ToS 禁止抓取(如部分社交站): 尊重并跳过
# 授权边界: 有没有权利抓,比能不能抓到更重要
5.3 频率控制
# 1) 每域名最小间隔(如 1-2 秒)
# 2) 并发抓取上限(全局)
# 3) 尊重 Retry-After / 429 响应
# 4) 突发任务排队而不是并发轰炸
# 温和频率: 既能工作,又不给人当"攻击者"的机会
6. 反爬边界
反爬机制存在是为了保护站点。MCP 抓取工具应当避免与反爬系统对抗——那是猫鼠游戏,也是合规风险。
6.1 该做的与不该做的
# 不做
# 1) 绕过验证码(打码平台/识别)
# 2) 伪造指纹绕过风控
# 3) 高频请求打垮站点
# 4) 破解登录/付费墙
# 做
# 1) 遇到反爬返回"被拒,换其它方式"
# 2) 提供替代路径(官方 API/其他来源)
# 3) 放慢或停止
# 反爬被触发是"信号",不是"挑战"——退让是专业
6.2 风控应对
# 被 403/验证码拦下时
# 1) 明确返回"该站点启用了反爬保护"
# 2) 不自动重试轰炸(越试越黑)
# 3) 模型降级: 换搜索摘要 / 换来源 / 告知用户
# 4) 用户手动提供内容(粘贴正文)也是合法路径
# 诚实降级 > 偷偷绕墙
6.3 来源多样性与验证
# 多来源抓取的验证
# 1) 同一事实多来源交叉验证
# 2) 返回来源 URL 与抓取时间(可溯源)
# 3) 标注"信息来自网页,可能有误/过时"
# 模型引用网页信息要像引用资料一样严谨
7. 会话与 Cookie 管理
有些页面需要登录态或会话。会话管理有状态,也就有隔离与安全要求。
7.1 会话模型
# 1) 无状态默认: 每次抓取独立(不携带 Cookie)
# 2) 有状态可选: 用户提供 Cookie/Token(显式授权)
# 3) 会话隔离: 每个 MCP 会话独立 Cookie 容器
# 4) 凭证不进日志/审计
# 默认无状态,需要登录的页面走显式授权
7.2 Cookie 存储
# 1) Cookie 只存服务器内存(或加密存储)
# 2) 不跨会话共享用户 Cookie
# 3) 会话结束清除
# 4) 不把第三方站点凭证用于其它用途
# 会话凭证是高敏数据,按密钥级别对待
8. JavaScript 渲染页面
现代网页大量由 JS 渲染。要不要上无头浏览器,取决于页面是否真的需要。
8.1 何时需要渲染
# 需要 JS 渲染的迹象
# 1) 内容由前端框架(React/Vue)动态填充
# 2) 滚动加载(无限滚动列表)
# 3) 登录后才渲染
# 不需要的迹象
# 4) 内容在 HTML 源码里(SSR/静态)
# 判断: 先抓原始 HTML 看正文在不在,在就不用渲染
8.2 无头浏览器方案
# 用 Playwright 渲染(仅按需启用)
async def fetch_rendered(url: str, wait_ms: int = 1500):
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(url, timeout=15000)
await page.wait_for_timeout(wait_ms) # 等 JS 执行
html = await page.content()
await browser.close()
return extract_readable(html)
8.3 渲染的代价
# 1) 资源开销: 无头浏览器吃 CPU/内存,并发受限
# 2) 速度慢: 比纯 HTTP 慢 5-10 倍
# 3) 更易触发反爬: 浏览器指纹识别
# 策略: 默认关,需要时显式传 render_js=true
# 能用静态抓就别上浏览器——省资源也少惹风控
9. 生产实践
9.1 抓取代理与基础设施
# 1) 抓取代理(住宅/数据中心)用于地理与频率控制——但勿用于反爬对抗
# 2) 缓存抓取结果(同 URL 短 TTL,避免重复抓)
# 3) 抓取超时与重试(瞬时失败重试 1 次)
# 4) 请求头规范(声明 User-agent)
# 基础设施服务"正常抓取",不服务"对抗"
9.2 监控与审计
# 指标
# 1) 抓取成功/失败率(按域名)
# 2) 反爬触发次数(403/验证码)
# 3) 平均响应时间
# 4) robots 拒绝次数
# 审计
# 抓取日志: URL、时间、来源、robots 决策、结果
# 反爬触发上升 → 检查是否被当攻击工具
9.3 工具设计检查清单
# ☐ 流水线: fetch → parse → clean → extract → structure
# ☐ robots.txt 检查 + 日志
# ☐ 频率控制(域名级间隔 + 全局并发)
# ☐ 超时 + 大小 + 重定向限制
# ☐ 结构化 schema 提取
# ☐ 默认无状态,登录走显式授权
# ☐ 反爬触发 → 诚实降级而非对抗
# ☐ 缓存 + 监控 + 审计
10. 常见陷阱
- 不查 robots:抓了不该抓的路径,合规风险——抓前解析 robots.txt。
- 对抗反爬:绕过验证码、伪造指纹——猫鼠游戏且违法,退让并降级。
- 原始 HTML 进上下文:几 MB 源码 token 爆炸——提取正文再返回。
- 无频率控制:并发轰炸触发封禁——域名级间隔 + 全局并发上限。
- 登录态乱共享:用户的 Cookie 跨会话泄漏——会话隔离 + 内存存储。
- 动辄开无头浏览器:静态页也渲染,慢且惹风控——按需启用。
- 无缓存:同一 URL 反复抓——短 TTL 缓存。
- JS 渲染后超时:页面加载不完——等待策略 + 超时兜底。
11. 总结
网页抓取 MCP 工具的工程核心是流水线化提取、结构化输出、合规克制:用「取回 → 清理 → 提取 → 结构化」的流水线把网页变成干净的正文与 JSON,用 robots 检查、频率控制与来源验证守住合规底线,用静态优先、JS 渲染按需的决策控制成本,用诚实降级应对反爬而不是对抗。抓取工具让模型能「看实时网页」,但它的价值建立在尊重站点规则与来源可溯源之上——会抓是能力,知道什么能抓、什么时候该停,才是专业。一句话:抓取要快、要准,更要站得住脚。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。