网页抓取新姿势:OpenClaw浏览器控制详解

科技2小时前发布 muybien
1 0 0

网页抓取新姿势:OpenClaw浏览器控制详解

“`html

一、传统爬虫的困境:为什么你的爬虫总是被封

做网页数据采集的朋友大概都遇到过这种场景:用requests写好的爬虫跑得好好的,突然某天全部请求返回403;辛辛苦苦模拟登录成功,结果下一秒访问其他页面就被踢回登录页;好不容易抓到的数据,因为网站全站改成React/Vue框架渲染,HTML里只剩下一个空的div。

这些问题不是你的代码写得不够好,而是传统HTTP请求式的爬虫天然存在缺陷。网站越来越”聪明”,它们用JavaScript动态渲染页面、用浏览器指纹识别机器人、用验证码和行为分析来判断访问者是人还是程序。requests发出去的请求在服务器眼里就是一张”无脸照”,而真正的浏览器却能”刷脸入场”。

OpenClaw正是为解决这些问题而生的。它本质上是一个高级浏览器自动化工具,通过程序直接控制真实的Chrome浏览器内核执行操作,让网站无法从技术层面区分自动化访问和真实用户访问。不只是”能加载JS”这么简单——它能完整保留登录态、精准填写表单、智能处理弹窗、甚至模拟多账号并发操作。

二、5分钟快速上手:安装与第一个自动化脚本

安装OpenClaw环境

OpenClaw基于Node.js生态,安装前请确保本地有Node.js环境(v16以上)。在项目目录下执行安装命令:

npm install openclaw --save
npm install puppeteer-core --save  # OpenClaw依赖的底层驱动

如果你需要完整的浏览器内核(首次安装会下载约150MB的Chrome),使用:

npm install openclaw --save
npx openclaw install chrome  # 安装完整Chrome浏览器

写一个最基本的脚本

打开百度、搜索关键词、截图保存——这是浏览器自动化的”Hello World”。完整代码如下:

const { OpenClaw } = require('openclaw');

(async () => {
  const browser = await OpenClaw.launch({
    headless: false,  // 生产环境设为true
    userDataDir: './browser-data',  // 保留浏览器配置
  });

  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  
  // 搜索框输入内容
  await page.type('#kw', 'OpenClaw浏览器自动化');
  await page.click('#su');
  
  // 等待搜索结果加载
  await page.waitForSelector('.result');
  
  // 截图保存
  await page.screenshot({ path: 'search-result.png', fullPage: true });
  
  console.log('截图已保存');
  await browser.close();
})();

这段代码展示了OpenClaw的核心逻辑:启动浏览器 → 创建页面 → 执行操作 → 关闭资源。注意到userDataDir参数了吗?这是保持登录态的关键所在。

三、实战一:自动登录与表单批量填写

模拟登录并保持会话

很多数据采集场景需要先登录账号。传统做法是抓包分析登录接口、用requests模拟POST请求——这种方式在验证码、短信登录、风控检测面前几乎束手无策。用OpenClaw模拟真人操作登录,就能绕过这些障碍。

const { OpenClaw } = require('openclaw');
const fs = require('fs');

(async () => {
  const browser = await OpenClaw.launch({
    userDataDir: './user-session-001',
    headless: false,
  });

  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
  });

  const page = await context.newPage();

  // 打开登录页
  await page.goto('https://example.com/login');
  
  // 填写用户名密码(选择器根据实际页面调整)
  await page.fill('input[name="username"]', 'your_account');
  await page.fill('input[name="password"]', 'your_password');
  
  // 点击登录按钮
  await page.click('button[type="submit"]');
  
  // 等待登录成功后的跳转
  await page.waitForURL('**/dashboard**', { timeout: 10000 });
  
  // 保存登录状态(后续脚本直接复用userDataDir即可保持登录)
  console.log('登录成功,当前页面:', page.url());
  
  // 后续操作...
  
  await browser.close();
})();

关键点在于:第一次登录时指定userDataDir路径,后续脚本复用这个路径打开浏览器时,之前登录的Cookie和Session都会被保留。这样做的好处是避免每次都重新登录,也不需要手动处理Token传递。

批量填写表单:抓取房产信息实战

比登录更常见的需求是批量填写表单。比如某房产平台需要逐个查询小区房价,我们用OpenClaw实现自动化查询:

const { OpenClaw } = require('openclaw');

const districts = ['浦东新区', '徐汇区', '静安区', '黄浦区'];

(async () => {
  const browser = await OpenClaw.launch({ headless: true });
  const page = await browser.newPage();
  
  const results = [];
  
  for (const district of districts) {
    try {
      await page.goto('https://example-house.com/search');
      
      // 清除之前的输入
      await page.click('input#district-input', { clickCount: 3 });
      await page.keyboard.press('Backspace');
      
      // 输入区域名称
      await page.fill('input#district-input', district);
      
      // 点击搜索
      await page.click('button.search-btn');
      
      // 等待结果加载
      await page.waitForSelector('.house-list .house-item', { timeout: 5000 });
      
      // 提取数据
      const items = await page.$$eval('.house-item', els => {
        return els.slice(0, 10).map(el => ({
          title: el.querySelector('.title')?.innerText,
          price: el.querySelector('.price')?.innerText,
          area: el.querySelector('.area')?.innerText,
        }));
      });
      
      results.push({ district, items });
      console.log(`${district} 查询完成,获取 ${items.length} 条数据`);
      
    } catch (err) {
      console.error(`${district} 查询失败:`, err.message);
    }
  }
  
  // 保存结果
  const fs = require('fs');
  fs.writeFileSync('house-data.json', JSON.stringify(results, null, 2));
  
  await browser.close();
  console.log('全部查询完成,结果已保存');
})();

这个脚本循环查询四个区域、每个区域取前10条房源信息。用try-catch包裹每个查询步骤,即使某个区域查询失败也不会中断整个流程。真实项目中建议加入随机延时(await page.waitForTimeout(randomDelay()))模拟人类操作节奏,降低被识别为机器人的概率。

四、实战二:价格监控与数据自动采集

电商价格监控

价格监控是浏览器自动化的经典应用场景。比价网站、促销活动追踪、竞品价格分析——这些需求用传统爬虫实现需要处理复杂的反爬机制,而用OpenClaw可以直接”看”到页面内容。

const { OpenClaw } = require('openclaw');

class PriceMonitor {
  constructor() {
    this.browser = null;
    this.targets = [
      { name: 'iPhone15 Pro', url: 'https://example-shop.com/product/iphone15pro' },
      { name: 'MacBook Air M3', url: 'https://example-shop.com/product/macbook-air-m3' },
    ];
  }

  async start() {
    this.browser = await OpenClaw.launch({ headless: true });
    
    const results = [];
    for (const target of this.targets) {
      const price = await this.getPrice(target.url);
      results.push({
        name: target.name,
        price,
        timestamp: new Date().toISOString(),
      });
      console.log(`${target.name}: ¥${price}`);
    }
    
    await this.browser.close();
    return results;
  }

  async getPrice(url) {
    const page = await this.browser.newPage();
    
    // 设置额外的请求头
    await page.setExtraHTTPHeaders({
      'Accept-Language': 'zh-CN,zh;q=0.9',
    });
    
    await page.goto(url, { waitUntil: 'networkidle2' });
    
    // 等待价格元素加载(可能有多处价格,取主价格)
    await page.waitForSelector('.product-price', { timeout: 10000 });
    
    // 提取价格文本
    const priceText = await page.$eval('.product-price', el => el.innerText);
    
    // 清洗价格数据(去掉"¥"符号和逗号)
    const price = parseFloat(priceText.replace(/[¥,]/g, ''));
    
    await page.close();
    return price;
  }
}

// 执行监控
const monitor = new PriceMonitor();
const data = await monitor.start();
console.log('监控数据:', data);

这个监控类的设计思路值得借鉴:把浏览器实例作为类属性复用,每个目标单独开一个page处理,任务完成后统一关闭。这样既保证了并发效率,又避免了资源泄漏。把这段代码配合cron定时任务,就能实现每日自动价格采集。

登录态维持的进阶技巧

前面提到用userDataDir保持登录态,但实际场景更复杂:Token过期怎么办?多账号怎么管理?这里介绍一个更稳健的方案:

const { OpenClaw } = require('openclaw');
const fs = require('fs');

// 管理多个账号的登录状态
class SessionManager {
  constructor() {
    this.sessionDir = './sessions';
    if (!fs.existsSync(this.sessionDir)) {
      fs.mkdirSync(this.sessionDir, { recursive: true });
    }
  }

  // 获取或创建指定账号的浏览器上下文
  async getContext(accountId) {
    const browser = await OpenClaw.launch({
      userDataDir: `${this.sessionDir}/${accountId}`,
      headless: true,
    });
    
    const context = await browser.newContext();
    const page = await context.newPage();
    
    // 检查登录状态
    await page.goto('https://example.com/api/check-login');
    const loginStatus = await page.evaluate(() => {
      return document.body.innerText.includes('"loggedIn":true');
    });
    
    if (!loginStatus) {
      console.log(`账号 ${accountId} 未登录或登录已过期`);
      await browser.close();
      return null;
    }
    
    return { browser, context, page };
  }
}

// 使用示例
(async () => {
  const manager = new SessionManager();
  
  const session1 = await manager.getContext('user_001');
  if (session1) {
    await session1.page.goto('https://example.com/user-center');
    // 进行需要登录的操作
    await session1.browser.close();
  }
})();

这个方案的核心思路是:每个账号独立一个userDataDir目录,首次登录后状态会被持久化保存。定期运行”检查登录状态”的脚本,如果发现登录失效,就重新执行登录流程并更新目录。这样可以确保长期稳定运行。

五、批量采集的最佳实践与性能优化

并发控制与资源管理

批量采集时很多人会陷入两个极端:串行执行太慢、并发太高被封。这里给出一个经过验证的并发控制模式:

const { OpenClaw } = require('openclaw');

// 并发控制器:控制同时打开的浏览器数量
class ConcurrencyController {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
  }

  async run(task) {
    return new Promise((resolve, reject) => {
      const execute = async () => {
        this.running++;
        try {
          const result = await task();
          resolve(result);
        } catch (err) {
          reject(err);
        } finally {
          this.running--;
          this.processQueue();
        }
      };

      if (this.running < this.maxConcurrent) {
        execute();
      } else {
        this.queue.push(execute);
      }
    });
  }

  processQueue() {
    if (this.queue.length > 0) {
      const next = this.queue.shift();
      next();
    }
  }
}

// 批量采集函数
async function scrapeAll(urls) {
  const controller = new ConcurrencyController(3);  // 最多3个并发
  const browser = await OpenClaw.launch({ headless: true });
  
  const tasks = urls.map((url, index) => async () => {
    console.log(`开始采集: ${url} (并发数: ${controller.running})`);
    
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    
    // 提取数据
    const data = await page.evaluate(() => {
      return {
        title: document.querySelector('h1')?.innerText,
        content: document.querySelector('.content')?.innerText,
      };
    });
    
    await page.close();
    
    // 模拟人类操作间隔(随机1-3秒)
    await new Promise(r => setTimeout(r, 1000 + Math.random() * 2000));
    
    return data;
  });
  
  const results = await Promise.all(tasks.map(t => controller.run(t)));
  
  await browser.close();
  return results;
}

这个并发控制器的设计参考了计算机科学中的信号量(Semaphore)模式。通过限制同时运行的浏览器实例数量,既能保证采集效率,又不会因为并发过高触发网站的反爬机制。配合随机延时,真实用户很难分辨这是机器人还是真人操作。

错误处理与断点续采

大规模采集任务必须考虑容错。一个设计良好的采集器应该能做到:单条失败不影响全局、任务中断后能断点续采。

const fs = require('fs');

class ResumableScraper {
constructor(outputFile) {
this.outputFile = outputFile;
this.processedFile = outputFile + '.processed';
}

// 读取已处理列表
getProcessedIds() {
if (fs.existsSync(this.processedFile)) {
return new Set(fs.readFileSync(this.processedFile, 'utf-8').split('\n').filter(Boolean));
}
return new Set();
}

// 标记已处理
markProcessed(id) {
fs.appendFileSync(this.processedFile, id + '\n');
}

// 保存结果(追加模式)
saveResult(result) {
const line = JSON.stringify(result) + '\n';
fs.appendFileSync(this.outputFile, line);
}
}

// 使用示例
(async () => {
const scraper = new ResumableScraper('results.jsonl');
const processed = scraper.getProcessedIds();

const allUrls = [
'https://example.com/item/1',
'https://example.com/item/2',
// ... 更多URL
];

const browser = await OpenClaw.launch({ headless: true });

for (const url of allUrls) {
const itemId = url.split('/').pop();

// 跳过已处理的
if (processed.has(itemId)) {
console.log(`跳过: ${itemId}`);
continue;
}

try {
const page = await browser.newPage();
await page.goto(url, { timeout: 15000 });

const data = await page.evaluate(() => {
return { /*

© 版权声明

相关文章

暂无评论

none
暂无评论...