4、Puppeteer 页面交互:元素定位、点击与输入、表单操作

好,咱们进入正题。页面交互这块,说白了就是让 Puppeteer 像真人一样操作浏览器。你想想看,光能打开页面有啥用?得能点按钮、填表单、选选项,这才叫自动化。

我个人习惯把页面交互分成三步走:先找到元素,再操作它,最后验证结果。今天咱们就把前两步聊透。

4.1 元素定位:CSS 选择器与 XPath

定位元素是基本功。Puppeteer 支持两种主流方式:CSS 选择器和 XPath。我刚开始用的时候,总觉得 XPath 更强大,后来发现其实 CSS 选择器在 90% 的场景下都够用,而且速度更快。

4.1.1 CSS 选择器

Puppeteer 的 page.$()page.$$() 就是干这个的。前者返回第一个匹配元素,后者返回所有匹配元素。

// 获取单个元素
const button = await page.$('#submit-btn');

// 获取所有列表项
const items = await page.$$('ul.list > li');

// 更复杂的组合选择器
const firstRow = await page.$('table tbody tr:first-child');

我在项目中遇到过一个问题:有些页面用了动态 class 名称,比如 class="btn_3a4f2k",这种用 CSS 选择器就不好使了。这时候我一般改用属性选择器或者 XPath。

小技巧:page.waitForSelector() 代替直接定位。它能等元素出现后再操作,避免页面还没渲染完就报错。

4.1.2 XPath

XPath 的定位能力更强,尤其适合复杂条件。Puppeteer 里用 page.$x() 来执行 XPath 查询。

// 根据文本内容定位
const [element] = await page.$x('//button[contains(text(), "提交")]');

// 根据层级关系定位
const [input] = await page.$x('//div[@class="form-group"]//input[@type="text"]');

// 更复杂的逻辑
const [link] = await page.$x('//a[starts-with(@href, "/download") and not(@target)]');

嗯,这里要注意:$x() 返回的是数组,哪怕只有一个结果。我经常忘了加解构赋值,结果拿到的是一整个数组,操作半天报错才反应过来。

避坑指南:XPath 在 Shadow DOM 下不好使。我曾经花了一下午调试一个弹窗里的按钮,死活定位不到,最后发现是 Shadow DOM 的问题。遇到这种情况,改用 page.evaluate() 直接操作 DOM 更靠谱。

4.2 点击与输入

定位到元素之后,最常用的操作就是点击和输入。Puppeteer 提供了很直观的 API。

4.2.1 点击操作

// 最基础的点击
await page.click('#login-btn');

// 带延迟的点击,模拟真人操作
await page.click('#submit-btn', { delay: 100 });

// 点击坐标位置
await page.mouse.click(100, 200);

为什么要点击要加 delay?说白了,有些网站会检测操作速度。真人点击不可能瞬间完成,加个 50-100 毫秒的延迟,能有效降低被反爬的风险。

4.2.2 输入文本

// 清空并输入
await page.type('#username', 'admin');

// 带延迟的输入,更像真人打字
await page.type('#password', 'mypassword', { delay: 30 });

// 更精细的控制:先清空再输入
await page.$eval('#email', el => el.value = '');
await page.type('#email', 'test@example.com');

我记得有一次做自动化测试,输入框总是少几个字符。排查了半天,发现是页面有输入事件监听,输入太快导致事件没触发完整。加上 delay: 30 之后,问题就解决了。

核心要点:能用 page.type() 就别用 page.$eval() 直接赋值。前者会触发键盘事件,后者只是改了 DOM 属性,很多前端框架不认。

4.3 表单操作

表单操作是数据采集的重头戏。登录、注册、搜索,哪个都离不开表单。

4.3.1 完整表单填写

// 一个典型的登录表单操作
await page.type('#username', 'myuser', { delay: 20 });
await page.type('#password', 'mypass123', { delay: 20 });
await page.click('#login-btn');

// 等待登录完成
await page.waitForNavigation();

你想想看,如果只是填个表单,那太简单了。实际项目中,表单往往有各种验证:必填项、格式校验、验证码……

4.3.2 处理验证码

验证码是个头疼的问题。我的经验是:能不处理就不处理。比如有些网站的验证码只在连续失败后才出现,那我们可以控制登录频率,避免触发验证码。

如果实在绕不过去,可以考虑接入打码平台,或者用 OCR 识别。但说实话,这两种方式都不太稳定。

注意:别在表单提交后立刻进行下一步操作。一定要等页面跳转或出现成功提示。我习惯用 page.waitForSelector() 或者 page.waitForResponse() 来确认操作成功。

4.4 下拉框与复选框处理

这两种控件在表单里很常见,处理方式也各有特点。

4.4.1 下拉框(Select)

// 通过 value 选择
await page.select('#city', 'beijing');

// 通过 label 文本选择
await page.select('#city', '北京');

// 多选下拉框
await page.select('#hobbies', ['reading', 'music']);

下拉框看似简单,但有个坑:page.select() 只对原生的 <select> 元素有效。现在很多前端框架用自定义下拉框(比如用 div 模拟的),那就得用点击的方式处理了。

// 自定义下拉框的处理方式
await page.click('#custom-select');
await page.waitForSelector('.dropdown-item');
await page.click('.dropdown-item[data-value="beijing"]');

4.4.2 复选框与单选框

// 勾选复选框
await page.click('#agree-checkbox');

// 判断是否已勾选
const isChecked = await page.$eval('#agree-checkbox', el => el.checked);
if (!isChecked) {
  await page.click('#agree-checkbox');
}

// 单选框选择
await page.click('input[name="gender"][value="male"]');

我曾经踩过一个坑:复选框的点击事件绑定了两次,导致点击一次后状态来回切换。后来我改用 page.evaluate() 直接设置 checked 属性,绕过了事件绑定。

经验之谈:处理复选框时,先判断状态再操作,比直接点击更可靠。尤其是那些「同意协议」之类的复选框,默认可能是勾选状态,你一点反而取消了。

4.5 综合示例:自动注册流程

来,咱们把今天学的串起来,写一个完整的注册流程自动化脚本。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.goto('https://example.com/register');
  
  // 填写基本信息
  await page.type('#username', 'test_user', { delay: 30 });
  await page.type('#email', 'test@example.com', { delay: 30 });
  await page.type('#password', 'Pass123!', { delay: 30 });
  
  // 选择城市(原生下拉框)
  await page.select('#city', 'shanghai');
  
  // 勾选同意协议
  const agreeChecked = await page.$eval('#agree', el => el.checked);
  if (!agreeChecked) {
    await page.click('#agree');
  }
  
  // 点击注册按钮
  await page.click('#register-btn');
  
  // 等待注册成功提示
  await page.waitForSelector('.success-message', { timeout: 5000 });
  
  console.log('注册成功!');
  
  await browser.close();
})();

这个例子涵盖了今天讲的大部分内容:元素定位、输入、下拉框、复选框、点击。你想想看,如果把这些操作组合起来,能做的事情就多了去了。

总结一下:页面交互的核心就三点——
1. 用对定位方式(CSS 选择器优先,XPath 做补充)
2. 操作要模拟真人(加延迟、等加载)
3. 处理特殊情况(自定义控件、验证码、事件绑定)

下节课咱们聊聊更高级的话题:如何处理弹窗、文件上传、以及页面滚动。这些在实际项目中也是高频需求。