玩转Puppeteer,Puppeteer简单入门实战

2,575次阅读
没有评论

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-elements
P-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 的扩展程序后台页面句柄的代码
```json
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.press
page.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实战

我们来实现一个功能:采集豆瓣读书的畅销书籍:书籍地址是https://read.douban.com/category/1?sort=hot 我们将自动翻页采集所有书籍信息。
代码如下:

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<=600) {
            await page.waitForSelector(".page-next");
            await page.click(".page-next");
          } else {
            //收集完成后写入到文件里面
            // fs.writeFileSync(
            //   path.resolve(__dirname, "./list.json"),
            //   JSON.stringify(list)
            // );
            resolve(list);
          }
        }
      });
      await page.goto(
        url
      );
    } catch (e) {
      console.log(e);
      resolve(null);
    }
  });
}

(async () => {
  // 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)
})();

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

正文完
 

公众号