puppeteer 学习笔记


<第一次更新 2024.10.31> 最近又使用了puppeteer做综合类的自动化工具,特地来修一下 blog,还能水篇推文。


因为 OI 学不下去了,所以整天搞一些小玩意。

首先明确一件事,那就是 puppeteer 并不是网络爬虫或者自动化工具的首选。它模拟用户操作,速度非常慢,一般用于加密性强、交互程度高的页面自动化,为了保障安全或者特殊功能需求才使用(比如应对各式各样的反爬软件),因为是纯模拟用户操作,所以安全性非常有保障。对于其他的情况,我更推荐使用 axios 库或者 requestshttps 等传统网络请求库。

安装

首先安装 node.jsnode.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:指定保存的文件类型,jpgpng

  • quality:图片质量,0-100

  • fullpage:是否保存完整的页面。

  • clip:指定截图区域 {x,y,width,height}

  • omitBackground:去除白色背景。

  • encoding:图片编码,base64binary

可以通过以下代码实现:

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...`); 		  }
    }
}

文件写入操作也可以通过 fswrite 操作完成。

  • 保存页面为 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

https://blog.csdn.net/danmyw/article/details/142936489

https://juejin.cn/post/7351300787648020499

posted @ 2020-11-27 18:43  云烟万象但过眼  阅读(269)  评论(0编辑  收藏  举报