玩转Puppeteer,Puppeteer简单入门实战

2023年 10月 8日 96.5k 0

Puppeteer简介

Puppeteer 是一个 Node.js 库,它提供了一个高级 API 来通过 DevTools 协议来控制 Chrome/Chromium。 Puppeteer 默认以无头模式运行,但可以配置为在完整(“有头”)Chrome/Chromium 中运行。Puppeteer能做些什么?

  • 生成页面的屏幕截图和 PDF
  • 抓取 SPA(单页应用程序)并生成预渲染内容(即“SSR”(服务器端渲染))
  • 自动化表单提交、UI 测试、键盘输入等
  • 使用最新的 JavaScript 和浏览器功能创建自动化测试环境。
  • 捕获站点的时间线跟踪以帮助诊断性能问题。
  • 可以测试 Chrome 扩展程序

玩转Puppeteer,Puppeteer简单入门

安装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,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,Puppeteer简单入门

  • 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)
})();

运行之后的效果,image.png这个例子主要是利用了onResponse的事件来监听网页的ajax请求,然后通过page.click来翻页,有时候我们为了逃避监测,可以在翻页之前停顿一段时间。Puppeteer的采集和传统后端采集的区别是Puppeteer模拟了人的点击行为,更加合理

老俊说技术公众号

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论