puppeteer 学习笔记
<第一次更新 2024.10.31> 最近又使用了puppeteer做综合类的自动化工具,特地来修一下 blog
,还能水篇推文。
因为 OI
学不下去了,所以整天搞一些小玩意。
首先明确一件事,那就是 puppeteer
并不是网络爬虫或者自动化工具的首选。它模拟用户操作,速度非常慢,一般用于加密性强、交互程度高的页面自动化,为了保障安全或者特殊功能需求才使用(比如应对各式各样的反爬软件),因为是纯模拟用户操作,所以安全性非常有保障。对于其他的情况,我更推荐使用 axios
库或者 requests
、https
等传统网络请求库。
安装
首先安装 node.js
、node.exe
,配置好环境变量等之后进行以下操作:
在工作目录下执行:
npm install express -g
npm install puppeteer
然后每次在当前目录中执行
node xxx.js [$value]
即可执行。
使用
python 爬虫可以拿下多数情况下的爬虫操作,但遇到动态 js
之类的玩意就不太行了。
这个时候 puppeteer 的模拟浏览器行为可以解决这个问题。py 也有类似的东西,但是有谷歌官方做支持的 puppeteer 的 bug 等会少一些。
0.实例创建与基本操作
我们可以通过以下代码创建一个实例:
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
headless: false,
// true 为不观看操作过程(2024年更新:建议使用headless:"new"来代替true的无头模式,否则一直弹警告。
devtools:true, // 开启调试工具
slowMo:1000, // 每1000ms执行一次操作
args: ["--start-maximized"],
// 设置参数,如将浏览器最大化。更多参数参考https://peter.sh/experiments/chromium-command-line-switches/
// executablePath: "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
// 可以使用已安装的 chrome 浏览器替换默认的 chromium 浏览器,此处设置浏览器执行文件路径
});
const page = (await browswer.pages())[0]; // 创建浏览器自带一个标签页
const page = await browser.newPage(); // 或者你可以选择重新创建一个
await page.goto(Url, {
timeout: 0, // 不限等待时间(大概吧?),可调整
waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidl2"],
//load: window.onload事件被触发时候页面加载成功,某些情况下它根本不会发生。
//domcontentloaded Domcontentloaded事件触发时候认为导航成功
//networkidle0 在 500ms 内没有网络连接时就算成功(全部的request结束),才认为页面加载成功
//networkidle2 500ms 内有不超过 2 个网络连接时就算成功(还有两个以下的request),就认为页面加载成功
});
await page.close(); // 关闭标签页
await browser.close(); // 关闭浏览器
page 有如下常用操作:
await page.goto(url); // 前往某个 url
await page.reload(); // 刷新
cookies=[{/*something*/},{...}];
await page.setCookie(...cookies); // 设置cookies,注意格式
await page.close(); //关闭当前页
//await page.waitForTimeout(/*Time (ms)*/);
有时 cmd
命令中需要传入一些参数,可以直接带在 .js 文件后面。
const value = process.argv[2]; // 2 为取出*.js后第一项
/*
如: node test.js https://www.baidu.com
那么将取出 https://www.baidu.com
*/
console.log(value); // 输出在标准输出中
等待指令:
// 2024更新:无论是waitFor还是waitForTimeout都已失效,建议使用下面的。
await new Promise((r) => { setTimeout(r, 1000);
// 或者封装成函数。
function delay(time) { return new Promise((resolve) => { setTimeout(resolve, time); }); }
let delay = (ms)=>new Promise((r) => setTimeout(r, ms));
// 等待指定元素加载完毕
await page.waitForSelector('selector');
await page.waitForXpath('xpath');
await page.waitForNetworkIdle(); // 在 500 毫秒内没有网络连接时认为页面加载成功
await page.waitForNavigation(); // 等待页面完全加载完成。
// 等待页面标题包含“Puppeteer”的页面跳转完成
await page.waitForNavigation({ waitUntil: 'titleContains', url: /puppeteer/i });
console.log('Page navigation completed');
// 等待一个异步操作完成
await page.waitForFunction(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 1000);
});
}, { polling: 100 });
console.log('Async function completed');
// 等待指定的URL请求完成
await page.waitForRequest(request => {
return request.url().endsWith('.png');
}, { timeout: 5000 });
console.log('PNG request completed');
// 等待指定的URL响应完成
await page.waitForResponse(response => {
return response.url().endsWith('.js');
}, { timeout: 5000 });
console.log('JS response completed');
数字代表项数,用空格分开。
更多操作见官方文档.许多 js 基本操作与 c++ 类似甚至完全一样。
1.获取页面元素
可以通过 selector 或 xpath 获取元素。
const element = await page.$eval('selector', el => el.something);
const value = await page.$x('xpath');
// 获取DOM之后可进行一系列操作,如:点击
await page.click('selector');
await value[0].click();
// 获取 className
const b = await page.evaluate((s) => {
return document.querySelector(s)&&document.querySelector(s).className;
}, selector);
// 获取 style
const style = await page.$eval(selector, (el) =>
el&&JSON.parse(JSON.stringify(window.getComputedStyle(el)))
);
这将获取整个元素,具体要取出哪些内容要自己选择。
selector
便于获得一些 href
之类的玩意,xpath
则被我用于点击。(2024补充:其实 selector
用于交互式的操作依旧好用,我现在基本都用 selector
了)
xpath
获取的元素有若干属性,可通过通配符获取子节点个数:
const ele = await page.$x('//*[@id="comicContain"]/*');
const len = ele.length;
2.输入/点击/截图
实时获取命令行输入需要引入 readline-sync 库。
输入与点击操作:
await page.focus('selector'); // 页面聚焦
// 模拟页面失焦
await page.evaluate((selector) => {
if(document.querySelector(selector)){
document.querySelector(selector).value = "曲目2050";
document.querySelector(selector).blur();
}
}, 'input[name=input-playlist-title]');
await page.tap(selector); //手机端
await page.click('selector'); // 选择元素并点击
await page.type('selector'); // 通过某个selector获取元素并输入
await (await page.$x('xpath'))[0].type(something_to_type); // 通过某个xpath获取元素并输入
await (await page.$x('xpath'))[0].click(); // 选择元素并点击
await page.keyboard.press('[option]'); // 按一下
await page.keyboard.down('[option]'); // 按着
await page.keyboard.up('[option]'); // 松开
有时需要对某个元素截图并保存,参数如下:
-
path
:指定截图后的文件保存路径。 -
type
:指定保存的文件类型,jpg
或png
。 -
quality
:图片质量,0-100
。 -
fullpage
:是否保存完整的页面。 -
clip
:指定截图区域{x,y,width,height}
。 -
omitBackground
:去除白色背景。 -
encoding
:图片编码,base64
或binary
。
可以通过以下代码实现:
async function screenshotDOMElement(page, selector, path, padding = 0) {
const rect = await page.evaluate(selector => {
try{
const element = document.querySelector(selector);
const {x, y, width, height} = element.getBoundingClientRect();
if(width * height != 0){
return {left: x, top: y, width, height, id: element.id}; }
else { return null; }
}catch(e){return null; }
}, selector);
return await page.screenshot({path: path, clip: rect ? {x: rect.left - padding, y: rect.top - padding, width: rect.width + padding * 2, height: rect.height + padding * 2 } : null });
}
// 传入: page实例、selector选择器、path保存路径.
NodeJs
与终端交互的读入方法如下:
var v_type = readlineSync.question('Some Output with the question');
那么 v_type 中就获得了实时的输入。
3.文件下载\保存
文件系统相关操作需要引入 fs 库。
文件路径相关操作则需要引入 path 库。
文件下载操作通过点击操作触发。
const cdp = await page.target().createCDPSession();
save_path = '';
await cdp.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: path.resolve(__dirname, `${save_path}`)
// path 的这个格式必不可少,save_path 是真正的路径。
}); // 文件下载相关行为
对于保存路径可能不存在的情况,可以通过如下代码完成路径创建。
async function dirExists(savepath) { whole = savepath.split('\\'); var now = whole[0];
for (var i = 1; i < whole.length; ++i) {
now = now + '\\' + whole[i];
if (!fs.existsSync(now)) {
fs.mkdirSync(now); console.log(`File ${now} is not exist, now is making...`); }
}
}
文件写入操作也可以通过 fs
的 write
操作完成。
- 保存页面为
PDF
await page.pdf({path: 'blogList.pdf', format: 'A4'});
新 Tab 事件
点击事件可能导致新页面的打开(如点击 <a>
标签),此时可通过监听页面创建来获取到新的页面,并且保留旧的。
const newPagePromise = new Promise(x => browser.once('targetcreated', target => x(target.page()))); // 声明变量
await page.click('selector');
const newpage = await newPagePromise;
模拟设备显示
Puppeteer 还支持设置模拟移动设备界面。通过 page.emulate()
方法可以模拟一些主流的移动设备界面。具体支持的设备清单可以参见 官方提供的清单
用法如下:
import puppeteer from 'puppeteer';
import {KnownDevices} from 'puppeteer';
iphone15();
async function iphone15() {
const iPhone = KnownDevices['iPhone 15 Pro'];
const browser = await puppeteer.launch({
headless:false,
devtools:true
});
const pages = await browser.pages();
const page = pages[0];
await page.emulate(iPhone);
await page.goto('https://yywxdgy.github.io/');
}
网页请求&相应拦截
Request
await page.setRequestInterception(true); // 开启请求拦截
page.on('request', request => { // 监听请求事件
const headers = request.headers(); // 获取请求头部
headers['Authorization'] = 'Bearer ' + token; // 添加token
request.continue({ headers }); // 继续请求
});
Response
page.on('response', response => { // 监听响应事件
const status = response.status(); // 获取响应状态码
const url = response.url(); // 获取响应 URL
});
参考资料:
https://zhuanlan.zhihu.com/p/624900686
https://blog.csdn.net/weixin_45855502/article/details/123464474