Puppeteer 实战场景 —— 生成 PDF

1,742 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

前言

上一篇介绍了 Puppeteer 的基础用法和常用API,其中有提到 Puppeteer 的一个使用场景是“生成PDF”。接下来,我们实战一下 PDF 生成,感受一下 Puppeteer 的魅力。

给单个 HTML 生成 PDF

先来个简单的,这里拿 Vue 文档为例,把文档的介绍页转存为 pdf 。注意!生成PDF只支持无界面的操作,headless 必须为 true。 否则会报错哦。

const puppeteer = require('puppeteer');

//启动 chromium
puppeteer.launch({
    headless:true, //生成PDF只支持无界面的操作!(我刚开始调试设了 false ,一直报错哈哈哈)
    //指定视口大小
    defaultViewport:{
        width:1920,
        height:1080
    }
}).then(async (browser)=>{
    //打开一个标签页
    const page = await browser.newPage();
    //进入指定页面,默认会等待页面load事件触发
    await page.goto('https://cn.vuejs.org/v2/guide/index.html'); 
    //指定生成的pdf文件存放路径
    await page.pdf({path: `./vueDoc-pdf/guide.pdf`}); 
    //关闭页面
    page.close()
    //关闭 chromium
    browser.close();
})

page.goto 支持配置 waitUntil,满足什么条件认为页面跳转完成,默认是 load 事件触发时。也可以配置 networkidle0, 表示不再有网络连接时触发(至少500毫秒后)。更多配置项详见 page.goto

给多个HTML生成PDF

上面的示例只生成一个页面,如果想把整个 Vue 教程拿下来呢?这里就用个简单粗暴的方法:依次打开各个页面,逐个生成。还是以 Vue 文档为例,实现如下:

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

(async ()=>{

   //指定存放pdf的文件夹
   const folder = 'vueDoc'
   fs.mkdir(folder,()=>{ console.log('文件夹创建成功') })

    //启动无头浏览器
   const browser = await puppeteer.launch({headless:true }) //PDF 生成仅在无界面模式支持, 调试完记得设为 true
   const page = await browser.newPage();
   await page.goto('https://cn.vuejs.org/v2/guide/index.html'); //默认会等待页面load事件触发

   // 1) 已知Vue文档左侧菜单结构为:.menu-root>li>a
   // 获取所有一级链接
   const urls = await page.evaluate(()=>{
      return new Promise( resolve => {
         const aNodes = $('.menu-root>li>a')
         const urls = aNodes.map(n=>{
               return aNodes[n].href
         })        
         resolve(urls);
      })
   })

   // 2)遍历 urls, 逐个访问并生成 pdf
   for( let i = 0; i<urls.length; i++ ){
      const url = urls[i], 
         tmp = url.split('/'),
         fileName = tmp[tmp.length-1].split('.')[0]
      await page.goto(url); //默认会等待页面load事件触发
      await page.pdf({path: `./${folder}/${i}_${fileName}.pdf`}); //指定生成的pdf文件存放路径
      console.log(`${i}_${fileName}.pdf 已生成`)
   }

   page.close()
   browser.close();
})()

给多个HTML生成PDF_plus

如果你有去跑一下上面的代码,可能也会碰到跟我一样的问题:

在这里插入图片描述 没错我是故意看着你摔跤的,自己摔过印象才深刻嘛~

puppeteer 默认的请求超时时间为 30s,有时候可能会因为网络环境或者别的因素导致某个页面生成失败,进而阻塞其他页面的生成。所以我们还需要做一些容错处理,最好顺便统计一下错误数据,以便后续重试或者别的啥:

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

(async ()=>{
   
    //指定存放pdf的文件夹
    const folder = 'vueDoc' 
    fs.mkdir(folder,()=>{ console.log('文件夹创建成功') })

    //启动无头浏览器
    const browser = await puppeteer.launch({headless:true }) //PDF 生成仅在无界面模式支持, 调试完记得设为 true
    const page = await browser.newPage();
    await page.goto('https://cn.vuejs.org/v2/guide/index.html'); //默认会等待页面load事件触发

    // 1) 已知Vue文档左侧菜单结构为:.menu-root>li>a
    // 获取所有一级链接
    const urls = await page.evaluate(()=>{
        return new Promise( resolve => {
            const aNodes = $('.menu-root>li>a')
            const urls = aNodes.map(n=>{
                return aNodes[n].href
            })        
            resolve(urls);
        })
    })

    // 2)遍历 urls, 逐个访问并生成 pdf    
    let successUrls = [], failUrls = [] // 用于统计成功、失败情况
    for(let i = 17; i<urls.length; i++){
        const url = urls[i], 
            tmp = url.split('/'),
            fileName = tmp[tmp.length-1].split('.')[0]
        try{            
            await page.goto(url); //默认会等待页面load事件触发
            await page.pdf({path: `./${folder}/${i}_${fileName}.pdf`}); //指定生成的pdf文件存放路径
            console.log(`${fileName}.pdf 已生成!`)
            successUrls.push(url)
        }catch{
            //如果页面打开超时,会抛出错误。为了保证后面的页面生成不被影响,这里做一下容错处理。
            failUrls.push(url)
            console.log(`${fileName}.pdf 生成失败!`)
            continue
        }
    }

    console.log(`PDF生成完毕!成功${successUrls.length}个,失败${failUrls.length}个`)
    console.log(`失败详情:${failUrls}`)

    //TODO: 失败重试

    page.close()
    browser.close();
})()

最后

写得有点眼花了,后面有空再继续完善,做一下重试、生成的 PDF 合并啥的。 上个图看看效果吧。

在这里插入图片描述

在这里插入图片描述