关于 extract
extract 是一项用于从网站提取结构化数据的技能,面向需要可重复采集流程的用户。每次任务会生成可独立运行的 Playwright 脚本和 JSON、CSV 等数据文件,并根据页面机制处理加载、滚动或分页。
主要功能
- 01生成独立运行的 Playwright 脚本
- 02导出 JSON、CSV 等数据
- 03处理延迟加载与页面水合
- 04处理虚拟列表和无限滚动
- 05支持分页采集与结果校验
适用场景
- 01采集商品价格或评分
- 02提取搜索结果中的作者和标题
- 03收集职位列表或表格数据
- 04抓取多页或滚动加载的数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ actionbook/ actionbook --skill extract安装后在 Claude Code 中输入/extract确认已加载。
详细介绍
何时使用此技能
当用户希望从网站获取数据时启用:
- “提取此页面上的所有产品价格”
- “抓取……中的结果表格”
- “从 arXiv 搜索结果中获取作者和标题列表”
- “收集此页面上的所有职位信息”
- “获取此仪表盘表格中的数据”
- “收集……中的评论评分”
- “下载……中的所有链接/图片/卡片”
交付物始终包含两项内容:
- 可执行的 Playwright 脚本 — 独立的
.cjs文件,无需在运行时使用 Actionbook 即可重现数据提取。 - 提取的数据 — 写入磁盘的 JSON(默认格式)、CSV 或用户指定的格式。
决策策略
将 Actionbook 用作有条件的加速工具,而非必经步骤。目标是以最短路径找到可靠的选择器。
User request
│
├─► actionbook search "<site> <intent>"
│ ├─ Results with Health Score ≥ 70% ──► actionbook get "<ID>" ──► use selectors
│ └─ No results / low score ──► Fallback
│
└─► Fallback: actionbook browser open <url>
├─ actionbook browser snapshot (accessibility tree → find selectors)
├─ actionbook browser screenshot (visual confirmation)
└─ manual selector discovery via DOM inspection
选择器来源的优先顺序:
| 优先级 | 来源 | 适用情况 |
|---|---|---|
| 1 | actionbook get | 网站已建立索引,健康度评分 ≥ 70% |
| 2 | actionbook browser snapshot | 网站未建立索引或选择器已过时 |
| 3 | 通过截图 + 快照检查 DOM | 复杂 SPA/动态内容 |
**不可妥协的规则:**如果 search + get 已为所需字段提供可用的选择器,就从 get 选择器开始,默认不要直接转向完整回退方案(snapshot/screenshot)。例外:当运行时行为可能影响脚本正确性时,允许进行轻量机制探测(用于检查 hydration/虚拟化/分页)。只有当探测/样本验证显示选择器存在缺口或不稳定时,才升级到 snapshot/screenshot。
根据机制调整脚本策略
网站会使用一些会导致朴素抓取失效的模式。生成的 Playwright 脚本必须考虑以下情况:
流式传输/SSR/RSC hydration
页面可能先渲染外壳,然后再流式传输或 hydration 内容。
// Wait for hydration to complete — not just DOMContentLoaded
await page.waitForSelector('[data-item]', { state: 'attached' });
await page.waitForFunction(() => {
const items = document.querySelectorAll('[data-item]');
return items.length > 0 && !document.querySelector('[data-pending]');
});
**检测线索:**React 根节点带有 data-reactroot、Next.js __NEXT_DATA__,或空容器在 JS 运行后填充。如果 actionbook browser text "<selector>" 返回空内容,但截图显示了内容,则 hydration 尚未完成。
虚拟化列表/虚拟 DOM
DOM 中只存在可见行。滚动会渲染新行并销毁旧行。
// Scroll-and-collect loop for virtualized lists (scroll container aware)
const allItems = [];
const maxScrolls = 50;
let scrolls = 0;
const container = await page.$('<scroll-container-selector>');
if (!container) throw new Error('Scroll container not found');
let previousTop = await container.evaluate(el => el.scrollTop);
while (scrolls < maxScrolls) {
const items = await page.$$eval('[data-row]', rows =>
rows.map(r => ({ text: r.textContent.trim() }))
);
for (const item of items) {
if (!allItems.find(i => i.text === item.text)) allItems.push(item);
}
await container.evaluate(el => el.scrollBy(0, 600));
await page.waitForTimeout(300);
const currentTop = await container.evaluate(el => el.scrollTop);
if (currentTop === previousTop) break;
previousTop = currentTop;
scrolls += 1;
}
**检测线索:**容器具有固定高度并设置了 overflow: auto/scroll;DOM 中的行数远少于标明的总数;行具有 transform: translateY(...) 或 position: absolute; top: ...px。
无限滚动/延迟加载
用户滚动到页面底部附近时会追加新内容。
// Scroll to bottom until no new content loads (with no-growth tolerance)
let itemCount = 0;
let noGrowthStreak = 0;
const maxScrolls = 80;
let scrolls = 0;
while (scrolls < maxScrolls && noGrowthStreak < 3) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1200);
const newCount = await page.$$eval('.item', els => els.length);
if (newCount > itemCount) {
itemCount = newCount;
noGrowthStreak = 0;
} else {
noGrowthStreak += 1;
}
scrolls += 1;
}
**检测线索:**页面 JS 中存在 Intersection Observer、“加载更多”按钮、底部的哨兵元素,或滚动时触发网络请求。
分页
多页结果通过“下一页”按钮或编号页面展示。
// Click-through pagination (navigation-aware, SPA-safe)
const allData = [];
const maxPages = 50;
let pageIndex = 0;
while (pageIndex < maxPages) {
const pageData = await page.$$eval('.result-item', items =>
items.map(el => ({ title: el.querySelector('h3')?.textContent?.trim() }))
);
allData.push(...pageData);
const nextBtn = await page.$('a.next-page:not([disabled])');
if (!nextBtn) break;
const previousUrl = page.url();
const previousFirstItem = await page
.$eval('.result-item', el => el.textContent?.trim() || '')
.catch(() => '');
await nextBtn.click();
// Post-click detection only: advance must be caused by this click
const advanced = await Promise.any([
page
.waitForURL(url => url.toString() !== previousUrl, { timeout: 5000 })
.then(() => true),
page
.waitForFunction(
prev => {
const first = document.querySelector('.result-item');
return !!first && (first.textContent || '').trim() !== prev;
},
previousFirstItem,
{ timeout: 5000 }
)
.then(() => true),
]).catch(() => false);
if (!advanced) break;
await page.waitForLoadState('networkidle').catch(() => {});
pageIndex += 1;
}
执行流程
第 1 步:了解目标
从用户请求中确定:
- URL — 要提取内容的页面
- 数据结构 — 所需字段/列
- 范围 — 单页、分页、无限滚动或多页抓取
- 输出格式 — JSON(默认格式)、CSV 或其他格式
第 2 步:获取选择器并选择执行路径
# Try Actionbook index first
actionbook search "<site> <data-description>" --domain <domain>
# If good results (health ≥ 70%), get full selectors
actionbook get "<ID>"
严格按照以下路径执行:
-
**路径 A(
get质量良好时的默认路径):**所需字段均由get选择器覆盖,且质量可接受。- 从
get选择器开始,尽快起草脚本。 - 在确定脚本策略前,可以进行轻量机制探测(
browser text、快速滚动检查)。 - 除非探测/样本验证显示不匹配,否则在首次起草前不要运行完整回退方案(
snapshot/screenshot)。 - 字段映射默认使用
get选择器,并将来源标记为actionbook_get。
- 从
-
**路径 B(部分覆盖/不稳定):**存在
get,但缺少所需字段、选择器匹配到 0 个元素,或验证失败。- 只针对失败的字段/步骤运行定向回退。
-
**路径 C(无可用覆盖):**search/get 没有可用结果。
- 运行完整回退发现流程。
步骤 3:探测页面机制,仅在必要时使用备用方案
路径 A 的机制检测时机:
- 在最终脚本草稿完成之前或样本验证期间运行最小化探测。
- 运行任何探测命令之前,确保已打开正确的页面上下文:
actionbook browser open "<url>"(如果当前标签页上下文未知或已过期)
- 如果探测或样本运行结果表明存在不匹配(缺少行、选择器不稳定、分页行为不正确),则升级到路径 B,进行有针对性的备用处理。
按路径进行备用发现:
路径 B:有针对性的备用处理(仅处理失败的字段/步骤):
actionbook browser open "<url>" # if not already open
actionbook browser snapshot # focus on failed field/container mapping
# actionbook browser screenshot # optional visual confirmation for failed area
路径 C:完整备用处理(没有可用覆盖):
actionbook browser open "<url>"
actionbook browser snapshot
actionbook browser screenshot
机制探测(脚本策略需要确认时运行):
# Hydration / streaming check
actionbook browser text "<container-selector>"
# Infinite scroll quick signal (explicit before/after decision)
actionbook browser eval "document.querySelectorAll('<item-selector>').length" # before
actionbook browser click "<scroll-container-selector-or-body>" # focus scroll context
actionbook browser eval "const c=document.querySelector('<scroll-container-selector>') || document.scrollingElement; c.scrollBy(0, c.clientHeight || window.innerHeight);"
actionbook browser eval "document.querySelectorAll('<item-selector>').length" # after
# If count increases, treat page as lazy-load/infinite-scroll.
备用方案触发条件:
actionbook get无法映射所有必需字段。actionbook get选择器在样本运行中返回空值或不稳定的值。- 运行时行为与预期机制冲突(例如虚拟化容器、延迟 hydration)。
步骤 4:生成 Playwright 脚本
编写一个独立的 Playwright 脚本(extract_<domain>_<slug>.cjs),该脚本需要:
- 导航到目标 URL。
- 等待正确的就绪信号(而不只是
load——参见上文机制说明)。 - 处理检测到的机制(虚拟滚动、分页等)。
- 将数据提取为结构化对象。
- 将输出写入磁盘(
JSON.stringify/ CSV)。 - 关闭浏览器。
- 设置防护措施(
maxPages、maxScrolls、超时预算),避免无限循环。
脚本模板:
// extract_<domain>_<slug>.cjs
// Generated by Actionbook extract skill
// Usage: node extract_<domain>_<slug>.cjs
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('<URL>', { waitUntil: 'domcontentloaded' });
// -- wait for readiness --
await page.waitForSelector('<container>', { state: 'visible' });
// -- extract --
const data = await page.$$eval('<item-selector>', items =>
items.map(el => ({
// fields mapped from user request
}))
);
// -- output --
const fs = require('fs');
fs.writeFileSync('output.json', JSON.stringify(data, null, 2));
console.log(`Extracted ${data.length} items → output.json`);
await browser.close();
})();
步骤 5:执行并验证
运行脚本,确认其正常工作:
node extract_<domain>_<slug>.cjs
验证规则:
| 检查项 | 通过条件 |
|---|---|
| 脚本以 0 退出 | 没有运行时错误 |
| 输出文件存在 | 已写入非空文件 |
| 记录数 > 0 | 至少提取到一项 |
| 没有 null/空字段 | 每个已声明字段在 ≥ 90% 的记录中都有值 |
| 数据与页面一致 | 使用 actionbook browser text 抽查第一条和最后一条记录 |
如果验证失败,请检查输出、调整选择器或等待策略,然后重新运行。
步骤 6:交付
向用户提供:
- 脚本路径——可随时重新运行的
.cjs文件。 - 数据路径——输出 JSON/CSV 文件。
- 记录数——提取到的条目数量。
- 说明——与机制相关的注意事项(例如,“此网站使用无限滚动;脚本默认最多滚动 50 页”)。
输出约定
每次 extract 调用都会生成:
| 产物 | 路径 | 格式 |
|---|---|---|
| Playwright 脚本 | ./extract_<domain>_<slug>.cjs | 使用 playwright 的独立 Node.js 脚本 |
| 提取的数据 | ./output.json(默认)或用户指定的路径 | 对象数组(默认 JSON 格式)、CSV,或用户指定的格式 |
脚本必须可重复运行——只要运行环境中有 Node.js + Playwright,用户之后就应能在未安装 Actionbook 的情况下执行脚本。
选择器优先级
当 actionbook get 提供多种选择器类型时:
| 优先级 | 类型 | 原因 |
|---|---|---|
| 1 | data-testid | 稳定,面向测试,很少变动 |
| 2 | aria-label | 以无障碍为导向,语义明确 |
| 3 | CSS 选择器 | 结构性选择器,重新设计后可能失效 |
| 4 | XPath | 最后的选择,最脆弱 |
错误处理
| 错误 | 操作 |
|---|---|
actionbook search 未返回结果 | 使用 snapshot + screenshot 作为备用方案 |
| 选择器返回 0 个元素 | 重新生成快照,与截图对比并更新选择器 |
| 脚本超时 | 增加 waitForTimeout 时长,检查是否存在反机器人措施 |
| 数据不完整(某些字段为空) | 检查内容是否延迟加载;添加滚动/等待 |
| 反机器人措施 / CAPTCHA | 告知用户;建议使用 headless: false 运行,或通过 actionbook setup 扩展模式使用用户自己的浏览器会话 |