别再手动写爬虫了!AI驱动的浏览器自动化神器,让你的效率提升10倍
GitHub星标暴涨的背后:browserbase/stagehand如何用自然语言重新定义网页交互
前言:传统浏览器自动化的困境
在过去的几年里,如果你想自动化浏览器操作,通常需要面对这些令人头疼的问题:
编写选择器是一场噩梦——当你好不容易定位到一个按钮,第二天网站更新后class名变了,一切努力付诸东流。处理动态内容让你抓耳挠腮,AJAX请求、懒加载、iframe嵌套,每一层都是新的挑战。更别提那些反爬机制严苛的网站,你需要像个老练的间谍一样精心设计每个请求头、绕过每个检测脚本。
直到Stagehand的出现。
这个由Browserbase团队打造的开源项目,将AI大模型的能力与浏览器自动化完美融合,让你可以用自然语言来控制浏览器。就像聘请了一位24小时在线的网页操作助手,你只需要说”点击登录按钮”或者”填写用户名和密码”,Stagehand就能理解你的意图并精准执行。
在本文中,我将带你从零开始,全面掌握这个革命性的工具。无论你是刚入门的新手还是有经验的开发者,都能从中获得实用的知识和技巧。
第一部分:为什么Stagehand值得关注
1.1 重新定义浏览器自动化的边界
传统的浏览器自动化工具如Selenium、Playwright、Puppeteer,本质上都是”指令执行器”——你需要精确告诉它们要找什么元素、在哪里点击、输入什么内容。这种方式的局限性显而易见:
代码与页面结构强耦合,一旦页面变化就需要频繁维护。复杂交互场景需要编写大量逻辑判断,代码可读性和可维护性都很差。对于动态渲染的现代Web应用,传统的定位策略往往力不从心。
Stagehand则采用了完全不同的思路——它是”意图理解器”。你描述你想要完成的任务,AI来理解你的意图并决定如何操作页面。这种方式的革命性在于:
智能元素识别:不再依赖脆弱的CSS选择器或XPath,Stagehand使用多模态AI模型来”看”页面,识别按钮、输入框、图片等元素。
自然语言交互:你可以用”点击蓝色的大按钮”或”找到购物车图标并点击它”这样的描述来指示操作。
自动容错处理:当页面结构变化时,AI能够智能适应,减少脚本失效的概率。
1.2 技术架构浅析
Stagehand构建在Playwright之上,充分利用了Playwright强大的浏览器控制能力。同时,它集成了多个AI模型提供商(包括OpenAI、Anthropic等),将自然语言理解能力注入到浏览器操作中。
用户自然语言指令
↓
Stagehand AI层(意图解析)
↓
Playwright执行层(浏览器控制)
↓
网页响应反馈
↓
AI结果验证
这种架构设计既保留了Playwright的稳定性和兼容性,又赋予了AI智能决策的能力。
1.3 社区反响与生态发展
自开源以来,Stagehand在GitHub上获得了广泛关注。它不仅吸引了大量个人开发者,更引起了许多企业的兴趣。从自动化测试到数据采集,从RPA(机器人流程自动化)到网页监控,Stagehand的应用场景正在不断拓展。
更值得关注的是,围绕Stagehand已经形成了初步的生态——有人开发了专门的适配器,有人贡献了预配置的模板库,还有人在探索将其与其他AI工具结合的可能性。
第二部分:环境搭建与快速入门
2.1 前置要求
在开始使用Stagehand之前,请确保你的开发环境满足以下要求:
Node.js环境:Stagehand是一个Node.js项目,需要Node.js 18或更高版本。你可以通过以下命令检查你的Node版本:
node --version
如果没有安装Node.js,建议使用nvm(Node Version Manager)来管理多个Node版本,这样可以为不同的项目维护独立的环境。
Python环境(可选):虽然Stagehand主要面向Node.js生态,但如果你想使用Python客户端或者进行一些数据处理,Python 3.8+会很有帮助。
AI API密钥:Stagehand需要连接到AI模型服务,你需要准备以下至少一种API密钥:
– OpenAI API密钥(推荐,支持GPT-4V等视觉模型)
– Anthropic API密钥(支持Claude的视觉能力)
– 或者是兼容OpenAI API格式的其他服务(如本地部署的模型)
2.2 项目初始化
让我们创建一个新的项目来开始Stagehand的学习之旅。
# 创建项目目录
mkdir my-stagehand-project
cd my-stagehand-project
# 初始化npm项目
npm init -y
# 安装stagehand核心包
npm install stagehand
对于Python用户,可以使用相应的Python SDK:
pip install stagehand-py
安装完成后,你需要在项目根目录创建环境配置文件.env,来存储你的API密钥:
# .env文件内容
OPENAI_API_KEY=sk-your-openai-api-key-here
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here
重要安全提示:切记将.env文件添加到.gitignore中,避免将敏感的API密钥提交到版本控制系统。
2.3 基础配置与初始化
创建一个stagehand.config.js文件来配置你的Stagehand实例:
// stagehand.config.js
const { Stagehand } = require('stagehand');
const stagehand = new Stagehand({
// 模型配置
model: 'gpt-4o', // 使用支持视觉的模型
provider: 'openai',
// 浏览器配置
headless: false, // 设置为true可在无头模式运行
browser: 'chromium', // 可选: chromium, firefox, webkit
// 调试配置
verbose: true, // 输出详细日志
logger: console.log,
// 安全与限制
maxRetries: 3, // 操作失败重试次数
timeout: 30000, // 操作超时时间(毫秒)
});
module.exports = { stagehand };
2.4 第一个Stagehand脚本
让我们创建一个最简单的脚本来验证环境配置是否正确:
// first-script.js
const { Stagehand } = require('stagehand');
async function main() {
// 创建Stagehand实例
const stagehand = new Stagehand({
model: 'gpt-4o',
headless: false,
});
try {
// 初始化
await stagehand.init();
// 打开网页
await stagehand.goto('https://example.com');
// 使用自然语言操作
await stagehand.act('获取页面的标题');
// 提取信息
const title = await stagehand.extract('页面的主要标题是什么?');
console.log('提取的标题:', title);
} catch (error) {
console.error('执行出错:', error);
} finally {
// 清理资源
await stagehand.close();
}
}
main();
运行这个脚本:
node first-script.js
如果一切正常,你应该会看到浏览器打开example.com,然后Stagehand会分析页面并执行相应的操作。
第三部分:核心功能详解
3.1 智能导航(goto)
goto方法是Stagehand中进行页面导航的核心功能。它不仅仅是一个简单的URL跳转,还包含了智能等待和状态验证。
// 基础用法
await stagehand.goto('https://www.example.com');
// 带选项的导航
await stagehand.goto('https://www.example.com', {
timeout: 60000, // 导航超时时间
waitUntil: 'networkidle', // 等待网络空闲
referer: 'https://google.com', // 设置引用来源
});
// 等待特定条件
await stagehand.goto('https://www.example.com');
await stagehand.waitForLoadState('domcontentloaded');
Stagehand的goto方法会等待页面达到稳定状态后才返回,这意味着你不必手动添加额外的等待时间。waitUntil选项支持以下值:
– load:默认,等待load事件
– domcontentloaded:等待DOM内容加载完成
– networkidle:等待网络请求完成(无活动超过500ms)
– commit:等待导航首次响应
3.2 自然语言操作(act)
act是Stagehand最核心的功能——它接受自然语言指令并执行相应的浏览器操作。
// 点击元素
await stagehand.act('点击登录按钮');
// 输入文本
await stagehand.act('在搜索框中输入"人工智能"');
// 悬停和悬停事件
await stagehand.act('鼠标悬停在产品图片上');
// 滚动操作
await stagehand.act('向下滚动页面');
// 组合操作
await stagehand.act('勾选"记住我"复选框');
// 复杂操作
await stagehand.act('点击导航栏中的"关于我们"链接');
act方法的底层工作流程:
- 截取当前页面截图
- 将截图和指令发送给AI模型
- AI分析页面结构,理解指令意图
- 确定需要操作的元素及其操作方式
- 生成Playwright代码并执行
- 验证操作是否成功
// act方法的高级选项
await stagehand.act('填写注册表单', {
// 操作超时时间
timeout: 30000,
// 是否在执行前等待元素可见
waitForVisible: true,
// 人类化的延迟(模拟人类操作速度)
humanlikeDelay: { min: 100, max: 300 },
// 失败时是否截图
screenshotOnFailure: true,
});
3.3 智能提取(extract)
extract方法用于从页面中提取结构化信息。它接受自然语言查询,返回AI理解后的数据。
// 提取单个信息
const title = await stagehand.extract('页面的标题是什么?');
console.log(title);
// 提取多个字段
const productInfo = await stagehand.extract({
query: '提取所有产品名称和价格',
// 指定返回格式
type: 'object', // 或 'string', 'array', 'json'
});
console.log(productInfo);
// 可能返回: { products: [{ name: '产品A', price: '99元' }, ...] }
extract方法的高级用法:
// 使用JSON Schema定义期望的返回格式
const structuredData = await stagehand.extract({
query: '提取文章信息',
type: 'json',
schema: {
title: '文章标题',
author: '作者姓名',
publishDate: '发布日期',
content: '文章摘要(前200字)',
tags: '所有标签列表',
},
});
// 批量提取(从列表页面提取多个条目)
const products = await stagehand.extract({
query: '提取页面上所有商品的信息',
type: 'array',
itemSchema: {
name: '商品名称',
price: '商品价格',
rating: '用户评分',
link: '商品详情页链接',
},
});
3.4 条件等待(wait)
Stagehand提供了智能的等待功能,可以等待页面达到特定状态或元素满足条件。
// 等待元素出现
await stagehand.wait('出现了"登录成功"的提示');
// 等待URL匹配
await stagehand.wait('URL变为/dashboard');
// 等待页面状态
await stagehand.wait('页面加载完成');
// 等待特定元素可见
await stagehand.wait('用户头像可见');
// 带超时的等待
await stagehand.wait('弹出了确认对话框', {
timeout: 10000,
timeoutMessage: '对话框未在预期时间内出现',
});
3.5 多步骤执行(do)
当一个任务需要多个步骤时,do方法提供了更好的流程控制能力。
const result = await stagehand.do(async (page) => {
// 第一步:导航到目标页面
await page.goto('https://www.example.com/login');
// 第二步:填写表单
await page.fill('input[name="username"]', 'myuser');
await page.fill('input[name="password"]', 'mypass');
// 第三步:点击登录
await page.click('button[type="submit"]');
// 第四步:等待跳转
await page.waitForURL('**/dashboard');
// 第五步:提取数据
const welcomeText = await page.textContent('.welcome-message');
return { success: true, welcomeText };
});
console.log(result);
// { success: true, welcomeText: '欢迎回来,myuser!' }
第四部分:实战教程——从入门到精通
4.1 实战一:自动化登录与数据采集
让我们从最常见的场景开始——编写一个自动化登录并采集数据的脚本。
场景描述:登录某个网站,获取用户个人中心的订单列表信息。
// login-and-collect.js
const { Stagehand } = require('stagehand');
class DataCollector {
constructor() {
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: true, // 正式运行使用无头模式
verbose: false, // 正式运行关闭详细日志
});
}
async initialize() {
await this.stagehand.init();
console.log('Stagehand初始化完成');
}
async login(username, password) {
console.log('开始登录流程...');
// 导航到登录页
await this.stagehand.goto('https://example-site.com/login');
// 使用自然语言填写登录表单
await this.stagehand.act('在用户名输入框中输入' + username);
await this.stagehand.act('在密码输入框中输入' + password);
// 点击登录按钮
await this.stagehand.act('点击登录按钮');
// 等待登录完成
await this.stagehand.wait('页面跳转到仪表盘');
console.log('登录成功!');
}
async collectOrderData() {
console.log('开始采集订单数据...');
// 导航到订单页面
await this.stagehand.goto('https://example-site.com/orders');
await this.stagehand.wait('订单列表加载完成');
// 提取订单信息
const orders = await this.stagehand.extract({
query: '提取所有订单的信息,包括订单号、日期、金额和状态',
type: 'array',
itemSchema: {
orderNumber: '订单号',
date: '下单日期',
amount: '订单金额',
status: '订单状态',
},
});
return orders;
}
async close() {
await this.stagehand.close();
console.log('资源已清理');
}
}
// 主函数
async function main() {
const collector = new DataCollector();
try {
await collector.initialize();
await collector.login('your_username', 'your_password');
// 采集数据
const orderData = await collector.collectOrderData();
console.log('\n===== 采集到的订单数据 =====');
console.log(JSON.stringify(orderData, null, 2));
// 这里可以添加数据存储逻辑
// await saveToDatabase(orderData);
} catch (error) {
console.error('执行过程中发生错误:', error.message);
// 发生错误时截图保存
await collector.stagehand.takeScreenshot('error-screenshot.png');
} finally {
await collector.close();
}
}
main();
4.2 实战二:批量表单填写与提交
这个例子展示如何处理复杂的表单填写场景,包括多种输入类型。
// batch-form-submission.js
const { Stagehand } = require('stagehand');
async function fillRegistrationForm(userData) {
const stagehand = new Stagehand({ model: 'gpt-4o' });
try {
await stagehand.init();
await stagehand.goto('https://example-site.com/register');
// 基本信息填写
await stagehand.act(`在"姓名"字段输入: ${userData.name}`);
await stagehand.act(`在"邮箱"字段输入: ${userData.email}`);
await stagehand.act(`在"手机号"字段输入: ${userData.phone}`);
await stagehand.act(`在"密码"字段输入: ${userData.password}`);
await stagehand.act(`在"确认密码"字段输入: ${userData.password}`);
// 处理单选按钮(性别选择)
await stagehand.act(`选择性别: ${userData.gender}`);
// 处理复选框(兴趣标签)
for (const interest of userData.interests) {
await stagehand.act(`勾选"${interest}"选项`);
}
// 处理下拉选择
await stagehand.act(`在国家下拉框中选择: ${userData.country}`);
await stagehand.act(`在城市下拉框中选择: ${userData.city}`);
// 处理日期选择
await stagehand.act(`选择出生日期: ${userData.birthday}`);
// 处理文件上传
await stagehand.act(`上传头像图片`, {
// 可以传递文件路径
// files: ['./avatar.jpg']
});
// 滚动到页面底部(如果有协议同意框)
await stagehand.act('向下滚动到页面底部');
// 处理协议同意
await stagehand.act('勾选"我已阅读并同意用户协议"');
// 提交表单
await stagehand.act('点击"注册"按钮');
// 等待并处理可能的验证
await stagehand.wait('注册成功提示或错误提示');
// 检查注册结果
const result = await stagehand.extract('页面上显示的注册结果是什么?');
console.log('注册结果:', result);
} finally {
await stagehand.close();
}
}
// 使用示例
const userData = {
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
password: 'SecurePass123!',
gender: '男',
interests: ['阅读', '编程', '旅游'],
country: '中国',
city: '北京',
birthday: '1995-06-15',
};
fillRegistrationForm(userData);
4.3 实战三:处理分页和无限滚动
很多网站使用分页或无限滚动来加载内容。下面的例子展示如何采集这类页面的数据。
// pagination-scraper.js
const { Stagehand } = require('stagehand');
class PaginatedScraper {
constructor() {
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
this.maxPages = 10; // 最大采集页数限制
this.allData = [];
}
async scrapeListPage(url, itemSelector) {
await this.stagehand.goto(url);
await this.stagehand.wait('列表内容加载完成');
let pageCount = 0;
while (pageCount < this.maxPages) {
console.log(`正在采集第 ${pageCount + 1} 页...`);
// 提取当前页的所有项目
const items = await this.stagehand.extract({
query: '提取当前页面所有文章的信息',
type: 'array',
itemSchema: {
title: '文章标题',
author: '作者',
publishDate: '发布日期',
summary: '文章摘要',
url: '文章链接',
},
});
this.allData.push(...items);
console.log(`第 ${pageCount + 1} 页: 获取 ${items.length} 条数据`);
// 检查是否有下一页
const hasNextPage = await this.checkNextPage();
if (!hasNextPage) {
console.log('已到达最后一页');
break;
}
// 点击下一页
await this.stagehand.act('点击"下一页"按钮');
await this.stagehand.wait('新内容加载完成');
pageCount++;
// 添加礼貌性延迟,避免对服务器造成压力
await this.delay(1000 + Math.random() * 1000);
}
return this.allData;
}
async checkNextPage() {
// 尝试提取判断是否还有下一页
const pagination = await this.stagehand.extract({
query: '当前页码和总页码分别是多少?',
type: 'object',
});
// 如果当前页码小于总页码,说明还有下一页
if (pagination.current && pagination.total) {
return pagination.current < pagination.total;
}
// 备选方案:检查"下一页"按钮是否可点击
try {
const buttonDisabled = await this.stagehand.evaluate(() => {
const nextBtn = document.querySelector('.pagination .next');
return nextBtn ? nextBtn.disabled : true;
});
return !buttonDisabled;
} catch {
return false;
}
}
async scrapeInfiniteScroll(url) {
console.log(`开始采集无限滚动页面: ${url}`);
await this.stagehand.goto(url);
let previousHeight = 0;
let scrollCount = 0;
const maxScrolls = 50; // 最大滚动次数
while (scrollCount < maxScrolls) {
// 提取当前可见区域的内容
const visibleItems = await this.stagehand.extract({
query: '提取当前可见的所有产品信息',
type: 'array',
itemSchema: {
name: '产品名称',
price: '产品价格',
rating: '产品评分',
},
});
// 添加到总数据(去重处理)
for (const item of visibleItems) {
if (!this.isDuplicate(item)) {
this.allData.push(item);
}
}
console.log(`滚动 ${scrollCount + 1}: 当前累计 ${this.allData.length} 条数据`);
// 获取当前页面高度
const currentHeight = await this.stagehand.evaluate(() => {
return document.documentElement.scrollHeight;
});
// 如果高度没有变化,说明已经加载完毕
if (currentHeight === previousHeight) {
console.log('页面已滚动到底部');
break;
}
// 向下滚动
await this.stagehand.act('向下滚动到底部');
scrollCount++;
// 等待新内容加载
await this.delay(1500 + Math.random() * 500);
previousHeight = currentHeight;
}
return this.allData;
}
isDuplicate(newItem) {
return this.allData.some(
existing => existing.name === newItem.name
);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async close() {
await this.stagehand.close();
}
}
// 使用示例 - 采集分页列表
async function main() {
const scraper = new PaginatedScraper();
try {
await scraper.stagehand.init();
// 采集分页列表
const articles = await scraper.scrapeListPage(
'https://example-blog.com/articles',
'.article-card'
);
console.log(`\n总共采集到 ${articles.length} 篇文章`);
console.log(JSON.stringify(articles.slice(0, 5), null, 2));
} finally {
await scraper.close();
}
}
main();
4.4 实战四:处理弹窗和模态框
弹窗和模态框是网页交互中的常见元素,也是自动化测试的难点之一。
// handle-modals.js
const { Stagehand } = require('stagehand');
async function handleVariousPopups() {
const stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
try {
await stagehand.init();
await stagehand.goto('https://example-site.com');
// ============================================
// 场景1:处理Cookie同意弹窗
// ============================================
try {
// 尝试多种可能的关闭方式
await stagehand.act('点击"接受所有Cookie"按钮', { timeout: 3000 });
console.log('Cookie弹窗已处理');
} catch {
try {
await stagehand.act('点击"好的,我知道了"按钮', { timeout: 2000 });
} catch {
try {
await stagehand.act('关闭Cookie提示', { timeout: 2000 });
} catch {
console.log('未检测到Cookie弹窗,继续执行');
}
}
}
// ============================================
// 场景2:处理公告/通知弹窗
// ============================================
await stagehand.wait('页面完全加载');
// 检查并关闭可能出现的公告弹窗
const hasAnnouncement = await stagehand.check('是否有弹窗出现?');
if (hasAnnouncement) {
await stagehand.act('关闭弹窗或点击"不再显示"复选框后关闭');
console.log('公告弹窗已处理');
}
// ============================================
// 场景3:处理确认对话框
// ============================================
// 例如:删除操作前的确认
await stagehand.act('点击删除按钮');
const dialogVisible = await stagehand.wait('确认对话框出现', { timeout: 5000 });
if (dialogVisible) {
// 根据业务逻辑决定是确认还是取消
const confirmDelete = true; // 或根据其他条件判断
if (confirmDelete) {
await stagehand.act('在确认对话框中点击"确定"');
} else {
await stagehand.act('在确认对话框中点击"取消"');
}
}
// ============================================
// 场景4:处理登录/注册弹窗
// ============================================
await stagehand.act('点击"登录"按钮');
await stagehand.wait('登录弹窗打开');
// 在弹窗中填写登录信息
await stagehand.act('在弹窗中的用户名输入框输入"testuser"');
await stagehand.act('在弹窗中的密码输入框输入"testpass"');
await stagehand.act('点击弹窗中的登录按钮');
// ============================================
// 场景5:处理iframe内的内容
// ============================================
// 有些弹窗内容在iframe中
try {
await stagehand.act('切换到弹窗的iframe中');
// 在iframe内操作
await stagehand.act('填写iframe内的表单');
// 操作完成后切回主文档
await stagehand.act('切换回主文档');
} catch {
console.log('弹窗不在iframe中或iframe切换失败');
}
// ============================================
// 场景6:处理嵌套弹窗
// ============================================
// 先处理内层弹窗
await stagehand.act('关闭内层弹窗');
await this.delay(300);
// 再处理外层弹窗
await stagehand.act('在外层弹窗中点击确定');
} finally {
await stagehand.close();
}
}
// 通用弹窗处理策略
async function smartPopupHandler(stagehand) {
const popupStrategies = [
// 策略1:查找并点击关闭按钮
async () => {
const closeSelectors = [
'[aria-label="Close"]',
'.modal-close',
'.popup-close',
'.close-btn',
'[class*="close"]',
'button:has-text("关闭")',
'button:has-text("×")',
];
for (const selector of closeSelectors) {
try {
await stagehand.click(selector);
return true;
} catch {
continue;
}
}
return false;
},
// 策略2:点击遮罩层关闭
async () => {
try {
await stagehand.click('.modal-overlay', { position: { x: 10, y: 10 } });
return true;
} catch {
return false;
}
},
// 策略3:按ESC键关闭
async () => {
try {
await stagehand.keyboard.press('Escape');
return true;
} catch {
return false;
}
},
];
for (const strategy of popupStrategies) {
try {
const handled = await strategy();
if (handled) {
await stagehand.wait('弹窗消失');
return true;
}
} catch {
continue;
}
}
return false;
}
module.exports = { handleVariousPopups, smartPopupHandler };
4.5 实战五:电商网站数据采集
这个综合实战展示如何采集电商网站的商品信息。
// ecommerce-scraper.js
const { Stagehand } = require('stagehand');
class EcommerceScraper {
constructor(config) {
this.config = {
headless: true,
...config,
};
this.stagehand = new Stagehand({
model: 'gpt-4o',
...this.config,
});
this.products = [];
}
async initialize() {
await this.stagehand.init();
console.log('电商数据采集器已就绪');
}
async searchProducts(keyword) {
console.log(`开始搜索: ${keyword}`);
// 导航到搜索页面
await this.stagehand.goto('https://ecommerce-example.com/search');
// 输入搜索关键词
await this.stagehand.act(`在搜索框中输入"${keyword}"并按回车`);
// 等待搜索结果加载
await this.stagehand.wait('搜索结果列表出现');
console.log('搜索完成,开始采集...');
}
async filterResults(filters) {
// 应用价格筛选
if (filters.minPrice || filters.maxPrice) {
const priceText = filters.minPrice && filters.maxPrice
? `${filters.minPrice}到${filters.maxPrice}`
: filters.minPrice
? `${filters.minPrice}以上`
: `${filters.maxPrice}以下`;
await this.stagehand.act(`设置价格筛选为${priceText}`);
await this.stagehand.wait('筛选结果更新');
}
// 应用评分筛选
if (filters.minRating) {
await this.stagehand.act(`筛选${filters.minRating}星及以上商品`);
await this.stagehand.wait('筛选结果更新');
}
// 选择排序方式
if (filters.sortBy) {
const sortMap = {
'price-low': '价格从低到高',
'price-high': '价格从高到低',
'sales': '销量优先',
'rating': '评分优先',
'newest': '新品优先',
};
await this.stagehand.act(`按"${sortMap[filters.sortBy] || filters.sortBy}"排序`);
await this.stagehand.wait('排序结果更新');
}
}
async collectProductList(maxPages = 5) {
let pageNum = 0;
while (pageNum < maxPages) {
console.log(`\n正在采集第 ${pageNum + 1} 页...`);
// 采集当前页商品列表
const pageProducts = await this.collectCurrentPageProducts();
this.products.push(...pageProducts);
console.log(`第 ${pageNum + 1} 页: 新增 ${pageProducts.length} 个商品`);
console.log(`累计已采集: ${this.products.length} 个商品`);
// 检查是否还有下一页
const hasNextPage = await this.checkPagination(pageNum);
if (!hasNextPage) {
console.log('已到达最后一页');
break;
}
// 点击下一页
await this.stagehand.act('点击下一页按钮');
await this.stagehand.wait('新页商品加载完成');
pageNum++;
// 礼貌性延迟
await this.delay(1500 + Math.random() * 1000);
}
return this.products;
}
async collectCurrentPageProducts() {
// 提取当前页所有商品的简要信息
const productSummaries = await this.stagehand.extract({
query: '提取当前页面所有商品的简要信息',
type: 'array',
itemSchema: {
id: '商品ID或编号',
title: '商品标题/名称',
price: '商品价格',
originalPrice: '原价(如有折扣)',
rating: '商品评分',
sales: '销量',
shop: '店铺名称',
link: '商品详情页链接',
},
});
return productSummaries;
}
async collectProductDetails(productLink) {
console.log(`正在采集商品详情: ${productLink}`);
// 在新标签页打开商品详情
await this.stagehand.goto(productLink, { waitUntil: 'networkidle' });
// 提取详细信息
const details = await this.stagehand.extract({
query: '提取商品的详细信息',
type: 'object',
schema: {
title: '商品标题',
price: '商品价格',
description: '商品详细描述',
images: '商品图片URL列表',
specifications: '商品规格参数',
shop: {
name: '店铺名称',
rating: '店铺评分',
location: '店铺所在地',
},
shipping: '发货信息',
stock: '库存状态',
reviews: '用户评价概要',
},
});
// 返回主列表页
await this.stagehand.goBack();
await this.stagehand.wait('商品列表恢复');
return details;
}
async checkPagination(currentPage) {
// 检查分页信息
try {
const pageInfo = await this.stagehand.extract({
query: '当前在第几页?是否有下一页?',
type: 'object',
});
return pageInfo.hasNextPage;
} catch {
// 尝试直接检查下一页按钮
try {
const nextBtn = await this.stagehand.$('.pagination .next:not([disabled])');
return nextBtn !== null;
} catch {
return false;
}
}
}
async collectReviews(maxReviews = 50) {
console.log('开始采集商品评论...');
// 提取评论列表
const reviews = [];
let collected = 0;
while (collected < maxReviews) {
const pageReviews = await this.stagehand.extract({
query: '提取当前页所有评论内容',
type: 'array',
itemSchema: {
author: '评论者名称',
date: '评论日期',
rating: '评分',
content: '评论正文',
likes: '点赞数',
images: '评论晒图',
},
});
reviews.push(...pageReviews);
collected += pageReviews.length;
console.log(`已采集 ${reviews.length} 条评论`);
// 检查是否有下一页评论
const hasMoreReviews = await this.stagehand.check('是否有更多评论可加载?');
if (!hasMoreReviews || reviews.length >= maxReviews) {
break;
}
await this.stagehand.act('加载更多评论');
await this.stagehand.wait('新评论加载完成');
await this.delay(1000);
}
return reviews;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async generateReport() {
// 生成数据报告
const report = {
totalProducts: this.products.length,
collectedAt: new Date().toISOString(),
summary: {
priceRange: this.getPriceRange(),
avgRating: this.getAverageRating(),
topShops: this.getTopShops(),
},
products: this.products,
};
console.log('\n===== 数据采集报告 =====');
console.log(`总计采集商品: ${report.totalProducts}`);
console.log(`价格区间: ${report.summary.priceRange}`);
console.log(`平均评分: ${report.summary.avgRating.toFixed(2)}`);
console.log(`热卖店铺: ${report.summary.topShops.join(', ')}`);
return report;
}
getPriceRange() {
const prices = this.products.map(p => parseFloat(p.price));
const min = Math.min(...prices);
const max = Math.max(...prices);
return `${min} - ${max}`;
}
getAverageRating() {
const ratings = this.products
.map(p => parseFloat(p.rating))
.filter(r => !isNaN(r));
return ratings.reduce((sum, r) => sum + r, 0) / ratings.length;
}
getTopShops() {
const shopCount = {};
this.products.forEach(p => {
shopCount[p.shop] = (shopCount[p.shop] || 0) + 1;
});
return Object.entries(shopCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([shop]) => shop);
}
async saveToFile(filename = 'products.json') {
const fs = require('fs').promises;
await fs.writeFile(
filename,
JSON.stringify(this.products, null, 2),
'utf-8'
);
console.log(`数据已保存到 ${filename}`);
}
async close() {
await this.stagehand.close();
console.log('采集器已关闭');
}
}
// 主函数
async function main() {
const scraper = new EcommerceScraper();
try {
await scraper.initialize();
// 搜索商品
await scraper.searchProducts('无线蓝牙耳机');
// 应用筛选
await scraper.filterResults({
minPrice: 100,
maxPrice: 500,
minRating: 4.0,
sortBy: 'sales',
});
// 采集商品列表(3页)
await scraper.collectProductList(3);
// 采集第一个商品的详情
if (scraper.products.length > 0) {
const firstProduct = scraper.products[0];
const details = await scraper.collectProductDetails(firstProduct.link);
console.log('商品详情:', details);
}
// 生成报告并保存
await scraper.generateReport();
await scraper.saveToFile('ecommerce-products.json');
} catch (error) {
console.error('采集过程中出错:', error);
} finally {
await scraper.close();
}
}
main();
第五部分:高级技巧与最佳实践
5.1 错误处理与重试机制
健壮的自动化脚本必须具备完善的错误处理能力。
// robust-error-handling.js
const { Stagehand } = require('stagehand');
class RobustAutomation {
constructor() {
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
// 配置重试策略
this.retryConfig = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
exponentialBackoff: true,
};
}
// 带重试的包装函数
async withRetry(operation, context = '操作') {
let lastError;
for (let attempt = 1; attempt <= this.retryConfig.maxRetries; attempt++) {
try {
console.log(`${context}: 第 ${attempt} 次尝试...`);
return await operation();
} catch (error) {
lastError = error;
console.error(`${context} 失败 (尝试 ${attempt}/${this.retryConfig.maxRetries}):`, error.message);
if (attempt < this.retryConfig.maxRetries) {
// 计算延迟时间(指数退避)
const delay = this.calculateDelay(attempt);
console.log(`等待 ${delay}ms 后重试...`);
await this.delay(delay);
}
}
}
// 所有重试都失败后
console.error(`${context} 在 ${this.retryConfig.maxRetries} 次尝试后仍然失败`);
throw lastError;
}
calculateDelay(attempt) {
if (this.retryConfig.exponentialBackoff) {
const delay = this.retryConfig.baseDelay * Math.pow(2, attempt - 1);
return Math.min(delay, this.retryConfig.maxDelay);
}
return this.retryConfig.baseDelay;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 安全的页面操作
async safeAct(instruction, options = {}) {
return this.withRetry(async () => {
// 操作前截图(用于调试)
if (options.screenshotBefore) {
await this.stagehand.takeScreenshot('before-action.png');
}
const result = await this.stagehand.act(instruction, {
timeout: options.timeout || 30000,
});
// 操作后截图
if (options.screenshotAfter) {
await this.stagehand.takeScreenshot('after-action.png');
}
return result;
}, `执行操作: ${instruction}`);
}
// 安全的导航
async safeGoto(url, options = {}) {
return this.withRetry(async () => {
console.log(`正在导航到: ${url}`);
await this.stagehand.goto(url, {
timeout: options.timeout || 60000,
waitUntil: options.waitUntil || 'networkidle',
});
// 验证页面加载成功
await this.validatePageLoaded();
return true;
}, `导航到 ${url}`);
}
async validatePageLoaded() {
// 检查页面是否正常加载
const title = await this.stagehand.title();
if (!title || title.includes('error') || title.includes('404')) {
throw new Error(`页面加载异常: ${title}`);
}
// 检查是否有明显的错误提示
const hasError = await this.stagehand.evaluate(() => {
const errorElements = document.querySelectorAll(
'.error, .error-page, [class*="error"]'
);
return errorElements.length > 0;
});
if (hasError) {
throw new Error('页面包含错误元素');
}
}
// 全局错误处理器
setupGlobalErrorHandlers() {
process.on('unhandledRejection', async (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
await this.takeErrorSnapshot('unhandled-rejection');
});
process.on('uncaughtException', async (error) => {
console.error('未捕获的异常:', error);
await this.takeErrorSnapshot('uncaught-exception');
});
}
async takeErrorSnapshot(label) {
try {
const filename = `error-${label}-${Date.now()}.png`;
await this.stagehand.takeScreenshot(filename);
console.log(`错误快照已保存: ${filename}`);
} catch {
console.error('无法保存错误快照');
}
}
}
// 使用示例
async function main() {
const automation = new RobustAutomation();
// 设置全局错误处理
automation.setupGlobalErrorHandlers();
try {
await automation.stagehand.init();
// 安全导航
await automation.safeGoto('https://example-site.com', {
timeout: 30000,
});
// 安全操作
await automation.safeAct('点击登录按钮', {
screenshotBefore: true,
screenshotAfter: true,
timeout: 15000,
});
} catch (error) {
console.error('执行失败:', error);
await automation.takeErrorSnapshot('final-error');
} finally {
await automation.stagehand.close();
}
}
main();
5.2 并行执行与效率优化
对于需要处理大量页面的场景,并行执行可以显著提升效率。
// parallel-execution.js
const { Stagehand } = require('stagehand');
const pLimit = require('p-limit'); // 并发限制库
class ParallelScraper {
constructor(maxConcurrent = 3) {
this.maxConcurrent = maxConcurrent;
this.limit = pLimit(maxConcurrent);
}
// 创建独立的Stagehand实例
createInstance(instanceId) {
return new Stagehand({
model: 'gpt-4o',
headless: true,
verbose: false,
});
}
// 并行采集多个页面
async scrapeMultiple(urls) {
console.log(`开始并行采集 ${urls.length} 个页面 (最大并发: ${this.maxConcurrent})`);
// 使用Promise.all并行执行所有任务
const tasks = urls.map((url, index) =>
this.limit(async () => {
console.log(`[${index + 1}/${urls.length}] 开始采集: ${url}`);
const instance = this.createInstance(index);
try {
await instance.init();
await instance.goto(url);
const data = await instance.extract({
query: '提取页面标题和主要内容',
type: 'object',
});
console.log(`[${index + 1}/${urls.length}] 完成: ${url}`);
return {
url,
success: true,
data,
};
} catch (error) {
console.error(`[${index + 1}/${urls.length}] 失败: ${url}`, error.message);
return {
url,
success: false,
error: error.message,
};
} finally {
await instance.close();
}
})
);
const results = await Promise.all(tasks);
console.log('\n===== 采集完成 =====');
console.log(`成功: ${results.filter(r => r.success).length}`);
console.log(`失败: ${results.filter(r => !r.success).length}`);
return results;
}
// 分批处理(适合大量URL)
async scrapeInBatches(urls, batchSize = 10) {
const allResults = [];
for (let i = 0; i < urls.length; i += batchSize) {
const batch = urls.slice(i, i + batchSize);
console.log(`\n处理批次 ${Math.floor(i / batchSize) + 1}: 包含 ${batch.length} 个URL`);
const batchResults = await this.scrapeMultiple(batch);
allResults.push(...batchResults);
// 批次间延迟
if (i + batchSize < urls.length) {
console.log('批次间等待...');
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
return allResults;
}
// 使用工作池模式
async scrapeWithWorkerPool(urls, workerCount = 5) {
console.log(`启动工作池 (${workerCount} 个worker)`);
// 创建worker队列
const workers = Array.from({ length: workerCount }, (_, i) => i);
const urlQueue = [...urls];
const results = [];
// 每个worker的处理函数
const workerTask = async (workerId) => {
const workerResults = [];
while (urlQueue.length > 0) {
// 从队列取出一个URL
const url = urlQueue.shift();
if (!url) break;
console.log(`[Worker ${workerId}] 处理: ${url}`);
const instance = this.createInstance(workerId);
try {
await instance.init();
await instance.goto(url);
const data = await instance.extract({
query: '提取页面数据',
type: 'object',
});
workerResults.push({ url, success: true, data });
} catch (error) {
workerResults.push({ url, success: false, error: error.message });
} finally {
await instance.close();
}
// 任务间延迟(避免过快)
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500));
}
return workerResults;
};
// 并行运行所有worker
const workerResults = await Promise.all(
workers.map(id => workerTask(id))
);
// 合并结果
for (const workerResult of workerResults) {
results.push(...workerResult);
}
return results;
}
}
// 使用示例
async function main() {
const scraper = new ParallelScraper(maxConcurrent = 3);
// 准备URL列表
const urls = [
'https://example-site.com/page/1',
'https://example-site.com/page/2',
'https://example-site.com/page/3',
'https://example-site.com/page/4',
'https://example-site.com/page/5',
];
// 选择一种执行方式
// 1. 直接并行(URL数量较少时)
const results = await scraper.scrapeMultiple(urls);
// 2. 分批处理(URL数量较多时)
// const results = await scraper.scrapeInBatches(urls, batchSize = 5);
// 3. 工作池模式(自定义并发数)
// const results = await scraper.scrapeWithWorkerPool(urls, workerCount = 5);
console.log('\n所有任务完成');
console.log(JSON.stringify(results, null, 2));
}
main();
5.3 上下文管理与状态持久化
对于复杂的多步骤流程,需要妥善管理浏览器上下文和会话状态。
// context-management.js
const { Stagehand } = require('stagehand');
const fs = require('fs');
class StatefulScraper {
constructor() {
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
this.stateFile = './session-state.json';
this.sessionState = {};
}
async initialize() {
// 尝试恢复之前的会话状态
await this.loadState();
await this.stagehand.init();
// 如果有保存的cookies,恢复登录状态
if (this.sessionState.cookies) {
console.log('恢复会话状态...');
await this.restoreSession();
}
}
async loadState() {
try {
if (fs.existsSync(this.stateFile)) {
const data = fs.readFileSync(this.stateFile, 'utf-8');
this.sessionState = JSON.parse(data);
console.log('已加载保存的状态');
}
} catch (error) {
console.log('无法加载状态文件,将创建新会话');
this.sessionState = {};
}
}
async saveState() {
try {
// 保存cookies
this.sessionState.cookies = await this.stagehand.context.cookies();
// 保存localStorage
this.sessionState.localStorage = await this.stagehand.evaluate(() => {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
return data;
});
// 保存sessionStorage
this.sessionState.sessionStorage = await this.stagehand.evaluate(() => {
const data = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
data[key] = sessionStorage.getItem(key);
}
return data;
});
// 保存当前URL
this.sessionState.lastUrl = this.stagehand.url();
// 保存时间戳
this.sessionState.savedAt = new Date().toISOString();
fs.writeFileSync(
this.stateFile,
JSON.stringify(this.sessionState, null, 2)
);
console.log('状态已保存');
} catch (error) {
console.error('保存状态失败:', error);
}
}
async restoreSession() {
try {
// 恢复cookies
if (this.sessionState.cookies) {
await this.stagehand.context.addCookies(this.sessionState.cookies);
}
// 打开任意页面以设置storage
await this.stagehand.goto(this.sessionState.lastUrl || 'https://example.com');
// 恢复localStorage
if (this.sessionState.localStorage) {
await this.stagehand.evaluate((data) => {
for (const [key, value] of Object.entries(data)) {
localStorage.setItem(key, value);
}
}, this.sessionState.localStorage);
}
// 恢复sessionStorage
if (this.sessionState.sessionStorage) {
await this.stagehand.evaluate((data) => {
for (const [key, value] of Object.entries(data)) {
sessionStorage.setItem(key, value);
}
}, this.sessionState.sessionStorage);
}
console.log('会话状态已恢复');
} catch (error) {
console.error('恢复会话失败:', error);
this.sessionState = {};
}
}
// 渐进式保存
async checkpoint(name) {
this.sessionState.checkpoints = this.sessionState.checkpoints || {};
this.sessionState.checkpoints[name] = {
url: await this.stagehand.url(),
timestamp: new Date().toISOString(),
};
// 每10个checkpoint保存一次
const checkpointCount = Object.keys(this.sessionState.checkpoints).length;
if (checkpointCount % 10 === 0) {
await this.saveState();
}
}
// 从checkpoint恢复
async restoreCheckpoint(name) {
const checkpoint = this.sessionState.checkpoints?.[name];
if (checkpoint) {
await this.stagehand.goto(checkpoint.url);
console.log(`已恢复到checkpoint: ${name}`);
} else {
console.log(`未找到checkpoint: ${name}`);
}
}
async close() {
await this.saveState();
await this.stagehand.close();
}
}
// 使用示例
async function main() {
const scraper = new StatefulScraper();
try {
await scraper.initialize();
// 登录流程
await scraper.stagehand.goto('https://example-site.com/login');
await scraper.stagehand.act('填写登录表单');
await scraper.stagehand.act('点击登录');
// 保存登录状态
await scraper.saveState();
await scraper.checkpoint('after-login');
// 继续其他操作...
await scraper.stagehand.goto('https://example-site.com/dashboard');
await scraper.checkpoint('dashboard');
// 后续运行时会自动恢复登录状态
await scraper.close();
} catch (error) {
console.error('执行出错:', error);
}
}
main();
5.4 日志与调试技巧
完善的日志记录对于调试和监控自动化流程至关重要。
// logging-and-debugging.js
const { Stagehand } = require('stagehand');
class DebuggableAutomation {
constructor(options = {}) {
this.debugMode = options.debugMode || false;
this.logFile = options.logFile || './automation.log';
this.screenshotDir = options.screenshotDir || './screenshots';
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: !this.debugMode,
verbose: this.debugMode,
logger: this.log.bind(this),
});
this.operationLog = [];
}
log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
data,
};
this.operationLog.push(logEntry);
// 控制台输出
const levelPrefix = {
DEBUG: '🔍',
INFO: 'ℹ️',
WARN: '⚠️',
ERROR: '❌',
};
console.log(
`${levelPrefix[level] || '📝'} [${timestamp}] [${level}] ${message}`
);
if (data && this.debugMode) {
console.log(' 数据:', JSON.stringify(data, null, 2));
}
}
// 操作前记录
async logBeforeOperation(operation, context = {}) {
this.log('INFO', `开始操作: ${operation}`, context);
if (this.debugMode) {
await this.takeAnnotatedScreenshot(`before-${operation}`);
}
const startTime = Date.now();
return { startTime };
}
// 操作后记录
async logAfterOperation(operation, result, timing) {
const duration = Date.now() - timing.startTime;
this.log('INFO', `完成操作: ${operation}`, {
duration: `${duration}ms`,
result: result ? '成功' : '失败',
});
if (this.debugMode) {
await this.takeAnnotatedScreenshot(`after-${operation}`);
}
}
async takeAnnotatedScreenshot(label) {
try {
const filename = `${this.screenshotDir}/${label}-${Date.now()}.png`;
await this.stagehand.takeScreenshot(filename);
this.log('DEBUG', `截图已保存: ${filename}`);
} catch (error) {
this.log('WARN', `截图失败: ${error.message}`);
}
}
// 性能追踪
async traceOperation(operation, asyncFn) {
const metrics = {
name: operation,
startTime: Date.now(),
memoryBefore: process.memoryUsage().heapUsed,
};
try {
this.log('DEBUG', `开始追踪: ${operation}`);
const result = await asyncFn();
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.memoryAfter = process.memoryUsage().heapUsed;
metrics.memoryDelta = metrics.memoryAfter - metrics.memoryBefore;
metrics.success = true;
this.log('INFO', `操作完成: ${operation}`, {
duration: `${metrics.duration}ms`,
memoryDelta: `${(metrics.memoryDelta / 1024 / 1024).toFixed(2)}MB`,
});
return { success: true, result, metrics };
} catch (error) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.success = false;
metrics.error = error.message;
this.log('ERROR', `操作失败: ${operation}`, {
duration: `${metrics.duration}ms`,
error: error.message,
});
await this.takeAnnotatedScreenshot(`error-${operation}`);
return { success: false, error, metrics };
}
}
// 生成操作报告
generateReport() {
const report = {
generatedAt: new Date().toISOString(),
totalOperations: this.operationLog.length,
operationsByLevel: {},
errors: [],
summary: {},
};
// 按级别统计
for (const entry of this.operationLog) {
report.operationsByLevel[entry.level] =
(report.operationsByLevel[entry.level] || 0) + 1;
if (entry.level === 'ERROR') {
report.errors.push(entry);
}
}
// 计算成功率
const successCount = report.operationsByLevel['INFO'] || 0;
const errorCount = report.operationsByLevel['ERROR'] || 0;
report.summary.successRate =
`${((successCount / (successCount + errorCount)) * 100).toFixed(1)}%`;
return report;
}
// 导出日志
exportLogs(filename = 'operation-log.json') {
const fs = require('fs');
fs.writeFileSync(filename, JSON.stringify(this.operationLog, null, 2));
this.log('INFO', `日志已导出: ${filename}`);
}
}
// 使用示例
async function main() {
const automation = new DebuggableAutomation({
debugMode: true,
logFile: './automation.log',
screenshotDir: './debug-screenshots',
});
try {
await automation.stagehand.init();
// 使用追踪包装操作
await automation.traceOperation('登录网站', async () => {
await automation.stagehand.goto('https://example-site.com');
await automation.stagehand.act('点击登录按钮');
await automation.stagehand.act('填写用户名和密码');
await automation.stagehand.act('提交表单');
});
await automation.traceOperation('导航到仪表盘', async () => {
await automation.stagehand.goto('https://example-site.com/dashboard');
});
// 生成报告
const report = automation.generateReport();
console.log('\n===== 操作报告 =====');
console.log(JSON.stringify(report, null, 2));
// 导出日志
automation.exportLogs();
} finally {
await automation.stagehand.close();
}
}
main();
第六部分:常见应用场景
6.1 自动化测试
Stagehand可以用于编写端到端测试,用自然语言描述测试用例。
// e2e-testing.js
const { Stagehand } = require('stagehand');
class E2ETestRunner {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.testResults = [];
}
async runTest(testName, testFn) {
console.log(`\n运行测试: ${testName}`);
const stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
try {
await stagehand.init();
await testFn(stagehand);
this.testResults.push({
name: testName,
status: 'PASSED',
duration: Date.now(),
});
console.log(`✅ ${testName} - 通过`);
return true;
} catch (error) {
this.testResults.push({
name: testName,
status: 'FAILED',
error: error.message,
duration: Date.now(),
});
console.log(`❌ ${testName} - 失败: ${error.message}`);
return false;
} finally {
await stagehand.close();
}
}
// 测试用例:用户注册流程
async testUserRegistration(stagehand) {
await stagehand.goto(`${this.baseUrl}/register`);
// 填写表单
await stagehand.act('填写邮箱为 test@example.com');
await stagehand.act('填写密码为 TestPass123!');
await stagehand.act('填写用户名为 testuser');
// 提交
await stagehand.act('点击注册按钮');
// 验证
const successMessage = await stagehand.extract(
'页面上是否显示"注册成功"的提示?'
);
if (!successMessage.includes('成功')) {
throw new Error('注册成功后未显示成功提示');
}
}
// 测试用例:搜索功能
async testSearchFunctionality(stagehand) {
await stagehand.goto(this.baseUrl);
await stagehand.act('在搜索框中输入"测试产品"');
await stagehand.act('点击搜索按钮或按回车');
await stagehand.wait('搜索结果出现');
const results = await stagehand.extract({
query: '有多少条搜索结果?',
type: 'string',
});
if (results.includes('0')) {
throw new Error('搜索未返回任何结果');
}
}
// 测试用例:购物车流程
async testShoppingCartFlow(stagehand) {
await stagehand.goto(`${this.baseUrl}/products`);
// 添加商品到购物车
await stagehand.act('点击第一个商品的"加入购物车"按钮');
await stagehand.wait('添加到购物车的提示');
// 验证购物车数量
await stagehand.act('点击购物车图标');
const cartCount = await stagehand.extract('购物车中有几件商品?');
if (!cartCount.includes('1')) {
throw new Error('购物车数量不正确');
}
}
// 生成测试报告
generateTestReport() {
const passed = this.testResults.filter(t => t.status === 'PASSED').length;
const failed = this.testResults.filter(t => t.status === 'FAILED').length;
console.log('\n========== 测试报告 ==========');
console.log(`总计: ${this.testResults.length}`);
console.log(`通过: ${passed}`);
console.log(`失败: ${failed}`);
console.log(`通过率: ${((passed / this.testResults.length) * 100).toFixed(1)}%`);
if (failed > 0) {
console.log('\n失败用例:');
this.testResults
.filter(t => t.status === 'FAILED')
.forEach(t => console.log(` - ${t.name}: ${t.error}`));
}
return {
summary: { total: passed + failed, passed, failed },
results: this.testResults,
};
}
}
// 使用示例
async function main() {
const runner = new E2ETestRunner('https://your-app-url.com');
await runner.runTest('用户注册流程', runner.testUserRegistration.bind(runner));
await runner.runTest('搜索功能', runner.testSearchFunctionality.bind(runner));
await runner.runTest('购物车流程', runner.testShoppingCartFlow.bind(runner));
runner.generateTestReport();
}
main();
6.2 网页监控与预警
设置定时任务监控网页变化,及时发现问题。
// web-monitor.js
const { Stagehand } = require('stagehand');
const schedule = require('node-schedule');
class WebMonitor {
constructor(config) {
this.config = {
headless: true,
checkInterval: config.checkInterval || '*/30 * * * *', // 默认每30分钟检查一次
...config,
};
this.stagehand = new Stagehand({ model: 'gpt-4o' });
this.baseline = {}; // 存储基准数据
this.alertCallbacks = [];
}
// 注册告警回调
onAlert(callback) {
this.alertCallbacks.push(callback);
}
// 发送告警
async sendAlert(alert) {
const alertMessage = {
timestamp: new Date().toISOString(),
...alert,
};
console.log('🚨 告警:', alertMessage);
for (const callback of this.alertCallbacks) {
try {
await callback(alertMessage);
} catch (error) {
console.error('告警发送失败:', error);
}
}
}
// 监控单个页面
async checkPage(pageConfig) {
const { name, url, selectors, expectedContent } = pageConfig;
console.log(`\n检查页面: ${name}`);
try {
await this.stagehand.init();
await this.stagehand.goto(url, { waitUntil: 'networkidle' });
// 页面可访问性检查
const isAccessible = await this.checkAccessibility();
if (!isAccessible) {
await this.sendAlert({
type: 'ACCESSIBILITY',
page: name,
message: '页面无法访问或加载超时',
url,
});
return;
}
// 检查关键内容
for (const content of expectedContent || []) {
const found = await this.checkContent(content);
if (!found) {
await this.sendAlert({
type: 'CONTENT_MISSING',
page: name,
message: `未找到预期内容: ${content}`,
url,
});
}
}
// 检测页面变化
if (this.baseline[name]) {
const changes = await this.detectChanges(name);
if (changes.length > 0) {
await this.sendAlert({
type: 'PAGE_CHANGED',
page: name,
changes,
url,
});
}
}
// 更新基准数据
await this.updateBaseline(name);
} catch (error) {
await this.sendAlert({
type: 'ERROR',
page: name,
message: error.message,
url,
});
} finally {
await this.stagehand.close();
}
}
async checkAccessibility() {
try {
const title = await this.stagehand.title();
return title && title.length > 0;
} catch {
return false;
}
}
async checkContent(expectedContent) {
const pageContent = await this.stagehand.extract({
query: `页面是否包含"${expectedContent}"?`,
type: 'string',
});
return pageContent.toLowerCase().includes(expectedContent.toLowerCase());
}
async detectChanges(pageName) {
const currentSnapshot = await this.takeSnapshot();
const baselineSnapshot = this.baseline[pageName];
const changes = [];
// 比较快照
if (baselineSnapshot) {
if (currentSnapshot.title !== baselineSnapshot.title) {
changes.push({
type: 'TITLE_CHANGED',
from: baselineSnapshot.title,
to: currentSnapshot.title,
});
}
if (currentSnapshot.content !== baselineSnapshot.content) {
changes.push({
type: 'CONTENT_CHANGED',
from: baselineSnapshot.content.substring(0, 200),
to: currentSnapshot.content.substring(0, 200),
});
}
}
return changes;
}
async takeSnapshot() {
const title = await this.stagehand.title();
const content = await this.stagehand.extract({
query: '页面的主要内容是什么?',
type: 'string',
});
return { title, content, timestamp: Date.now() };
}
async updateBaseline(pageName) {
this.baseline[pageName] = await this.takeSnapshot();
// 保存基准到文件
const fs = require('fs');
fs.writeFileSync(
'./baseline.json',
JSON.stringify(this.baseline, null, 2)
);
}
// 开始监控
async start() {
console.log('开始网页监控...');
// 立即执行一次检查
for (const page of this.config.pages) {
await this.checkPage(page);
}
// 设置定时检查
schedule.scheduleJob(this.config.checkInterval, async () => {
for (const page of this.config.pages) {
await this.checkPage(page);
}
});
}
}
// 配置告警方式
async function emailAlert(alert) {
// 发送邮件通知
console.log('📧 发送邮件告警...');
}
async function webhookAlert(alert) {
// 发送webhook通知
console.log('📡 发送webhook通知...');
}
// 使用示例
async function main() {
const monitor = new WebMonitor({
checkInterval: '*/15 * * * *', // 每15分钟检查一次
pages: [
{
name: '产品页面',
url: 'https://example-site.com/product/123',
expectedContent: ['价格', '库存', '添加到购物车'],
},
{
name: '新闻页面',
url: 'https://news-example.com/latest',
expectedContent: [],
},
],
});
// 注册告警方式
monitor.onAlert(emailAlert);
monitor.onAlert(webhookAlert);
await monitor.start();
}
main();
6.3 数据采集与ETL
构建自动化的数据采集管道。
// data-pipeline.js
const { Stagehand } = require('stagehand');
const { Transform } = require('stream');
class DataPipeline {
constructor() {
this.stagehand = new Stagehand({
model: 'gpt-4o',
headless: true,
});
this.data = [];
this.transformers = [];
}
// 添加数据转换器
addTransformer(name, transformFn) {
this.transformers.push({ name, fn: transformFn });
return this;
}
// 数据采集阶段
async extract(url, options = {}) {
console.log(`从 ${url} 提取数据...`);
await this.stagehand.goto(url);
await this.stagehand.wait(options.waitFor || 'domcontentloaded');
const rawData = await this.stagehand.extract(options.query || {
query: '提取所有数据',
type: 'array',
});
console.log(`提取到 ${rawData.length} 条原始数据`);
this.data.push(...rawData);
return this;
}
// 数据转换阶段
async transform(data) {
let transformedData = [...data];
for (const transformer of this.transformers) {
console.log(`应用转换: ${transformer.name}`);
transformedData = await transformer.fn(transformedData);
}
return transformedData;
}
// 加载数据到目标
async load(data, target) {
switch (target.type) {
case 'json':
await this.loadToJsonFile(data, target.path);
break;
case 'csv':
await this.loadToCsv(data, target.path);
break;
case 'database':
await this.loadToDatabase(data, target);
break;
case 'api':
await this.loadToApi(data, target);
break;
default:
console.log('未知的目标类型');
}
}
async loadToJsonFile(data, filepath) {
const fs = require('fs').promises;
await fs.writeFile(filepath, JSON.stringify(data, null, 2));
console.log(`数据已保存到 ${filepath}`);
}
async loadToCsv(data, filepath) {
// 简单的CSV转换
if (data.length === 0) return;
const headers = Object.keys(data[0]);
const csvRows = [
headers.join(','),
...data.map(row =>
headers.map(h => JSON.stringify(row[h] ?? '')).join(',')
),
];
const fs = require('fs').promises;
await fs.writeFile(filepath, csvRows.join('\n'));
console.log(`数据已保存到 ${filepath}`);
}
async loadToDatabase(data, config) {
console.log(`正在写入数据库: ${config.connectionString}`);
// 实现数据库写入逻辑
}
async loadToApi(data, config) {
console.log(`正在推送到API: ${config.endpoint}`);
// 实现API推送逻辑
}
// 完整的ETL流程
async runETL(config) {
console.log('===== 开始ETL流程 =====');
console.log(`时间: ${new Date().toISOString()}`);
try {
await this.stagehand.init();
// Extract - 采集多个数据源
for (const source of config.sources) {
await this.extract(source.url, source.options);
}
// Transform - 数据转换
const transformedData = await this.transform(this.data);
// Load - 加载到目标
await this.load(transformedData, config.target);
console.log('\n===== ETL流程完成 =====');
console.log(`处理记录数: ${transformedData.length}`);
return {
success: true,
recordCount: transformedData.length,
timestamp: new Date().toISOString(),
};
} catch (error) {
console.error('ETL流程失败:', error);
throw error;
} finally {
await this.stagehand.close();
}
}
async close() {
await this.stagehand.close();
}
}
// 使用示例
async function main() {
const pipeline = new DataPipeline();
// 添加数据转换器
pipeline.addTransformer('去重', (data) => {
const seen = new Set();
return data.filter(item => {
const key = JSON.stringify(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
});
pipeline.addTransformer('数据清洗', (data) => {
return data.map(item => ({
...item,
// 清理数据
title: item.title?.trim(),
price: parseFloat(item.price?.replace(/[^0-9.]/g, '')) || 0,
updatedAt: new Date().toISOString(),
}));
});
pipeline.addTransformer('分类标记', (data) => {
return data.map(item => ({
...item,
category: item.title?.includes('手机') ? '电子产品' : '其他',
priority: item.price > 1000 ? 'HIGH' : 'NORMAL',
}));
});
// 运行ETL
const result = await pipeline.runETL({
sources: [
{
url: 'https://shop-example.com/products',
options: {
query: '提取所有商品信息',
waitFor: 'networkidle',
},
},
],
target: {
type: 'json',
path: './output/products.json',
},
});
console.log('ETL结果:', result);
await pipeline.close();
}
main();
第七部分:常见问题与解决方案
7.1 网络与连接问题
// 网络问题处理示例
// 问题1:超时处理
async function handleTimeouts() {
const stagehand = new Stagehand({
model: 'gpt-4o',
timeout: {
default: 30000,
navigation: 60000,
action: 15000,
extraction: 20000,
},
});
try {
await stagehand.init();
// 使用更长的超时时间
await stagehand.goto('https://slow-site.com', {
timeout: 120000,
});
} catch (error) {
if (error.message.includes('timeout')) {
console.log('页面加载超时,尝试重试...');
// 添加重试逻辑
}
}
}
// 问题2:代理设置
async function useProxy() {
const stagehand = new Stagehand({
model: 'gpt-4o',
// 代理配置
proxy: {
server: 'http://proxy-server:8080',
username: 'proxy-user',
password: 'proxy-pass',
},
});
}
// 问题3:SSL证书问题
async function handleSSLErrors() {
const stagehand = new Stagehand({
model: 'gpt-4o',
ignoreHTTPSErrors: true, // 忽略SSL错误
});
}
7.2 页面元素问题
// 元素问题处理示例
// 问题1:元素不可见
async function handleInvisibleElements() {
const stagehand = new Stagehand({
model: 'gpt-4o',
});
await stagehand.goto('https://example.com');
// 先滚动到元素可见
await stagehand.act('滚动到页面底部使隐藏按钮可见');
// 或者使用JavaScript滚动
await stagehand.evaluate(() => {
const element = document.querySelector('.lazy-button');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
await stagehand.act('点击那个按钮');
}
// 问题2:元素被遮挡
async function handleObstructedElements() {
const stagehand = new Stagehand({
model: 'gpt-4o',
});
// 关闭可能遮挡的弹窗
await stagehand.act('关闭任何弹窗或遮罩层');
// 强制点击(通过坐标)
await stagehand.evaluate(() => {
const button = document.querySelector('.target-button');
if (button) {
const rect = button.getBoundingClientRect();
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
});
button.dispatchEvent(event);
}
});
}
// 问题3:动态加载的内容
async function handleDynamicContent() {
const stagehand = new Stagehand({
model: 'gpt-4o',
});
await stagehand.goto('https://example.com');
// 等待特定内容出现
await stagehand.wait('目标内容加载完成', {
timeout: 30000,
polling: 1000, // 轮询间隔
});
// 或者轮询检查
const contentLoaded = await stagehand.evaluate(() => {
return document.querySelector('.dynamic-content') !== null;
});
if (!contentLoaded) {
// 触发加载
await stagehand.act('触发内容加载');
await stagehand.wait('内容出现');
}
}
7.3 AI模型相关问题
// AI模型问题处理示例
// 问题1:API限流
async function handleRateLimiting() {
const stagehand = new Stagehand({
model: 'gpt-4o',
});
// 实现简单的限流器
const rateLimiter = {
queue: [],
processing: false,
lastRequestTime: 0,
minInterval: 1000, // 最小请求间隔
async throttle() {
const now = Date.now();
const elapsed = now - this.lastRequestTime;
if (elapsed < this.minInterval) {
await new Promise(resolve =>
setTimeout(resolve, this.minInterval - elapsed)
);
}
this.lastRequestTime = Date.now();
},
};
// 在每次操作前调用
await rateLimiter.throttle();
await stagehand.act('执行操作');
}
// 问题2:模型响应不稳定
async function handleUnstableResponses() {
const stagehand = new Stagehand({
model: 'gpt-4o',
// 增加温度参数以获得更一致的输出
temperature: 0.1,
});
// 或者使用few-shot提示
await stagehand.act('提取商品信息', {
instruction: `
请按以下格式提取商品信息:
{
"name": "商品名称",
"price": "商品价格",
"rating": "评分(如4.5)"
}
示例:
输入页面包含"iPhone 15,价格5999元,评分4.8"
输出应为:
{"name": "iPhone 15", "price": "5999元", "rating": "4.8"}
`,
});
}
// 问题3:模型无法理解复杂页面
async function handleComplexPages() {
const stagehand = new Stagehand({
model: 'gpt-4o',
});
await stagehand.goto('https://complex-page.com');
// 分步骤处理复杂页面
const step1 = await stagehand.extract('提取第一部分内容');
await stagehand.act('滚动到第二部分');
const step2 = await stagehand.extract('提取第二部分内容');
await stagehand.act('滚动到第三部分');
const step3 = await stagehand.extract('提取第三部分内容');
// 合并结果
const fullContent = [step1, step2, step3];
}
第八部分:最佳实践总结
8.1 代码组织建议
良好的代码组织可以提高项目的可维护性和可扩展性。
// 推荐的目录结构
// project/
// ├── src/
// │ ├── actions/ # 封装通用操作
// │ │ ├── login.js
// │ │ ├── navigation.js
// │ │ └── forms.js
// │ ├── extractors/ # 数据提取器
// │ │ ├── products.js
// │ │ ├── articles.js
// │ │ └── users.js
// │ ├── hooks/ # 生命周期钩子
// │ │ ├── beforeAction.js
// │ │ └── afterAction.js
// │ ├── utils/ # 工具函数
// │ │ ├── retry.js
// │ │ ├── logger.js
// │ │ └── validator.js
// │ ├── config/ # 配置文件
// │ │ └── environments.js
// │ └── tasks/ # 任务定义
// │ ├── scrapeProducts.js
// │ └── monitorPrices.js
// ├── tests/ # 测试文件
// ├── screenshots/ # 截图保存
// ├── logs/ # 日志文件
// └── package.json
// ============================================
// 通用操作封装示例
// ============================================
// src/actions/login.js
const { Stagehand } = require('stagehand');
class LoginActions {
constructor(stagehand) {
this.stagehand = stagehand;
}
async login(credentials) {
const { username, password, rememberMe = false } = credentials;
await this.stagehand.goto(credentials.loginUrl || 'https://example.com/login');
await this.stagehand.act(`填写用户名: ${username}`);
await this.stagehand.act(`填写密码: ${password}`);
if (rememberMe) {
await this.stagehand.act('勾选"记住我"');
}
await this.stagehand.act('点击登录按钮');
// 验证登录结果
const loginSuccess = await this.verifyLogin();
if (!loginSuccess) {
throw new Error('登录验证失败');
}
return true;
}
async verifyLogin() {
try {
await this.stagehand.wait('登录后的用户信息出现', { timeout: 5000 });
return true;
} catch {
// 检查是否有错误提示
const errorMessage = await this.stagehand.extract({
query: '页面上是否显示登录错误信息?',
type: 'string',
});
if (errorMessage.includes('错误') || errorMessage.includes('失败')) {
throw new Error(`登录失败: ${errorMessage}`);
}
return false;
}
}
async logout() {
await this.stagehand.act('点击用户头像');
await this.stagehand.act('点击"退出登录"');
await this.stagehand.wait('已退出登录');
}
}
module.exports = { LoginActions };
8.2 性能优化建议
优化Stagehand脚本的性能可以显著提高执行效率。
// 性能优化建议
// 1. 减少不必要的页面加载
// 不好:每次都加载整个页面
await stagehand.goto('https://example.com');
await stagehand.act('点击链接A');
await stagehand.goto('https://example.com/page2'); // 重新加载
await stagehand.act('点击链接B');
// 好:保持在当前页面导航
await stagehand.goto('https://example.com');
await stagehand.act('点击链接A');
await stagehand.act('点击链接B');
// 2. 批量提取数据
// 不好:多次单独提取
const title = await stagehand.extract('提取标题');
const price = await stagehand.extract('提取价格');
const rating = await stagehand.extract('提取评分');
// 好:一次性提取多个字段
const data = await stagehand.extract({
query: '提取标题、价格、评分',
type: 'object',
});
// 3. 使用无头模式处理不需要可视化的任务
const stagehand = new Stagehand({
model: 'gpt-4o',
headless: true, // 启用无头模式
});
// 4. 合理设置超时
await stagehand.goto(url, {
timeout: 30000, // 不要设置过长
waitUntil: 'domcontentloaded', // 使用合适的等待策略
});
// 5. 使用条件等待而不是固定延迟
// 不好:
await stagehand.act('点击加载按钮');
await new Promise(r => setTimeout(r, 5000)); // 浪费等待时间
// 好:
await stagehand.act('点击加载按钮');
await stagehand.wait('新内容加载完成'); // 刚好够用
// 6. 复用浏览器上下文
// 不好:每个任务都创建新实例
for (const task of tasks) {
const stagehand = new Stagehand({...});
await stagehand.init();
await task(stagehand);
await stagehand.close();
}
// 好:复用上下文
const stagehand = new Stagehand({...});
await stagehand.init();
for (const task of tasks) {
await task(stagehand);
}
await stagehand.close();
8.3 安全注意事项
// 安全最佳实践
// 1. 保护敏感凭据
// 不好:硬编码密码
const password = 'mySecretPassword';
// 好:使用环境变量
const password = process.env.SITE_PASSWORD;
// 好:使用加密的配置文件
const credentials = require('./encrypted-config.json');
// 2. 不要在日志中记录敏感信息
// 不好:
console.log(`登录: ${username} / ${password}`);
// 好:
console.log(`登录: ${username} / *****`);
// 3. 使用HTTPS
await stagehand.goto('https://secure-site.com');
// 4. 验证SSL证书(在必要时)
const stagehand = new Stagehand({
// 只有在明确知道目标站点的情况下才忽略证书错误
ignoreHTTPSErrors: false,
});
// 5. 限制操作范围
// 为自动化脚本设置合理的权限
await stagehand.context.grantPermissions(['notifications'], {
origin: 'https://trusted-site.com',
});
// 6. 防止XSS攻击
// 如果提取的数据用于后续处理,确保进行转义
const safeText = text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
结语:拥抱AI驱动的浏览器自动化
经过这篇详尽的教程,你应该已经对Stagehand有了全面的了解。从基础的环境搭建到复杂的实战应用,从简单的单页面操作到大规模的数据采集管道,Stagehand都展现出了强大的能力和极高的灵活性。
Stagehand的核心价值在于将AI的自然语言理解能力与浏览器自动化相结合,让你从繁琐的元素定位和复杂的交互逻辑中解放出来,专注于业务流程本身。这种”声明式”的自动化方式不仅提高了开发效率,还大大增强了脚本的健壮性和可维护性。
Stagehand适用的典型场景
- 数据采集与监控:定期采集电商、新闻、社交媒体等平台的数据
- 自动化测试:编写端到端测试用例,用自然语言描述测试步骤
- RPA流程:自动化日常办公任务,如表单填写、报表生成
- 网页内容分析:批量分析网页结构、提取结构化数据
- 原型验证:快速验证网页交互逻辑是否满足需求
进一步学习的方向
- 深入了解Playwright的底层API,作为Stagehand的补充
- 学习AI提示工程,优化与AI模型的交互效果
- 探索多模态AI模型在浏览器自动化中的更多应用
- 研究分布式爬虫架构,构建大规模的自动化系统
推荐的进阶学习资源
| 资源类型 | 推荐内容 |
|---|---|
| Stagehand官方文档 | https://docs.browserbase.com/stagehand |
| Playwright | https://playwright.dev/docs/intro |
| AI提示工程 | OpenAI Cookbook |
| 分布式爬虫 | Scrapy + Redis 架构设计 |
希望这篇教程能帮助你快速上手Stagehand,并在实际项目中发挥它的威力。如果你有任何问题或想法,欢迎在评论区与我交流!
相关链接
- GitHub仓库:https://github.com/browserbase/stagehand
- 官方文档:https://docs.browserbase.com/stagehand
- Browserbase平台:https://www.browserbase.com
- NPM包:https://www.npmjs.com/package/stagehand
评论区