关于 website-scraping
将网站页面转换为结构化 JSONL 记录的工作流技能,适用于数据采集任务。它帮助开发者选择合适的提取方式、构建稳健的小型抓取器,并对照已知样例验证结果。
主要功能
- 01选择轻量的数据提取方式
- 02规划记录字段与输出格式
- 03构建逐条处理的稳健提取器
- 04按需应对反爬措施
- 05保存原始数据并验证输出
适用场景
- 01从 URL 收集商品或列表数据
- 02将网页表格提取为结构化记录
- 03编写可重复运行的网站采集脚本
MCP 相关
该技能指出,对于一次性、低频任务,可用的抓取 MCP 服务可能是最轻量的选择。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ moonlight-lupin/ agent-skills --skill website-scraping安装后在 Claude Code 中输入/website-scraping确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignoreREADME.mdevals/evals.jsonrouting-fixtures.jsonexamples/agentic-browsing-quotes-demo/explore.pyplan.mdREADME.mdexamples/agentic-browsing-quotes-demo/final_runs/run_1/final_script_log.txtfinal_script.pyrecords.jsonlreferences/agentic-browsing.mdanti-bot.mdextraction.mdnamed-div-extraction.mdplaywright.mdrecon.mdscrapling.mdscripts/recon.py详细介绍
网站抓取
将网站内容转化为结构化数据的工作流。优先选择能满足需求的最轻量工具:静态 HTML → 页面内 JSON → 逆向分析 API → 浏览器自动化,按此顺序进行。跳过侦察,直接使用 Playwright,是最常见的耗时原因。
适用场景
本技能适用于任何符合“给定一个或多个 URL,生成结构化记录”描述的任务。例如:
- 竞争对手价格监控(酒店、航班、零售、租赁、学生公寓、SaaS)
- 电商产品目录提取
- 房地产或招聘信息采集
- 评论、评分、可用性日历
- 一次性“把这个表格导入电子表格”的任务
不在适用范围内:数据库持久化、定时调度、仪表板、按固定架构进行下游规范化。本技能生成 JSONL——调用方如何处理这些数据,由调用方自行负责。
工作流
按顺序执行以下步骤。大多数失败都源于跳过第 1 步。
1. 编写代码前先明确目标
打开编辑器之前,先以文字明确以下问题的答案:
- 你要哪些记录? 每行对应什么?每个产品?每个(产品 × 变体 × 日期)?每个(酒店 × 房型 × 入住日期)?要明确——许多网站迫使你在“每张商品卡片一行”和“每个价格变体一行”之间选择,而这个选择会影响整个抓取流程。
- 每条记录包含哪些字段? 区分必需字段和可选字段。标记源站可能不会在每条记录中都发布的字段(字段有时缺失很正常,不是错误)。
- 输入形式:单个 URL?URL 列表?需要先执行的搜索查询?要从中爬取数据的种子列表页?
- 输出形式:本技能建议使用 JSONL(每行一个 JSON 对象,键由调用方定义),并附带一个清单文件。请参阅下文的“输出约定”。
- 规模和频率:有多少条记录,多久执行一次?这个答案决定后续所有成本决策。一次性采集 100 个页面和每天刷新 10,000 个 SKU 是两个不同的项目:前者只需礼貌地使用
time.sleep;后者则必须证明代理/托管服务在经济上合理(请参阅第 4 步,第 5 级及以上)。现在就明确数据量和重复频率,而不是等到构建出一个无法满足需求的单次抓取器之后再考虑。 - 什么是“真实依据”? 选定一个具体项目,在浏览器中从头到尾检查,以验证抓取器结果是否一致。
如果用户没有明确回答以上任何问题,请先询问。花 30 秒澄清,可省去一小时错误格式的数据提取。
2. 勘察网站——选择最轻量的工具
在编写下面的决策树之前,先进行两项成本很低的检查。任意一项都可能让整个爬虫变得多余。
2.0a — 数据是否只需调用一个工具就能获取? 你可能正运行在一个已提供抓取/爬取/SERP 功能的 Agent 运行时中——例如 Hermes 的 web_search + web_extract 工具、内置搜索,或爬取 MCP 服务(Bright Data、Firecrawl 等)。对于一次性、低频任务,调用现成工具是最轻量的方案——比编写任何代码都轻量。打开编辑器前,先检查有哪些可用工具。(对于可重复执行、纳入版本控制或高频的任务,仍应编写脚本——这样才能重新运行、比较差异并交接。)
2.0b — 这是一个难以攻克的巨型平台吗? 少数网站——Amazon、LinkedIn、Instagram、TikTok、Facebook、YouTube、Zillow、Google Maps、Crunchbase 等——会投入大量资源来阻止自制爬虫,并且频繁更改内部实现。对于这些网站,手工编写爬虫通常得不偿失:它能运行一周,然后就会失效。这里最轻量的工具可能根本不是 urllib,而是商业结构化数据产品(例如 Bright Data 的数据集/Web-Scraper API、Apify actors 等),它们已经在为相应平台维护数据提取功能。请向用户说明这一权衡(“我可以手工编写爬虫,但对于 $SITE,维护完善的数据 API 会更可靠,而且维护成本可能更低——由你决定”),不要默默地埋头折腾一个脆弱的自定义爬虫。如果用户仍想自行实现,就继续——但请做好在第 4 步使用反机器人阶梯方案的准备。
如果这两个捷径都不适用,就按常规进行勘察。在真实浏览器中打开目标 URL,并打开 DevTools。从上到下遍历此决策树;找到第一个匹配项就停止。停止得越早,爬虫就越简单、越稳健。
Is the data you need visible in "View Source" (right-click → View Page Source)?
├─ YES → Static HTML. Use urllib / requests + an HTML parser. Done.
│
└─ NO → Is there a <script type="application/ld+json"> block with what you need?
│
├─ YES → JSON-LD. Universal schema (schema.org), present on most modern
│ sites for SEO. Often has product, price, address, rating, etc.
│ Parse the JSON block directly. No JS execution needed.
│
└─ NO → Is there a JSON blob in a <script id="__NEXT_DATA__">, __NUXT__,
drupal-settings-json, window.__INITIAL_STATE__, or similar?
│
├─ YES → Framework JSON-in-page. Pull the blob, json.loads it, navigate
│ to the fields you want. Still no browser needed.
│
└─ NO → Open DevTools Network tab. Reload. Filter to XHR/Fetch.
Does a clear JSON endpoint return the data?
│
├─ YES → Reverse the API. Copy as cURL, port to Python.
│ Often the cleanest path — same data the site uses, no scraping
│ of HTML at all. Watch for auth tokens / CSRF / rate limits.
│
└─ NO → It's client-rendered. Browser automation required.
Use Playwright. See references/playwright.md.
硬性规定:只有在前面的方法确实行不通时才使用 Playwright。Playwright 比 urllib 慢 10–50 倍、占用内存多 10 倍,也脆弱 10 倍。网站每隔几个月就会更改 HTML 结构,但通常不会那么频繁地更改 JSON-LD 或其底层 API。
如需使用自动化勘察助手访问 URL 并报告哪些路径看起来可行,请参阅 scripts/recon.py。每当处理新网站时,首先运行它。
3. 构建尽可能精简的提取器
从一条记录开始,而不是整个目录。先针对一个已知目标正确提取字段,再推广到列表。原因有两个:
- 在一条记录上更容易发现字段提取错误,不会像处理 200 条记录时那样被掩盖。
- 你需要迭代 5-10 次才能把解析器调正确——每次都针对整个网站操作,会耗尽速率限制额度,还会让网站的 WAF 熟悉你的特征。
内层循环的代码结构:
def extract_one(html_or_json) -> dict | None:
# one item -> one dict; None if this item is unparseable.
...
def extract_many(items_iter) -> list[dict]:
out, seen = [], set()
for item in items_iter:
try:
rec = extract_one(item)
except Exception as e:
log.warning("extract failed for %s: %s", _label(item), e)
continue
if rec is None:
continue
key = rec.get("id") or rec.get("url") or _hash(rec)
if key in seen:
continue
seen.add(key)
out.append(rec)
return out
这种结构强制落实三件事,以下是它们各自的重要性:
- 每条记录单独使用
try/except,不要对整个批次使用。 一条格式错误的列表项不应该导致其他 199 条也丢失。记录足够的上下文来定位该项(URL 或稳定 ID),然后继续处理。静默吞掉异常更糟——失败率会变得不可见。 - 根据稳定的规范 ID 去重,而不是根据记录是否相等。 如果网站提供数字 ID、SKU 或 URL slug,就用它。不要用
hash(record_dict)去重,因为真实记录会有细微变化(空白字符变动、价格小幅波动),导致无法识别重复项。 None表示“有意跳过此项”,异常则表示“已尝试但失败”。这是不同的信号,应分别记录。
并发。 上面的循环是顺序执行的,这也是正确的默认做法——对网站最友好,也最容易调试。只有当顺序执行确实太慢时才考虑并行,并限制并发量:
- 约 20 个 URL 以内:保持顺序执行,在请求之间使用较短的
time.sleep。不值得增加复杂度。 - 约 20–200 个 URL:使用有界并发——通过
asyncio.Semaphore(N)(或线程池 N)控制并发量,并将 N 设小一些(4–8)。绝不要一次发出所有请求;对 200 个 URL 使用不受限制的asyncio.gather,相当于自找拒绝服务,会导致你的 IP 被封禁。 - 数百个以上:你已经明确进入反机器人领域(步骤 4)。信号量大小取决于网站能够容忍的并发量,而不是你的 CPU——从较低值开始,只有在成功率保持稳定时才逐步提高。
并发上限由网站的速率限制(步骤 4,Tier 4)决定,而不是由你的硬件决定。不确定时,慢一些但完整胜过更快却被封禁。
4. 按比例应对反爬防护
现代网站会逐步加强对抓取行为的防护。请根据实际观察到的情况采取相应措施,而不是根据担忧过度防范。
| 观察到的情况 | 可能原因 | 解决方法 |
|---|---|---|
使用默认 UA 的 urllib.request 返回 403 / 429 | Bot UA 过滤器 | 在请求头中设置真实的浏览器 UA。通常单靠这一点就足够了。 |
| 响应正文为空,但浏览器显示了完整内容 | 客户端渲染 | 需要使用浏览器。使用默认设置的 Playwright 即可。 |
| 返回 200 和看起来真实的 HTML,但缺少所需字段(或每次运行的值都不同) | 渲染不完整——捕获前 DOM 尚未稳定 | 捕获前等待特定选择器出现;然后使用 validate 谓词重试,重新获取内容,直到出现标记。参见 references/anti-bot.md。 |
| HTML 正文标题显示“Just a moment...”或“Performing security verification” | Cloudflare 验证 | 添加 playwright-stealth。如果仍然失败,请为每个页面新建一个 browser.new_context()——Cloudflare 会标记同一上下文中的重复访问。参见 references/anti-bot.md。 |
| 第一次成功,第 5 次请求失败 | 按 IP 限速 | 在请求之间插入 time.sleep(1-3)。如果仍然失败,该网站需要代理轮换——请与用户确认抓取量是否合理。 |
| 每次访问都会出现验证码(可见),即使使用真实浏览器也一样 | 激进的 WAF | 使用隐身模式和新上下文。如果仍然失败,你已触及“是否还能抓取该网站”的边界。可选方案:使用托管解封 API(见下文)、住宅 IP 代理、undetected-playwright、在有界面浏览器中手动解验证码,或接受范围缩减(例如,仅抓取未登录落地页上显示的内容)。记录限制,然后继续。 |
| 需要登录 | 内容受身份验证保护 | 不在此技能的范围内。身份验证需要针对每个网站单独处理,并与用户另行商定(凭据、ToS、2FA)。请将其列为阻碍因素。 |
第一天不要就使用住宅代理。 它们确实需要花钱,也表明你的操作规模或强度达到了网站正试图阻止的程度。请确保使用场景足以证明其合理性。
托管解封服务的退路。 在“自行配置住宅代理”和“放弃”之间,还有一整类值得了解的托管服务:Bright Data 的 Web Unlocker、Zyte API、ScraperAPI 等服务会接收 URL 并返回干净的 HTML——代理轮换、TLS 指纹识别和 CAPTCHA 求解都由服务端处理。当 DIY 方案在值得投入的网站上遇到瓶颈时,通常正确的做法是只替换获取器,其余部分保持不变:你的 extract_one、去重逻辑、JSONL 写入器和清单都逐字节保持一致,只需更改 HTML 的获取方式。在清单中记录这项替换("tool": "managed-unblocker" 或具体供应商名称),让使用方了解数据来源。这样可以保持技能与供应商无关——托管服务只是相同获取边界背后的实现细节,不需要重写。参见 references/anti-bot.md,了解它在升级处理阶梯中的位置,以及该层级特有的两项规范:通过验证并重新获取钩子,对不完整的渲染内容进行重试(200 响应的内容虽通过了拦截页检查,却缺少目标字段);以及验证货币/区域设置,因为解封服务的出口国家会改变返回的数据,而不仅仅是访问权限。
参考:references/anti-bot.md 包含完整的升级处理阶梯,以及应对 Cloudflare/Turnstile 的具体模式。
5. 保存原始载荷,而不只是解析后的输出
对于抓取的每个条目,都要将原始 HTML / JSON 响应与解析后的记录一并保存。这是你能构建的最重要的可靠性保障。原因如下:
- 重放:解析器出错时(这是迟早的事),你可以从已保存的原始载荷重新解析,而不必重新获取。等你发现问题时,网站可能已宕机、正在限制你的请求,或已经发生变化。
- 取证:网站发生变化导致解析器失效时,对比正常工作的原始载荷与出错的原始载荷,就能看出变化之处。
- 成本低:HTML 很容易压缩。1MB 的页面经 gzip 压缩后约为 100KB。
约定:将原始载荷保存到输出 JSONL 文件旁的 raw/<id_or_hash>.html 或 .json。在解析后的记录中引用文件名,方便日后查找。
6. 对照真实数据进行验证
宣布完成之前:
- 拦截页检查(获取时就做,不要等到最后)。
200 OK并不意味着你获取到了真实内容。解析前,断言载荷非空,且不含拦截页特征,例如"Just a moment"、"Performing security verification"、"Access denied"、"unusual traffic"、Cloudflare/Turnstile iframe,或对页面而言短得可疑的正文。静默写入 200 条挑战页面记录的爬虫看起来似乎运行成功(status: "ok",200 行),但实际上没有任何可用数据。出现问题时要明确报错——将这些情况计入清单中的错误,不要把它们伪装成记录。这是最常见的静默数据损坏问题。
0a. 清理内容以供 LLM 使用(如果抓取的内容将输入 LLM)。 隐藏元素(<script>、<style>、<template>、隐藏的 div)可能包含提示词注入载荷,在抓取的文本进入 LLM 提示词后执行。保存内容或将其传递给任何模型前,先移除这些元素。如果已安装 Scrapling,请使用 scrapling extract get <url> output.md --ai-targeted——它会自动处理。否则,应用 references/scrapling.md(概念 1)中的手动清理函数。只要抓取的内容会进入 LLM 上下文窗口,就必须执行此步骤。
0b. 获取时断言货币/区域设置约定(如果数据存在本地化)。 对于任何有价格的数据——旅行、零售、市场平台——出口国家/地区(代理/解封服务的出口节点,或代码运行所在的位置)可能会改变页面的货币、语言和税务处理方式,因此 £1,200 可能会悄悄变成 $1,200 或 ¥1,200。读取页面实际呈现的货币,并与请求的货币进行比较;如果不匹配,就拒绝记录,而不要记录换算后的金额。悄悄换算后的价格看起来没问题,实际却是错的——比缺少数据更糟。与占位值(近似为 0,应重试)不同,这种情况是确定的:跳过该条目并标记出来,不要重新获取。详见 references/anti-bot.md。
- 冒烟测试:使用步骤 1 中唯一已知有效的条目运行爬虫。逐一对照浏览器中显示的内容,检查生成记录的每个字段。
- 抽样 N 条:从完整运行结果中随机抽取 5-10 条记录,并在浏览器中逐条核验。留意:列错位问题(每个字段都偏移一列)、单位混淆(£ 与 $、每周与每月)、日期格式错误(MM/DD 与 DD/MM),以及静默为空的字段。
- 数量检查:如果网站公布了总数(“该城市有 234 处房产”),请检查你的输出是否大致包含相同数量的记录(允许因过滤而少于网站列出的数量)。
- 重复运行的一致性:在几分钟内对同一 URL 抓取两次。如果记录或字段值随机变化,说明浏览器驱动的提取存在竞争条件——参见
references/extraction.md中的双重渲染陷阱。
7. 以 JSONL + manifest 格式写入输出
该技能推荐采用这种输出约定,因为这是最简单且能适应架构演进的格式:
output/
├── records.jsonl # one JSON object per line, caller-defined keys
├── manifest.json # run metadata: timestamp, source URLs, status, counts
└── raw/ # raw payloads, named by stable id
├── abc123.html
└── def456.json
JSONL:每行都是 json.dumps(record) + "\n"。没有标题行。不同运行之间可以自由添加字段,而不会影响旧版使用方。可流式读取。
Manifest 结构(即使运行部分失败,也要写入):
{
"scrape_started_at": "2026-05-24T10:34:00Z",
"scrape_finished_at": "2026-05-24T10:36:12Z",
"source_urls": ["https://example.com/listings"],
"status": "ok", // "ok" | "partial" | "failed"
"records_written": 187,
"items_attempted": 200,
"items_skipped": 13,
"errors": [
{"url": "...", "reason": "..."}
],
"user_agent": "Mozilla/5.0 ...",
"tool": "urllib" | "playwright" | "playwright+stealth"
}
manifest 让调用方(以及三个月后的你)知道数据是否可信。status: "partial" 且 items_skipped: 13 的运行结果,与 status: "ok" 的使用方式不同。
参考资料
深入资料位于 references/ 中——按需加载,无需预先阅读:
references/recon.md— 完整的侦察决策树,并通过具体示例说明如何识别各种模式(JSON-LD 结构、Next.js / Nuxt / Drupal 数据块位置、常见框架特征、隐藏的 REST API)。还介绍了侦察前的两种捷径:复用现有的运行时抓取工具;识别有维护的商业数据产品比自己动手更合适的难以处理的大型平台。references/playwright.md— 浏览器自动化模式:何时使用、domcontentloaded与networkidle(以及后者通常为何不合适)、等待选择器、隐身、无头与有头模式,以及为 SPA 注入页面端 JS 抓取器。references/anti-bot.md— 应对 Cloudflare、Turnstile、速率限制和基于 IP 的封锁的升级方案。涵盖具体模式(每个请求使用全新上下文、隐身库设置、托管式解封服务的退出方案以及仅替换抓取器的方法、何时放弃)。references/extraction.md— 提取 JSON-LD、查找 Next.js/Nuxt/Drupal 等框架中的页面内 JSON 数据块、处理双重渲染的 DOM、点击后轮询值变化、稳健去重、解析常见字段格式(尺寸范围、价格范围、日期、邮政编码、地址)。references/agentic-browsing.md— 适用于阻碍来自多步导航而非解析的特定场景(登录流程、筛选向导、日历选择器)。介绍 Microsoft 的 Webwright Agentic 浏览器框架:何时值得使用、何时过度设计、安装/CLI,以及“用一次来完善脚本,然后确定性地运行该脚本”的模式。不适用于批量提取——其自身文档也如此说明。references/named-div-extraction.md— 从<div id="...">容器中提取服务器渲染的文本(ASP.NET/旧版网站中很常见)。涵盖正则表达式与 BeautifulSoup 的选择,以及浏览器回退方案、嵌套 div 的陷阱、多语言变体处理,以及批量 urllib + 浏览器回退的混合模式。references/scrapling.md— 当网站结构发生变化导致 CSS 选择器失效(自适应元素定位)、抓取内容将输入大模型(提示词注入清理),或需要在编写完整抓取器前通过 CLI 快速测试时加载。
辅助脚本
scripts/recon.py—python recon.py <url>通过普通 HTTP 获取页面;必要时再使用 Playwright,并报告哪些提取策略看起来可行(静态 HTML 命中、JSON-LD 数据块、框架数据块、反机器人特征)。编写任何内容前,都要对每个新网站运行此脚本。
应避免的反模式
看起来是好主意,实际代价却大于收益的做法:
- 一开始就用 Playwright,“以防万一”。 先侦察。如果 urllib 能用,那么每一分钟调试 Playwright 都是在浪费时间。
- 全局使用激进的 try/except。 这会捕获你自己的错误,并造成静默的数据损坏。按条目处理并记录失败,才是合适的粒度。
- 将因租户而异的值硬编码。 URL 中的内部 ID(例如
academicYear=82、region_id=14)、CSRF 令牌、签名查询参数——应从页面读取,而不是硬编码。网站会轮换这些值,你可能会在毫无明显原因的情况下发现“爬虫返回 0 行”。 - 依赖看起来像自动生成的 CSS 类名(例如
.css-1a2b3c4)。它们是构建哈希产物,每次部署都会变化。优先使用语义化选择器([data-product-id]、[itemprop=price]、<h1>、结构化数据、ARIA 角色)。 - 用
time.sleep(N)代替等待选择器。 固定等待是造成不稳定的最大原因。始终等待特定的 DOM 条件。 - 丢弃原始载荷。 为调试解析错误而重新获取数据既慢、又会消耗速率限制额度,而且通常不可能做到(网站可能已经发生变化)。保留原始载荷的成本很低。
- 要求每个条目都包含所有字段。 有些网站的部分条目确实会缺少数据。在第 1 步中将字段标记为可选,并在字段缺失时避免程序崩溃。
关于道德与服务条款的说明
这项技能帮助你掌握抓取的操作方法,并不回答你是否应该抓取。在有意义的规模上运行爬虫之前,调用方有责任:
- 查看网站的
robots.txt和服务条款。 - 考虑是否有能完成同样任务的公共 API。
- 将流量控制在合理水平(相当于单个用户的浏览量,而不是 DDoS)。
- 没有合法依据时,不要抓取个人数据。
- 未经明确授权,不要抓取登录后才能访问的内容。
如果用户的需求触及上述任何界限,请将其标记为风险。“我们可以这样做,但请先查看服务条款”是合理的回应。
评测
evals/ 下有两组评测数据:
evals.json— 针对稳定练习站点(静态 HTML 和由 JS 渲染的页面)运行的端到端抓取任务,以及预期的提取策略。routing-fixtures.json— 轻量级契约测试数据:示例请求 → 预期路由(包括请求应改由entity-research或people-enrichment处理的情况)、必需的输出字段和禁止模式。
路由测试数据是规范,不会针对实时模型运行;
tests/test_routing_fixtures.py 用于验证其格式始终正确。