puppeteer入门实践

avatar

puppeteer初识

puppeteer是一个node库,他能提供一系列操作Chrome的API,通过这些API我们可以用程序代码来操纵Chrome去完成各种操作。

他运行在node环境,由Chorme官方团队在维护,既然是操作浏览器,那么我们可以手动进行的操作在pupeteer上都能进行。

另外pupeteer的英文就是木偶的意思,我们使用他就像操作木偶玩耍一样,可以轻松的做到:

  • 生成页面 PDF。
  • 抓取页面并生成预渲染内容。
  • 自动提交表单,进行 UI 测试,键盘输入等。
  • 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。
  • 捕获网站的 timeline trace,用来帮助分析性能问题。
  • 测试浏览器扩展。
  • ...... 当然puppeteer不止可以做这些,通过在代码中注入js脚本,我们几乎可以做到所有我们可以用代码实现的功能。

在puppeteer中,Chrome默认运行在"无头"模式,而所谓的无头模式,不过是不加载浏览器的UI界面,实际上并不会影响我们的操作。

使用无头模式,在无界面的情况下按照我们编写的代码去运行Chrome,可以减少人为因素的影响,使运行更加稳定。

当然我们可以通过他的属性值headless来控制让页面友好的显示出来,甚至可以控制页面显示多大。

在前端我们可以用它来爬取一些页面数据进行分析,可以进行网络请求的拦截来达到某种业务效果,总之puppeteer非常的好用。

既然puppeteer是调用的API来操作的浏览器,那么我们可以想到,puppeterr下的浏览器,与我们平时所见到的浏览器有什么不一样呢?

又是什么在吊起了浏览器进程,让我们可以使用代码与浏览器进行通信的呢?

首先让我们了解一下puppeteer对浏览器的整体架构的分解

  • Browser: 其实对应一个浏览器实例,有了这个实例之后我们就可以接着去打开一个浏览器窗口了。
  • BrowserContext:而一个浏览器实例,是可以打开好几个窗口的,他们可以拥有各自的cookie ,session 。
  • Page:表示一个窗口中的一个页面,当然一个窗口可以打开很多个页面。
  • Frame:页面中的 一个框架,每个页面有一个主框架,主要由 iframe 标签创建产生的
  • ExecutionContext: 是 javascript 的执行环境,每一个 Frame 都一个默认的 javascript 执行环境 其次,node库中的这些API,他们实现的原理其实是通过Chrome DevTools Protocol(CDP) 协议与浏览器进行通信的。

而node所做的就是将底层通过协议控制的浏览器操作,进行简化封装,让我们可以简单的使用。

比如下面是通过CDP来进行页面的跳转:

const cdp = new CDP();
await cdp.connect(wsEndpoint);
const targetsResponse = await cdp.send('Target.getTargets');
const pageTarget = targetsResponse.targetInfos.find((t) => t.type === 'page');
const attachResponse = await cdp.send('Target.attachToTarget', { targetId: pageTarget.targetId, flattentrue });
const sessionId = attachResponse.sessionId;
const navigateResponse = await cdp.send('Page.navigate', { url'https://m.jk.cn' }, sessionId);
console.log('navigateResponse', navigateResponse);

而使用puppeteer提供的API的话我们两行代码足以搞定:

const page = await browser.newPage();
await page.goto('https://m.jk.cn');

总体来看对话,他先是将我们日常用到的浏览分成了各个部分,然后实例化出一个浏览器对象,包括浏览器上下文,各个页面等。可以看到,puppeteer其实是对CDP操作浏览器对代码进行了简化,再由node封装导出API供我们使用。

再把与浏览器通信对CDP操作进行简化封装,最后使用node进行导出API,省去了中间复杂的CDP通信过程,让我们直接使用代码的方式操作浏览器。

puppeteer实践

我们使用浏览器,大多的操作都是在页面上进行点击操作,键盘输入操作等,其实浏览器本身提供的还有截图,保存页面pdf等功能。

在通过puppeteer对功能的封装简化之后,我们使用几行代码便可以完成截图,保存pdf。

值得一提的是,使用puppeteer几乎所有操作都是异步的,会返回Promise,我们可以使用.then去处理,但是在node环境下我们可以使用async,await优雅处理

因此我们的运行环境Nodejs 的版本不能低于 v7.6.0, 需要支持 async,await。

首先在项目目录下

npm install puppeteer --save

这样我们就可以开始使用puppeteer以编程的方式去操作浏览器啦。

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

上面的这几行代码就已经实现了我们从打开浏览器,跳转页面,截图,到关闭浏览器的所有过程

先通过 puppeteer.launch() 创建一个浏览器实例 Browser 对象 然后通过 Browser 对象创建页面 Page 对象 然后 page.goto() 跳转到指定的页面 调用 page.screenshot() 对页面进行截图 关闭浏览器 同样的,如果我们要获取页面的pdf我们只需要调用他提供的page下的API

await page.pdf({
      path: 'page.pdf',
      printBackground:true,
      format: 'A4'
    });

可以看到,每个API都可以传入参数,这些参数可以确定我们要将获取到的文件保存在哪path,printBackground是否需要背景图,以及要什么样的尺寸format。

更多的参数属性可以参考官方提供的参数说明。

通过puppeteer我们可以调用简单的API完成一些普通的浏览器操作,除了这些他还能做的事情其实还很多,我们可以模拟用户实现登录,或是爬取页面的数据。

结合入门的基础操作,这里以登录我们豌豆app为列做一个模拟登录的demo,以下为部分代码:

(async () => {
  try{
    const browser = await puppeteer.launch({
      headless:false,
      slowMo:300
    });
    const page = await browser.newPage();
    await page.emulate(puppeteer.devices['iPhone 6']);
    await page.goto('https://m.wandougongzhu.cn/user/login');
    await page.tap('.link-btn');
    await page.type('#app > div > div > div:nth-child(3) > input', '12345678901', {delay:300});
    await page.type('#app > div > div > div:nth-child(4) > input', '***********', {delay:300});
    await page.screenshot({ path: 'full.png', fullPage: true });
    await page.tap('#app > div > div > div.btn-box > div');
    await page.waitForTimeout(2000);
    await page.screenshot({ path: 'login.png', fullPage: true });
    await browser.close();
  } catch (error) {
    console.log(error);
  }
})();

为了方便展示,我们可以设置headless为false这样就可以显示浏览器的页面,接着我们分析一下代码

page.emulate调用此方法可以让页面展示为一种手机模式 page.tap是对页面上对DOM元素进行点击,这里我们点击使用账号密码登录 page.type这个方法可以获取页面上对文本输入DOM,获取焦点,并输入内容,这里我们输入账号密码 然后获取页面上对登录按钮,进行点击登录 在使用page.waitForTimeout做一个页面加载对等待,最后截图关闭

这样我们就实现了以代码对方式模拟用户进行登录操作,并且我们可以设置输入信息对间隔时间,这样可以绕过一些页面对反自动化对规则,保证代码对稳定性。

基于这种操作我们可以实现一些自动化的测试,比如一些表单的提交,按钮的点击测试,数据的输入等等。

通过上面的demo我们可以看出,puppeteer的操作大多是针对与页面上的DOM元素展开的,只要获取到页面上的DOM元素我们就可以展开一系列的操作。

现在的一些网页大多数都是采用js进行后期的渲染加载,使用puppeteer这种获取到DOM之后在对DOM进行操作对方法,会更稳定一些。

puppeteer 实现爬虫

puppeteer是根据页面上的真实存在的DOM进行各种操作的,那么只要我们可以获取到DOM元素,我们就可以获取到元素在页面上展示的值。

根据这个思路,我们就可以结合js完成一个简单的网页爬虫,通过注入的js代码,切换页面,获取页面DOM元素,进而拿到数据。

(async () => {
  let data = [];
  const browser = await puppeteer.launch({
    headless: false,
  });
  const page = await browser.newPage();
  for (let mo = 1; mo < 4; mo++) {
    for (let pg = 1; pg <= 10; pg++) {
      mo = mo.toString().padStart(2, "0");
      await page.goto(
        "https://www.bilibili.com/v/music/cover/?spm_id_from=333.5.b_7375626e6176.3#" +
          `/all/click/0/${pg}/2021-${mo}-01,2021-${mo}-28`
      );
      await page.waitForSelector(".vd-list-cnt > ul > li > div > div.r > a");
      let titles = await page.$$eval(".vd-list-cnt > ul > li > div > div.r > a", 
      (links) => links.map((item) => item.innerText)
      );
      console.log(titles);
      data = data.concat(titles);
    }
  }
  fs.writeFile("data.json", JSON.stringify(data, null, "\t"), function (err) {
    if (err) {
      console.log(err);
    }
  });
})();

上面就是结合js进行的简单的爬虫,一切我们在页面上看到的,都可以使用puppeteer去爬取,并且可以稳定的爬取到数据,避免一些页面的渲染都在js中,使用平常的爬虫很难在拿到数据。

通过这个简单的爬虫相信大家对puppeteer的印象又加深了不少,希望大家通过puppeteer与js代码的结合,可以完成一些业务上的难题。