别再手动写爬虫了!AI驱动的浏览器自动化神器,让你的效率提升10倍

别再手动写爬虫了!AI驱动的浏览器自动化神器,让你的效率提升10倍

别再手动写爬虫了!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方法的底层工作流程:

  1. 截取当前页面截图
  2. 将截图和指令发送给AI模型
  3. AI分析页面结构,理解指令意图
  4. 确定需要操作的元素及其操作方式
  5. 生成Playwright代码并执行
  6. 验证操作是否成功
// 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, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;');

结语:拥抱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

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注