Puppeteer简介
Puppeteer 是一个 Node.js 库,它提供了一个高级 API 来通过 DevTools 协议来控制 Chrome/Chromium。 Puppeteer 默认以无头模式运行,但可以配置为在完整(“有头”)Chrome/Chromium 中运行。Puppeteer能做些什么?
- 生成页面的屏幕截图和 PDF
- 抓取 SPA(单页应用程序)并生成预渲染内容(即“SSR”(服务器端渲染))
- 自动化表单提交、UI 测试、键盘输入等
- 使用最新的 JavaScript 和浏览器功能创建自动化测试环境。
- 捕获站点的时间线跟踪以帮助诊断性能问题。
- 可以测试 Chrome 扩展程序
安装Puppeteer
Puppeteer是基于nodejs环境的,要在项目中使用 Puppeteer,可以直接运行
npm i puppeteer
# or using yarn
yarn add puppeteer
# or using pnpm
pnpm i puppeteer
当安装 Puppeteer 时,它会自动下载最新版本的 Chrome (~170MB macOS、~282MB Linux、~280MB Windows),来保证可以与 Puppeteer 配合使用。浏览器默认下载到 $HOME/.cache/puppeteer 文件夹(从Puppeteer v19.0.0开始)为了减少包的安装包的体积,我们可以只安装puppeteer-core就行,然后在程序内指定系统用的chrome路径即可
如何查看不同平台下的chrome路径 ?
打开chrome,在浏览器地址栏输入chrome://version,显示的可执行文件路径即为chrome路径
安装puppeteer-core方法同安装puppeteer一样,使用方法如下:
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
注意:Puppeteer-core必须指定executablePath
使用Puppeteer
Puppeteer的使用一般示例如下:
import puppeteer from 'puppeteer';
(async () => {
// Launch the browser and open a new blank page
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Navigate the page to a URL
await page.goto('https://developer.chrome.com/');
// Set screen size
await page.setViewport({width: 1080, height: 1024});
// Type into search box
await page.type('.search-box__input', 'automate beyond recorder');
// Wait and click on first result
const searchResultSelector = '.search-box__link';
await page.waitForSelector(searchResultSelector);
await page.click(searchResultSelector);
// Locate the full title with a unique string
const textSelector = await page.waitForSelector(
'text/Customize and automate'
);
const fullTitle = await textSelector?.evaluate(el => el.textContent);
// Print the full title
console.log('The title of this blog post is "%s".', fullTitle);
await browser.close();
})();
配置Puppeteer
Puppeteer推荐使用配置文件的方式来做管理Puppeteer配置,Puppeteer 将在文件树中查找以下任何格式:
- .puppeteerrc.cjs,
- .puppeteerrc.js,
- .puppeteerrc (YAML/JSON),
- .puppeteerrc.json,
- .puppeteerrc.yaml,
- puppeteer.config.js,
- puppeteer.config.cjs
**Puppeteer 还会从应用程序的 package.json 读取 puppeteer 键。**这使得我们可以直接通过package.json来管理puppeteer如:但需要注意的是:puppeteer-core不支持配置文件的形式,配置文件只针对puppeteer包
查询选择器
当我们使用**puppeteer,**我们的主要目前大概率是要自动化操作网页,那么就必须要使用查询选择器(Query Selectors),Selector是与站点上的 DOM 交互的主要机制,典型的使用流程如下:
// Import puppeteer
import puppeteer from 'puppeteer';
(async () => {
// Launch the browser
const browser = await puppeteer.launch();
// Create a page
const page = await browser.newPage();
// Go to your site
await page.goto('YOUR_SITE');
// Query for an element handle.
const element = await page.waitForSelector('div > .class-name');
// Do something with element...
await element.click(); // Just an example.
// Dispose of handle
await element.dispose();
// Close browser.
await browser.close();
})();
Puppeteer 使用** CSS选择器语法**的超集进行查询。如:const element =await page.waitForSelector('div > .class-name');Puppeteer也支持P-elementsP-elements是带有 -p 供应商前缀的伪元素。它允许您使用 Puppeteer 特定的查询引擎(例如 XPath、文本查询和 ARIA)来增强选择器。
文本选择器 ( -p-text )
文本选择器将选择包含给定文本的“最小”元素,即使是在(开放的)影子根中。这里,“最小”是指包含给定文本的最深元素,但不是它们的父元素(技术上,它们也将包含给定文本)。使用示例:
const element = await page.waitForSelector('div ::-p-text(My name is Jun)');
// You can also use escapes.
const element = await page.waitForSelector(
':scope >>> ::-p-text(My name is Jun \(pronounced like "June"\))'
);
// or quotes
const element = await page.waitForSelector(
'div >>>> ::-p-text("My name is Jun (pronounced like \"June\")"):hover'
);
XPath 选择器 ( -p-xpath )
XPath 选择器将使用浏览器的本机 Document.evaluate 来查询元素,使用上可以配合DevTool的元素面板来查询示例:
const element = await page.waitForSelector('::-p-xpath(h2)');
//也可以如:
const element = await page.$x('/html/body');
在页面上下文中执行JavaScript并返回
Puppeteer 允许在 Puppeteer 驱动的页面上下文中执行 JavaScript 函数,并返回。示例:
// Import puppeteer
import puppeteer from 'puppeteer';
(async () => {
// Launch the browser
const browser = await puppeteer.launch();
// Create a page
const page = await browser.newPage();
// Go to your site
await page.goto('YOUR_SITE');
// Evaluate JavaScript
const three = await page.evaluate(() => {
return 1 + 2;
});
console.log(three);
// Close browser.
await browser.close();
})();
返回类型:
- 您计算的函数可以返回值。如果返回的值是原始类型,Puppeteer 会自动将其转换为脚本上下文中的原始类型,如前面的示例所示。
- 如果脚本返回一个对象,Puppeteer 会将其序列化为 JSON 并在脚本端重建它。此过程可能并不总是产生正确的结果
请求拦截
Puppeteer可以拦截网页的请求,一旦启用请求拦截,每个请求都将停止,除非它继续、响应或中止。代码层面的实现是通过setRequestInterception和page的onRequest事件结合使用
import puppeteer from 'puppeteer';
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
await page.goto('https://example.com');
await browser.close();
})();
请求拦截在一些需要捕获ajax内容的场景上很有用
测试 Chrome 扩展
以下是获取源位于 ./my-extension 的扩展程序后台页面句柄的代码
import puppeteer from 'puppeteer';
import path from 'path';
(async () => {
const pathToExtension = path.join(process.cwd(), 'my-extension');
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
const backgroundPageTarget = await browser.waitForTarget(
target => target.type() === 'background_page'
);
const backgroundPage = await backgroundPageTarget.page();
// Test the background page as you would any other page.
await browser.close();
})();
Puppeteer API
Puppeteer官网展示了所有的Puppeteer API文档 ,我们这里举例一些比较常用的,其他的可以根据需要及时查询就好Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer API 是分层次的,反映了浏览器结构。架构如下
- Puppeteer 使用 DevTools 协议 与浏览器进行通信。
- Browser 实例可以拥有浏览器上下文。
- BrowserContext 实例定义了一个浏览会话并可拥有多个页面。
- **Page **至少有一个框架:主框架。 可能还有其他框架由 iframe 或 框架标签 创建。
- frame 至少有一个执行上下文 - 默认的执行上下文 - 框架的 JavaScript 被执行。 一个框架可能有额外的与 扩展 关联的执行上下文。
- Worker 具有单一执行上下文,并且便于与 WebWorkers 进行交互。
一、获取元素信息
page.$(selector)
在页面内执行 document.querySelector。
page.$$(selector)
在页面内执行 document.querySelectorAll。
page.$x(expression)
解析指定的XPath表达式。
page.$eval(selector, pageFunction[, ...args])
在页面内执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction。
计算div的个数
const divsCounts = await page.$eval('div', divs => divs.length);
page.$eval(selector, pageFunction[, ...args])
在页面内执行 document.querySelector,然后把匹配到的元素作为第一个参数传给pageFunction。
示例:
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);
page.evaluate(pageFunction[, ...args])
执行脚本
示例
const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();
page.evaluateHandle(pageFunction[, ...args])
此方法和 page.evaluate 的唯一区别是此方法返回的是页内类型(JSHandle)
const aHandle = await page.evaluateHandle(() => document.body);
const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle);
console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
page.evaluateOnNewDocument(pageFunction[, ...args])
在所属页面的任意 script 执行之前被调用。常用于修改页面js环境
// preload.js
// 重写 `languages` 属性,使其用一个新的get方法
Object.defineProperty(navigator, "languages", {
get: function() {
return ["en-US", "en", "bn"];
}
});
二、模拟用户操作
1.Page类page.click(selector[, options])找到一个匹配 selector 选择器的元素,如果需要会把此元素滚动到可视,然后通过** page.mouse** 点击它。要注意如果 click() 触发了一个跳转,会有一个独立的 page.waitForNavigation() Promise对象需要等待。 正确的等待点击后的跳转是这样的:const [response] = await Promise.all([ page.waitForNavigation(waitOptions), page.click(selector, clickOptions),]);
page.focus(selector)找到一个匹配selector的元素,并且把焦点给它
page.hover(selector)找到一个匹配的元素,如果需要会把此元素滚动到可视,然后通过 page.mouse 来hover到元素的中间。
page.select(selector, ...values)当提供的下拉选择器完成选中后,触发change和input事件page.select('select#colors', 'blue'); // 单选择器page.select('select#colors', 'red', 'green', 'blue'); // 多选择器
page.type(selector, text[, options])每个字符输入后都会触发 keydown, keypress/input 和 keyup 事件要点击特殊按键,比如 Control 或 ArrowDown,用 keyboard.presspage.type('#mytextarea', 'Hello'); // 立即输入page.type('#mytextarea', 'World', {delay: 100}); // 输入变慢,像一个用户
2.Mouse类每个 page 对象都有它自己的 Mouse 对象,使用见 page.mouse。mouse.click(x, y, [options])mouse.down([options])mouse.move(x, y, [options])mouse.up([options])// 使用 ‘page.mouse’ 追踪 100x100 的矩形。await page.mouse.move(0, 0);await page.mouse.down();await page.mouse.move(0, 100);await page.mouse.move(100, 100);await page.mouse.move(100, 0);await page.mouse.move(0, 0);await page.mouse.up();
3.Keyboard类keyboard.down(key[, options])keyboard.press(key[, options])keyboard.sendCharacter(char)keyboard.type(text, options)keyboard.up(key)
按下 Shift 来选择一些字符串并且删除的例子:await page.keyboard.type('Hello World!');await page.keyboard.press('ArrowLeft');
await page.keyboard.down('Shift');for (let i = 0; i < ' World'.length; i++) await page.keyboard.press('ArrowLeft');await page.keyboard.up('Shift');
await page.keyboard.press('Backspace');// 结果字符串最终为 'Hello!'按下 A 的例子:await page.keyboard.down('Shift');await page.keyboard.press('KeyA');await page.keyboard.up('Shift');
三、选择器语法
1.Document.querySelector()格式:element = parentNode.querySelector(selectors);2.Document.querySelectorAll()格式:elementList = parentNode.querySelectorAll(selectors);获取文档中所有
元素的NodeList。获取文档中类名为 "myclass" 的元素的NodeList。var matches = document.querySelectorAll("p");var el = document.querySelector(".myclass");获取文档中所有class包含"note"或"alert"的
元素的列表,:var matches = document.querySelectorAll("div.note, div.alert");获取ID为"test"的容器内,其直接父元素是一个class为"highlighted"的div的所有
元素的列表。var container = document.querySelector("#test"); var matches = container.querySelectorAll("div.highlighted > p");获取class为"select"的容器内,其祖先元素是一个class为"outer"的下的所有class为“inner"的元素列表。(这里即使outer不在select内,inner也会被找到)var select = document.querySelector('.select'); var inner = select.querySelectorAll('.outer .inner');获取文档中属性名为"data-src"的iframe元素列表:var matches = document.querySelectorAll("iframe[data-src]");获取列表后,再找匹配项var highlightedItems = userList.querySelectorAll(".highlighted");highlightedItems.forEach(function(userItem) { deleteUser(userItem);});
Puppeteer实战
我们来实现一个功能:采集豆瓣读书的畅销书籍:书籍地址是read.douban.com/category/1?… 我们将自动翻页采集所有书籍信息。代码如下:
import puppeteer from 'puppeteer-core';
async function getList(page,url) {
return new Promise(async (resolve) => {
let list = [];
try {
page.removeAllListeners("response");
page.on("response", async (res) => {
let url = res.url();
if (url.indexOf("read.douban.com/j/kind") !== -1) {
let resData = await res.json();
list = [...list, ...resData.list];
console.log(list.length);
//由于数量太多,我们获取前600本就好
if (list.length < resData.total && list.length {
// Launch the browser and open a new blank page
//executablePath: 'C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe',
const browser = await puppeteer.launch({executablePath: 'C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe', headless: 'new'});
const page = await browser.newPage();
const url = "https://read.douban.com/category/1?sort=hot"
const result = await getList(page,url)
console.log(result)
})();
运行之后的效果,这个例子主要是利用了onResponse的事件来监听网页的ajax请求,然后通过page.click来翻页,有时候我们为了逃避监测,可以在翻页之前停顿一段时间。Puppeteer的采集和传统后端采集的区别是Puppeteer模拟了人的点击行为,更加合理