Stagehand 动态网页采集:脚本、AI 配置与结果核对
按 Stagehand 4.3.0 的实际接口配置本地浏览器和模型,完成登录、筛选与分页示例,并区分已运行的确定性流程和未执行的 AI 调用。
动态页面的稳定步骤可以交给脚本执行,需要语义判断的位置再配置 AI。无论动作由谁完成,最终都要检查采集到的业务记录。
本文提供 Stagehand 4.3.0 的本地示例。2026 年 10 月 29 日,确定性流程已实际完成登录、筛选和两页采集,在两种页面变体中都得到三条正确商品。
模型配置及 observe()、act()、extract() 分支已完成源码接口核对和类型检查,没有执行模型调用。 本文不提供 AI 准确率、自愈成功率或 Token 成本实测。
任务完成的标准是什么
本地示例站包含五件商品。任务是登录后筛选分类为 book、状态为 active 的记录,遍历两页,输出下面三件。
| ID | 名称 | 页面价格 |
|---|---|---|
| p1 | Atlas | 19.50 |
| p2 | Beacon | 24.00 |
| p3 | Compass | 31.25 |
p4 为 inactive,p5 属于 tool,均不能出现在结果中。价格保留为字符串,避免把 24.00 变成另一种展示格式。
另一种页面变体修改筛选与翻页按钮的文字,并将渲染延迟从 100ms 改成 700ms。元素 ID 保持稳定,因此确定性脚本应能继续工作。
这不是“页面任意改版”的测试,也不证明 AI 已经适应了文案变化。
获取与本文一致的示例版本
先准备 Git 和已能访问公开仓库的 GitHub CLI。下面使用仓库标识获取固定版本示例。
gh repo clone 16yun-cn/www-16yun-cn-v3 ai-crawling-examples -- --filter=blob:none --sparse
cd ai-crawling-examples
git checkout --detach bc6828e62c417b1a3459199811fd4e2164137a5f
git sparse-checkout set scripts/blog-ai-crawling这个固定提交包含本文运行过的脚本和依赖锁;稀疏检出只取运行示例需要的目录及仓库根文件,不要求启动官网。
后文安装命令从这个 checkout 的根目录执行。代码摘录按完整示例的文件和行号标注,省略的 import、变量初始化及清理步骤仍在原文件中。
你可以先阅读代码摘录理解行为,再执行完整脚本;不要将多段摘录当作一个可直接粘贴运行的独立文件。
固定 SDK 和 Zod 版本
示例源码仓库为 16yun-cn/www-16yun-cn-v3,完整文件位于 scripts/blog-ai-crawling/,其中 Node 依赖独立于网站。
| 依赖 | 本例固定版本 |
|---|---|
| Node.js | 运行环境为 24.22.2 |
@browserbasehq/stagehand | 4.3.0 |
zod | 4.4.3 |
| TypeScript | 5.9.3 |
| 本地浏览器 | 本次为 Chrome 153.0.8010.12 |
实施时遇到过 Zod 版本不一致导致的类型错误。对齐已发布 SDK 使用的版本后,类型检查通过。不要同时复制不同代际教程中的类型与初始化代码。
终端一从源码根目录启动示例站。
python3 scripts/blog-ai-crawling/serve.py --port 8765终端二复制依赖文件和示例到独立目录,先检查类型,再运行无模型分支。
STAGEHAND_ENV="$(mktemp -d)"
cp scripts/blog-ai-crawling/package.json \
scripts/blog-ai-crawling/package-lock.json \
scripts/blog-ai-crawling/stagehand_catalog.mts "$STAGEHAND_ENV/"
cd "$STAGEHAND_ENV"
npm ci
npm run check
npm run stagehand本地浏览器可能需要下载,也需要系统依赖。若已有兼容浏览器,可通过 CHROME_PATH 指定可执行文件。FIXTURE_URL 可以调整 localhost 端口;示例拒绝远程主机名。
4.3.0 使用 Stagehand.create
下面是模型分支的初始化形式,需在完整脚本的生命周期管理中使用。
对应示例:scripts/blog-ai-crawling/stagehand_catalog.mts:25-45。以下是便于说明的等价或精简写法。
import { localBrowser, Stagehand, type ModelName } from "@browserbasehq/stagehand";
const browser = await localBrowser.launch({ headless: true });
const stagehand = await Stagehand.create({
browser,
model: {
modelName: process.env.STAGEHAND_MODEL as ModelName,
apiKey: process.env.MODEL_API_KEY,
},
});不要将旧版 new Stagehand({ env: "LOCAL" }) 直接套到这个固定版本。示例默认不传 model,也不执行任何模型方法。
源码核对基于 browserbase/stagehand 提交 d59afde。模型配置见 packages/protocol/schemas.ts:677-694:提供 modelName、apiKey 等定义字段,不能随意增加一个 baseURL 属性。
先用脚本建立登录和页面状态
示例使用测试账号 reader,密码为 fixture-only,只对本地教学站有意义。
对应示例:scripts/blog-ai-crawling/stagehand_catalog.mts:46-52。以下仅摘录相关语句,已为阅读展开换行。
const [page] = await browser.context.pages();
await page.goto(`${base}/login`);
await page.locator('[name="user"]').fill("reader");
await page.locator('[name="password"]').fill("fixture-only");
await page.locator('button[type="submit"]').click();
await page.waitForSelector('a[href="/catalog"]');登录后保持同一浏览器上下文,访问目录页。页面使用 Cookie 识别测试会话;直接未登录访问会返回 401。
不要把示例账号替换为写死在博客或仓库里的生产账号。真实站点的身份验证应按其规则接入。
配置模型的位置
只有你决定运行模型分支时,才需要设置以下环境变量。
export STAGEHAND_MODEL='provider/model-id'
export MODEL_API_KEY='replace-with-your-own-provider-key'
npm run stagehand -- --aiprovider/model-id 是占位符,不是可用模型。需要替换为当前 SDK 和服务商账户支持的名称。所核对版本包含 openai、anthropic、google、groq、cerebras 前缀;完整模型可用性由对应服务决定。
密钥由你在运行环境提供,不需要写进文章或交给作者。本文的准备与验证没有执行这条 AI 运行命令。
observe 返回候选,脚本执行明确动作
模型分支先询问分类下拉框的位置,再由代码选择明确的值。
对应示例:scripts/blog-ai-crawling/stagehand_catalog.mts:60-67。以下仅摘录相关语句,已为阅读展开换行。
const observed = await stagehand.observe("Find the category dropdown", { page });
assert.ok(observed.data.length > 0);
await page.locator(observed.data[0].selector).selectOption("book");
await page.locator("#status").selectOption("active");这里的候选位于 .data,不能当作一个直接返回的数组。方法接口见 packages/sdk-ts/src/stagehand.ts:263-283。
“有候选”只说明拿到了响应。模型分支尚未运行,第一项是否正确仍需实际核对;页面重新渲染后也不能假设旧定位信息一直有效。
act 之后等待数据状态变化
筛选动作可以使用自然语言指令,随后仍应等待业务状态。
对应示例:scripts/blog-ai-crawling/stagehand_catalog.mts:69-73。以下仅摘录相关语句,已为阅读展开换行。
await stagehand.act("Apply the selected category and status filters", { page });
await page.waitForSelector(
'#results[data-ready="yes"][data-generation="2"]'
);generation 标记由本地示例提供,用于区分初始结果和重新筛选后的结果。只等待 #results 存在,会有读到上一轮内容的风险。
实际运行的确定性分支使用 #apply 点击按钮;不能把它的成功写成 act() 已经做出了正确判断。
extract 的结构约束与内容正确性
示例明确字段结构,并限制提取范围为结果容器。
对应示例:scripts/blog-ai-crawling/stagehand_catalog.mts:17-19。以下是便于说明的等价或精简写法。抽取调用另见 scripts/blog-ai-crawling/stagehand_catalog.mts:78-80。
const schema = z.object({
products: z.array(z.object({
id: z.string(), name: z.string(), price: z.string(),
})),
});
const result = await stagehand.extract(
"Extract the ID, name and exact displayed price of every product row. Preserve price as a string.",
schema,
{ page, locator: page.locator("#results") },
);
products.push(...result.data.products);packages/sdk-ts/src/stagehand.ts:285-324 显示,客户端会用传入 schema 再次解析响应数据。结构不符合要求时可暴露错误,但名称、价格或记录身份仍可能在类型正确时出错。
因此最终仍检查唯一 ID,并逐项比较已知业务记录。
翻页必须检查结束条件
本地任务明确只有两页。脚本等待每页的 ready 和 page 标记,读取结果后检查 Next 按钮是否 disabled。
第一页应该可继续,第二页应该结束。示例会断言这一规则,而不是看到第二页就无条件认定采完。
这是一份固定两页的教学脚本。真实分页需要根据站点提供的结束信号扩展,不能直接拿固定循环次数覆盖未知页数。
哪些结果已经运行,哪些尚未运行
| 检查项 | 本次结果 | 能支持的结论 |
|---|---|---|
| TypeScript 检查 | 通过 | 示例符合固定版本的类型接口 |
| 原版确定性流程 | p1、p2、p3,字段正确 | 登录、筛选和两页读取可工作 |
| 改文案、延迟版确定性流程 | 同样得到三条正确记录 | 稳定 ID 加状态等待覆盖了这组变化 |
| AI observe/act/extract | 未执行 | 仅有配置、源码和类型依据 |
| 自愈、模型准确率、Token 成本 | 未测量 | 不作运行效果承诺 |
报告保留调用模式、页面变体和结果。单次本地耗时没有被用作模型性能排名。
若你自行运行 AI 分支,应记录模型名、日期、任务成功情况及可获取的用量信息,再结合服务商账单计算成本;不要从确定性脚本耗时估算模型开销。
运行前区分环境变量的用途
完整示例的参数处理位于 scripts/blog-ai-crawling/stagehand_catalog.mts:8-15,浏览器与输出配置分别见该文件 26-30、99-101 行。
| 变量 | 作用 | 什么时候需要设置 |
|---|---|---|
FIXTURE_URL | 本地示例站地址 | 你使用不同于 8765 的端口时 |
CHROME_PATH | 浏览器可执行文件位置 | 你希望使用已有兼容浏览器时 |
LAB_OUTPUT | JSON 结果文件路径 | 你需要将报告保存在指定目录时 |
STAGEHAND_MODEL | 带服务商前缀的模型标识 | 仅显式运行 AI 分支时 |
MODEL_API_KEY | 模型服务凭据 | 仅显式运行 AI 分支时 |
不带 --ai 的默认命令不需要后两个变量。它按固定定位读取页面,并将调用记录保留为空数组。
STAGEHAND_MODEL 的 TypeScript 类型断言不会替你验证某个服务商是否实际开放了该模型。完整脚本只检查基本参数条件,实际服务可用性仍需你在自己的环境中确认。
如果本地端口改变,需要同时修改服务器启动参数和客户端地址。只改其中一端,会在连接阶段失败,与模型配置无关。
分两层验收这条流程
先验收确定性基线,再决定是否运行语义步骤,避免模型配置问题掩盖已经存在的页面状态问题。
| 层级 | 核对项 | 本次状态 |
|---|---|---|
| 本地站点 | 测试账号登录后能进入目录 | 已运行 |
| 页面状态 | 筛选后等待新 generation;每页等待 ready | 已运行 |
| 完整性 | 两页结束状态正确,ID 不重复 | 已运行 |
| 字段 | p1、p2、p3 的名称和价格对应正确 | 已运行 |
| 模型配置 | 初始化参数、模型标识和方法返回结构 | 已核对源码及类型 |
| 模型行为 | 候选正确、动作成功、抽取内容真实 | 未执行推理 |
从第一层切换到 AI 分支后,前面的业务断言仍然保留。模型返回了一个对象,不是删除这些检查的理由。
默认报告中有 original 和 changed 两个条目。阅读时先确认 mode,再检查 products 和 calls,避免将确定性记录误读为 AI 调用记录。
这次两个条目的 mode 都是 deterministic,calls 都为空。它只说明这份示例走了无模型分支,不是一个对任意系统有效的用量统计方法。
运行结束后,在第一个终端用 Ctrl-C 停止测试服务器。浏览器上下文和临时 profile 的清理由脚本 finally 完成;不要将本地教学站当作长期运行的业务服务。
如何选择脚本与 AI 的边界
已知导航、固定筛选值和明确结束条件,可以先用脚本实现。需要语义判断时,再将候选定位或动作选择交给模型,并保留结果核对。
本次没有验证商业代理、跨站登录持久化、验证码或公网页面泛化。代理与模型配置都不能替代正确的加载条件和业务断言。
工具选择可回到 AI 爬虫选型指南,按采集链路决定需要哪一层能力。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。