Stagehand 动态网页采集:脚本、AI 配置与结果核对

按 Stagehand 4.3.0 的实际接口配置本地浏览器和模型,完成登录、筛选与分页示例,并区分已运行的确定性流程和未执行的 AI 调用。

亿牛云技术团队2026年10月28日10 分钟阅读

动态页面的稳定步骤可以交给脚本执行,需要语义判断的位置再配置 AI。无论动作由谁完成,最终都要检查采集到的业务记录。

本文提供 Stagehand 4.3.0 的本地示例。2026 年 10 月 29 日,确定性流程已实际完成登录、筛选和两页采集,在两种页面变体中都得到三条正确商品。

模型配置及 observe()、act()、extract() 分支已完成源码接口核对和类型检查,没有执行模型调用。 本文不提供 AI 准确率、自愈成功率或 Token 成本实测。

任务完成的标准是什么

本地示例站包含五件商品。任务是登录后筛选分类为 book、状态为 active 的记录,遍历两页,输出下面三件。

ID名称页面价格
p1Atlas19.50
p2Beacon24.00
p3Compass31.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/stagehand4.3.0
zod4.4.3
TypeScript5.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 -- --ai

provider/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_OUTPUTJSON 结果文件路径你需要将报告保存在指定目录时
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 爬虫选型指南,按采集链路决定需要哪一层能力。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。