在当今的数字时代,Web自动化和数据采集已经成为许多企业和个人项目的重要组成部分。为了实现这些任务,需要一个强大而灵活的工具,能够模拟用户行为,自动执行各种网页操作,并提取所需的数据。而在这个领域中,Puppeteer无疑是最受欢迎的工具之一。
Puppeteer是由Google开发的一种无头浏览器工具,它提供了一个API和一组方法,用于控制和操作Chrome或Chromium浏览器的实例。通过Puppeteer,开发人员可以编写脚本来自动化网页操作,如点击按钮、填写表单、截取屏幕截图等。它还允许你拦截和修改网络请求、处理对话框以及执行自定义的JavaScript函数。无论是进行Web自动化测试、爬虫开发还是数据采集,Puppeteer都能为你提供强大的工具支持。
Puppeteer API 是分层次的,反映了浏览器结构。
安装与使用
我们使用Deno的库deno.land/x/puppeteer…。
deno-puppeteer 与 Node 版本相比如何?
deno-puppeteer有效地运行常规版本的 Puppeteer,除了一些小改动以使其与 Deno 兼容。
最显著的区别可能是一些方法获取/返回 Node Buffer,而不是获取/返回 Node Uint8Array。一种方法还返回 web nativeReadableStream而不是 Node Readable。
除此之外,pptr.dev上的文档通常适用。
从下面Chromium与Puppeteer的对应关系来看,16.2.0版本差不多对应的是105或106版本,一两年内是够用了。
使用前需要先安装:
PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts
PUPPETEER_PRODUCT=firefox deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts
样例:
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "example.png" });
await browser.close();
运行:deno run -A –unstable example.js
一些技巧
新页面
正常情况下,就是下面这一句:
const page = await browser.newPage();
使用匿名上下文:
// 创建一个匿名的浏览器上下文
const context = await browser.createIncognitoBrowserContext();
// 在一个原生的上下文中创建一个新页面
const page = await context.newPage();
页面跳转
到某个页面
await page.goto('https://example.com');
回退
page.goBack();
前进
page.goForward();
这几个方法,都有可以配置的参数,其中waitUntil在各个需要等待的环境都是一样的参数。
:::info
- timeout <number> 跳转等待时间,单位是毫秒, 默认是30秒, 传 0 表示无限等待。可以通过page.setDefaultNavigationTimeout(timeout)方法修改默认值
- waitUntil <string|Array<string>> 满足什么条件认为页面跳转完成,默认是 load 事件触发时。指定事件数组,那么所有事件触发后才认为是跳转完成。事件包括:
- load – 页面的load事件触发时
- domcontentloaded – 页面的 DOMContentLoaded 事件触发时
- networkidle0 – 不再有网络连接时触发(至少500毫秒后)
- networkidle2 – 只有2个网络连接时触发(至少500毫秒后)
:::
页面操作
获取DOM元素
使用$$eval
与$eval
,区别就是复数与单数,document.querySelectorAll和document.querySelector。
// 多个
const divsCounts = await page.$$eval('div', divs => divs.length);
// 一个元素
const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);
页面视口设置
await page.setViewport({ width: 1280, height: 800 });
click触发跳转
要注意如果 click() 触发了一个跳转,会有一个独立的 page.waitForNavigation()
Promise对象需要等待。 正确的等待点击后的跳转是这样的:
const [response] = await Promise.all([
page.waitForNavigation(waitOptions),
page.click(selector, clickOptions),
]);
选中
当提供的选择器完成选中后,触发change和input事件。如果没有元素匹配指定选择器,将报错。
page.select('select#colors', 'blue'); // 单选择器
page.select('select#colors', 'red', 'green', 'blue'); // 多选择器
输入
每个字符输入后都会触发 keydown、keypress、input 和 keyup 事件。
要点击特殊按键,比如 Control 或 ArrowDown,用 keyboard.press
page.type('#mytextarea', 'Hello'); // 立即输入
page.type('#mytextarea', 'World', {delay: 100}); // 输入变慢,像一个用户
等待元素
通常页面跳转,或者DOM有变化后,需要等待某个元素出现才能继续。
// wait for selector
await page.waitFor('.foo');
// wait for 1 second
await page.waitFor(1000);
// wait for predicate
await page.waitFor(() => !!document.querySelector('.foo'));
键盘操作
await page.focus('input[name="username"]');
await page.keyboard.type('myusername');
await page.keyboard.press('Enter');
处理对话框
page.on('dialog', async (dialog) => {
console.log('Dialog message:', dialog.message());
await dialog.dismiss(); // 关闭对话框
});
await page.evaluate(() => {
alert('This is an alert!');
});
执行JS代码
const result = await page.evaluate(x => {
return Promise.resolve(8 * x);
}, 7); // (7 可以是你自己代码里任意方式得到的值)
console.log(result); // 输出 "56"
// 模拟用户滚动
await page.evaluate(() => {
window.scrollBy(0, window.innerHeight);
});
也可以传递字符串:
console.log(await page.evaluate('1 + 2')); // 输出 "3"
const x = 10;
console.log(await page.evaluate(`1 + ${x}`)); // 输出 "11"
ElementHandle 实例 可以作为参数传给 page.evaluate:
const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();
给window注入一个方法
// 定义自定义函数
await page.exposeFunction('md5', text =>
crypto.createHash('md5').update(text).digest('hex')
);
// 在页面中执行自定义函数
await page.evaluate(async () => {
// 使用 window.md5 计算哈希
const myString = 'PUPPETEER';
const myHash = await window.md5(myString);
console.log(`md5 of ${myString} is ${myHash}`);
});
通过 page.exposeFunction 挂载到页面的方法在多次跳转后仍然有用
绕过CSP安全策略
await page.setBypassCSP(true);
给页面注入请求头或cookie
await page.setUserAgent("xxx");
await page.setCookie({
name: "authorization",
value: "",
domain: "xxx",
path: "/",
expires: Date.now() + 3600 * 1000,
});
await page.setExtraHTTPHeaders({
"token": "xxx",
"server": "deno",
});
拦截资源
这个在测试中很有用,因为大部分情况下,我们并不需要加载某个页面的图片、CSS之类,通常就是看页面是否正常,代码是否工作。这时就可以考虑将图片、视频这些资源禁止:
await page.setRequestInterception(true); // Optimize (no stylesheets, images)...
page.on("request", (request) => {
if (
["image", "stylesheet", "media"].includes(request.resourceType()) ||
request.url().endsWith(".wasm")
) {
request.abort();
} else {
// console.log("request intercepted", request.url(), request.resourceType());
request.continue();
}
});
注意:启用请求拦截器会禁用页面缓存。
截图
全屏截图:
await page.screenshot({ path: 'fullpage.png', fullPage: true });
特定元素截图
const elementHandle = await page.$('#myElement');
await elementHandle.screenshot({ path: 'element.png' });
生成PDF
await page.pdf({ path: 'example.pdf', format: 'A4' });
关闭页面
await page.close();
await browser.close();
虽然直接用browser.close就可以,在本地开发时并没有错,但我在GitLab CICD流水线上遇到一个问题,报错:
我这个用例page goto了600多个页面,可能与它有关,在页面少的时候也没事。百思不得其解,尝试加了page.close就好了。
样例
文心一言
使用用户名密码登陆。但卡在需要用手机接收短信验证这一步,没有办法,只能中断。
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
const browser = await puppeteer.launch({
headless: false,
// devtools: true,
// args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.goto("https://yige.baidu.com/", {
waitUntil: "domcontentloaded",
});
await page.waitForSelector(".login");
await Promise.all([
page.click(".login"),
page.waitForNavigation(),
]);
console.info("跳转到登陆页面成功");
await page.click(".tang-pass-footerBarULogin");
console.log("切换到用户名密码窗口");
await page.type('.pass-text-input-userName', 'xxx', {delay: 100});
await page.type('.pass-text-input-password', 'xxx', {delay: 100});
await page.waitForTimeout(1000);
await page.click(".pass-button-submit");
await page.close();
await browser.close();
扒取Bing图片
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
import { basename } from "https://deno.land/std@0.191.0/path/mod.ts";
import { ensureDir } from "https://deno.land/std@0.191.0/fs/mod.ts";
const browser = await puppeteer.launch({
headless: false,
});
async function downloadImages() {
const page = await browser.newPage();
await page.goto("https://cn.bing.com/images");
// 等待页面加载完成
await page.waitForSelector(".img_cont.hoff");
// 获取所有图片的DOM元素
const imageElements = await page.$$(".img_cont.hoff");
// 创建用于保存图片的文件夹
await ensureDir("images");
// 遍历图片元素并下载图片
for (let i = 0; i < imageElements.length; i++) {
const imageElement = imageElements[i];
const imageUrl = await imageElement.$eval("img", (img) => img.src);
const altText = await imageElement.$eval(
"img",
(img) => img.getAttribute("alt"),
);
// 下载图片并保存到本地文件夹
const imageBuffer = await fetch(imageUrl).then((response) =>
response.arrayBuffer()
).catch((err) => {
console.warn("Failed to load imageUrl: " + imageUrl, err);
});
if (imageBuffer) {
await Deno.writeFile(
`images/${basename(altText)}.jpg`,
new Uint8Array(imageBuffer),
);
}
}
console.log("图片下载完成!");
await browser.close();
}
downloadImages().catch(console.error);
总结
Puppeteer是一个能够模拟浏览器行为的工具,可以用于获取网页数据、生成截图、自动填写表单等操作。本文分享了一些使用Puppeteer的技巧和样例,不管是Deno还是Node.js都是通用的,希望能帮助到大家。