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() 返回的是数组,哪怕只有一个结果。我经常忘了加解构赋值,结果拿到的是一整个数组,操作半天报错才反应过来。
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. 处理特殊情况(自定义控件、验证码、事件绑定)
下节课咱们聊聊更高级的话题:如何处理弹窗、文件上传、以及页面滚动。这些在实际项目中也是高频需求。