Puppeteer: 更友好的 Headless Chrome Node API

简介:

很早很早之前,前端就有了对 headless 浏览器的需求,最多的应用场景有两个

  1. UI 自动化测试:摆脱手工浏览点击页面确认功能模式
  2. 爬虫:解决页面内容异步加载等问题

也就有了很多杰出的实现,前端经常使用的莫过于 PhantomJS 和 selenium-webdriver,但两个库有一个共性——难用!环境安装复杂,API 调用不友好,1027 年 Chrome 团队连续放了两个大招 Headless Chrome 和对应的 NodeJS API Puppeteer,直接让 PhantomJS 和 Selenium IDE for Firefox 作者悬宣布没必要继续维护其产品

Puppeteer

如同其 github 项目介绍:Puppeteer 是一个通过 DevTools Protocol 控制 headless chrome 的 high-level Node 库,也可以通过设置使用 非 headless Chrome

我们手工可以在浏览器上做的事情 Puppeteer 都能胜任

  1. 生成网页截图或者 PDF
  2. 爬取大量异步渲染内容的网页,基本就是人肉爬虫
  3. 模拟键盘输入、表单自动提交、UI 自动化测试

官方提供了一个 playground,可以快速体验一下。关于其具体使用不在赘述,官网的 demo 足矣让完全不了解的同学入门

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

实现网页截图就这么简单,自己也实现了一个简单的爬取百度图片的搜索结果的 demo,代码不过 40 行,用过 selenium-webdriver 的同学看了会流泪,接下来介绍几个好玩的特性

哲学

虽然 Puppeteer API 足够简单,但如果是从 webdriver 流转过来的同学会很不适应,主要是在 webdirver 中我们操作网页更多的是从程序的视角,而在 Puppeteer 中网页浏览者的视角。举个简单的例子,我们希望对一个表单的 input 做输入

webdriver 流程

  1. 通过选择器找到页面 input 元素
  2. 给元素设置值
    const input = await driver.findElement(By.id('kw'));
    await input.sendKeys('test');

Puppeteer 流程

  1. 光标应该 focus 到元素上
  2. 键盘点击输入
await page.focus('#kw');
await page.keyboard.sendCharacter('test');

在使用中可以多感受一下区别,会发现 Puppeteer 的使用会自然很多

async/await

看官方的例子就可以看出来,几乎所有的操作都是异步的,如果坚持使用回调或者 Promise.then 写出来的代码会非常丑陋且难读,Puppeteer 官方推荐的也是使用高版本 Node 用 async/await 语法

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'});
  await page.pdf({path: 'hn.pdf', format: 'A4'});

  await browser.close();
})();

查找元素

这是 UI 自动化测试最常用的功能了,Puppeteer 的处理也相当简单

  1. page.$(selector)
  2. page.$$(selector)

这两个函数分别会在页面内执行 document.querySelector 和 document.querySelectorAll,但返回值却不是 DOM 对象,如同 jQuery 的选择器,返回的是经过自己包装的 Promise<ElementHandle>,ElementHandle 帮我们封装了常用的 click 、boundingBox 等方法

获取 DOM 属性

我们写爬虫爬取页面图片列表,感觉可以通过 page.$$(selector) 获取到页面的元素列表,然后再去转成 DOM 对象,获取 src,然后并不行,想做对获取元素对应 DOM 属性的获取,需要用专门的 API

  1. page.$eval(selector, pageFunction[, ...args])
  2. page.$$eval(selector, pageFunction[, ...args])

大概用法

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);
const divsCounts = await page.$$eval('div', divs => divs.length);

值得注意的是如果 pageFunction 返回的是 Promise,那么 page.$eval 会等待方法 resolve

evaluate

如果我们有一些及其个性的需求,无法通过 page.$() 或者 page.$eval() 实现,可以用大招——evaluate,有几个相关的 API

  1. page.evaluate(pageFunction, …args)
  2. page.evaluateHandle(pageFunction, …args):
  3. page.evaluateOnNewDocument(pageFunction, ...args)

这几个函数非常类似,都是可以在页面环境执行我们舒心的 JavaScript,区别主要在执行环境和返回值上

前两个函数都是在当前页面环境内执行,的主要区别在返回值上,第一个返回一个 Serializable 的 Promise,第二个返回值是前面提到的 ElementHandle 对象父类型 JSHandle 的 Promise

const result = await page.evaluate(() => {
  return Promise.resolve(8 * 7);
});
console.log(result); // prints "56"

const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window));
aWindowHandle; // Handle for the window object. 相当于把返回对象做了一层包裹

page.evaluateOnNewDocument(pageFunction, ...args) 是在 browser 环境中执行,执行时机是文档被创建完成但是 script 没有执行阶段,经常用于修改 JavaScript 环境

注册函数

page.exposeFunction(name, puppeteerFunction) 用于在 window 对象注册一个函数,我们可以添加一个 window.readfile 函数

const puppeteer = require('puppeteer');
const fs = require('fs');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  page.on('console', msg => console.log(msg.text));
  
  // 注册 window.readfile
  await page.exposeFunction('readfile', async filePath => {
    return new Promise((resolve, reject) => {
      fs.readFile(filePath, 'utf8', (err, text) => {
        if (err)
          reject(err);
        else
          resolve(text);
      });
    });
  });
  
  await page.evaluate(async () => {
    // use window.readfile to read contents of a file
    const content = await window.readfile('/etc/hosts');
    console.log(content);
  });
  await browser.close();
});

修改终端

Puppeteer 提供了几个有用的方法让我们可以修改设备信息

  1. page.setViewport(viewport)
  2. page.setUserAgent(userAgent)
await page.setViewport({
  width: 1920,
  height: 1080
});

await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36');

page.emulateMedia(mediaType):可以用来修改页面访问的媒体类型,但仅仅支持

  1. screen
  2. print
  3. null:禁用 media emulation

page.emulate(options):前面介绍的几个函数相当于这个函数的快捷方式,这个函数可以设置多个内容

  1. viewport
  2. width
  3. height
  4. deviceScaleFactor
  5. isMobile
  6. hasTouch
  7. isLandscape
  8. userAgent

puppeteer/DeviceDescriptors 还给我们提供了几个大礼包

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.google.com');
  // other actions...
  await browser.close();
});

键盘

  1. keyboard.down
  2. keyboard.up
  3. keyboard.press
  4. keyboard.type
  5. keyboard.sendCharacter
// 直接输入、按键
page.keyboard.type('Hello World!');
page.keyboard.press('ArrowLeft');

// 按住不放
page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
  page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');

page.keyboard.press('Backspace');
page.keyboard.sendCharacter('嗨');

鼠标 & 屏幕

  1. mouse.click(x, y, [options]): options 可以设置
  2. button
  3. clickCount
  4. mouse.move(x, y, [options]): options 可以设置
  5. steps
  6. mouse.down([options])
  7. mouse.up([options])
  8. touchscreen.tap(x, y)

页面跳转控制

这几个 API 比较简单,不在展开介绍

  1. page.goto(url, options)
  2. page.goback(options)
  3. page.goForward(options)

事件

Puppeteer 提供了对一些页面常见事件的监听,用法和 jQuery 很类似,常用的有

  1. console:调用 console API
  2. dialog:页面出现弹窗
  3. error:页面 crash
  4. load
  5. pageerror:页面内未捕获错误
page.on('load', async () => {
  console.log('page loading done, start fetch...');

  const srcs = await page.$$eval((img) => img.src);
  console.log(`get ${srcs.length} images, start download`);

  srcs.forEach(async (src) => {
    // sleep
    await page.waitFor(200);
    await srcToImg(src, mn);
  });

  await browser.close();

});

性能

通过 page.getMetrics() 可以得到一些页面性能数据

  • Timestamp The timestamp when the metrics sample was taken.
  • Documents 页面文档数
  • Frames 页面 frame 数
  • JSEventListeners 页面内事件监听器数
  • Nodes 页面 DOM 节点数
  • LayoutCount 页面 layout 数
  • RecalcStyleCount 样式重算数
  • LayoutDuration 页面 layout 时间
  • RecalcStyleDuration 样式重算时长
  • ScriptDuration script 时间
  • TaskDuration 所有浏览器任务时长
  • JSHeapUsedSize JavaScript 占用堆大小
  • JSHeapTotalSize JavaScript 堆总量
{ 
  Timestamp: 382305.912236,
  Documents: 5,
  Frames: 3,
  JSEventListeners: 129,
  Nodes: 8810,
  LayoutCount: 38,
  RecalcStyleCount: 56,
  LayoutDuration: 0.596341000346001,
  RecalcStyleDuration: 0.180430999898817,
  ScriptDuration: 1.24401400075294,
  TaskDuration: 2.21657899935963,
  JSHeapUsedSize: 15430816,
  JSHeapTotalSize: 23449600 
}

最后

本文知识介绍了部分常用的 API,全部的 API 可以在 github 上查看,由于 Puppeteer 还没有发布正式版,API 迭代比较迅速,在使用中遇到问题也可以在 issue 中反馈。

在 0.11 版本中只有 page.$eval 并没有 page.$$eval,使用的时候只能通过 page.evaluate,通过大家的反馈,在 0.12 中已经添加了该功能,总体而言 Puppeteer 还是一个十分值得期待的 Node headless API

参考

Getting Started with Headless Chrome

无头浏览器 Puppeteer 初探

Getting started with Puppeteer and Chrome Headless for Web Scraping


    本文转自魏琼东博客园博客,原文链接:http://www.cnblogs.com/dolphinX/p/7715268.html,如需转载请自行联系原作者

相关文章
|
6天前
|
JSON 缓存 JavaScript
深入浅出:使用Node.js构建RESTful API
在这个数字时代,API已成为软件开发的基石之一。本文旨在引导初学者通过Node.js和Express框架快速搭建一个功能完备的RESTful API。我们将从零开始,逐步深入,不仅涉及代码编写,还包括设计原则、最佳实践及调试技巧。无论你是初探后端开发,还是希望扩展你的技术栈,这篇文章都将是你的理想指南。
|
14天前
|
JSON JavaScript API
深入浅出Node.js:从零开始构建RESTful API
【10月更文挑战第39天】 在数字化时代的浪潮中,API(应用程序编程接口)已成为连接不同软件应用的桥梁。本文将带领读者从零基础出发,逐步深入Node.js的世界,最终实现一个功能完备的RESTful API。通过实践,我们将探索如何利用Node.js的异步特性和强大的生态系统来构建高效、可扩展的服务。准备好迎接代码和概念的碰撞,一起解锁后端开发的新篇章。
|
25天前
|
JavaScript 中间件 API
Node.js进阶:Koa框架下的RESTful API设计与实现
【10月更文挑战第28天】本文介绍了如何在Koa框架下设计与实现RESTful API。首先概述了Koa框架的特点,接着讲解了RESTful API的设计原则,包括无状态和统一接口。最后,通过一个简单的博客系统示例,详细展示了如何使用Koa和koa-router实现常见的CRUD操作,包括获取、创建、更新和删除文章。
39 4
|
27天前
|
数据采集 存储 JavaScript
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
本文介绍了如何使用Puppeteer和Node.js爬取大学招生数据,并通过代理IP提升爬取的稳定性和效率。Puppeteer作为一个强大的Node.js库,能够模拟真实浏览器访问,支持JavaScript渲染,适合复杂的爬取任务。文章详细讲解了安装Puppeteer、配置代理IP、实现爬虫代码的步骤,并提供了代码示例。此外,还给出了注意事项和优化建议,帮助读者高效地抓取和分析招生数据。
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
|
18天前
|
JavaScript 前端开发 NoSQL
深入浅出:使用Node.js构建RESTful API
【10月更文挑战第35天】在数字时代的浪潮中,后端技术如同海洋中稳固的灯塔,为前端应用提供数据和逻辑支撑。本文旨在通过浅显易懂的方式,带领读者了解如何利用Node.js这一强大的后端平台,搭建一个高效、可靠的RESTful API。我们将从基础概念入手,逐步深入到代码实践,最终实现一个简单的API示例。这不仅是对技术的探索,也是对知识传递方式的一次创新尝试。让我们一起启航,探索Node.js的奥秘,解锁后端开发的无限可能。
|
2月前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API服务器
【10月更文挑战第12天】使用JavaScript和Node.js构建简单的RESTful API服务器
19 0
|
2月前
|
JSON JavaScript API
Node.js RESTful API
10月更文挑战第8天
15 0
|
2月前
|
JSON JavaScript 前端开发
使用 Node.js 和 Express 构建 RESTful API
【10月更文挑战第3天】 使用 Node.js 和 Express 构建 RESTful API
|
15天前
|
JSON API 数据格式
淘宝 / 天猫官方商品 / 订单订单 API 接口丨商品上传接口对接步骤
要对接淘宝/天猫官方商品或订单API,需先注册淘宝开放平台账号,创建应用获取App Key和App Secret。之后,详细阅读API文档,了解接口功能及权限要求,编写认证、构建请求、发送请求和处理响应的代码。最后,在沙箱环境中测试与调试,确保API调用的正确性和稳定性。
|
27天前
|
供应链 数据挖掘 API
电商API接口介绍——sku接口概述
商品SKU(Stock Keeping Unit)接口是电商API接口中的一种,专门用于获取商品的SKU信息。SKU是库存量单位,用于区分同一商品的不同规格、颜色、尺寸等属性。通过商品SKU接口,开发者可以获取商品的SKU列表、SKU属性、库存数量等详细信息。

热门文章

最新文章