什么是 Puppeteer?

Puppeteer 是一个控制 headless Chrome 的 Node.js API 。它是一个 Node.js 库,通过 DevTools 协议提供了一个高级的 API 来控制 headless Chrome。它还可以配置为使用完整的(非 headless)Chrome。

https://pptr.dev/

在浏览器中手动完成的大多数事情都可以通过使用 Puppeteer 完成,下面是一些入门的例子:

  • 生成屏幕截图和 PDF 页面

  • 检索 SPA 并生成预渲染内容(即 “SSR”)

  • 从网站上爬取内容

  • 自动提交表单,UI 测试,键盘输入等

  • 创建一个最新的自动测试环境。使用最新的 JavaScript 和浏览器功能,在最新版本的 Chrome 中直接运行测试

  • 捕获网站的时间线跟踪,以帮助诊断性能问题

npm install -g puppeteer

下面是一个用例:

const puppeteer = require('puppeteer');

async function printPDF() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://blog.risingstack.com', {waitUntil: 'networkidle0'});
  const pdf = await page.pdf({ format: 'A4' });
  await browser.close();
  return pdf;
})

这是一个简单的功能,可以导航到 URL 并生成站点的 PDF 文件。

首先,启动浏览器(PDF 生成仅在无头浏览器模式下支持),然后打开一个新页面,设置视口大小,并导航到提供的 URL。

设置该 waitUntil: ‘networkidle0’ 选项意味着当至少 500 毫秒没有网络连接时,Puppeteer 认为导航已完成。(查看 API 文档 以获取更多信息。)

最后,将 PDF 保存到一个变量中,关闭浏览器并返回 PDF。

注意:该 page.pdf方法接收一个 options 对象,也可以在其中使用 ‘path’ 选项将文件保存到磁盘。如果未提供路径,PDF 将不会保存到磁盘,而会获得一个缓冲区。(稍后再讨论如何处理它。)

如果你需要先登录以从受保护的页面生成 PDF,那么首先需要导航到登录页面,检查表单元素的 ID 或名称,填写它们,然后提交表单:

await page.type('#email', process.env.PDF_USER)
await page.type('#password', process.env.PDF_PASSWORD)
await page.click('#submit')

为了安全,要始终将登录凭据存储在环境变量中,不要对其进行硬编码!

样式操作

Puppeteer 也有针对这种样式操作的解决方案。你可以在生成 PDF 前插入样式标签,Puppeteer 会生成一个带有修改样式的文件。

await page.addStyleTag({
    content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' 
});

将文件发送到客户端并保存

现在已经在后端生成了一个 PDF 文件。接下来做什么?

正如上文提到的,如果不将文件保存到磁盘,将获得一个缓冲区。服务端只需要将具有正确内容类型的缓冲区发送到前端。

printPDF().then(pdf => {
    res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length });
    res.send(pdf);
})

而在前端,可以简单地向服务端发送请求,以获取生成的 PDF。

function getPDF() {
 return axios.get(`${API_URL}/your-pdf-endpoint`, {
   responseType: 'arraybuffer',
   headers: {
     'Accept': 'application/pdf'
   }
 });
}

发送请求后,缓冲区应开始下载。现在最后一步是将缓冲区转换为 PDF 文件。

savePDF = () => {
   this.openModal('Loading…'); // open modal
   return getPDF() // API call
     .then((response) => {
       const blob = new Blob([response.data], {type: 'application/pdf'});
       const link = document.createElement('a');
       link.href = window.URL.createObjectURL(blob);
       link.download = `your-file-name.pdf`;
       link.click();
       this.closeModal(); // close modal
     })
    .catch(err => /** error handling **/);
 }

在 Docker 中使用 Puppeteer

这是实现中最棘手的部分。官方文档指出“在 Docker 中启动和运行无头 Chrome 可能很棘手”。官方文档有一个 故障排除 部分,你可以在其中找到有关使用 Docker 安装 puppeteer 的所有必要信息。

如果你在 Alpine 映像上安装 Puppeteer,请确保向下滚动到 页面的这一部分。否则,你可能会无法运行最新版本的 Puppeteer,并且你还需要使用一个标志来禁用 shm 的使用:

const browser = await puppeteer.launch({
  headless: true,
  args: ['--disable-dev-shm-usage']
});

否则,Puppeteer 子进程可能在它正常启动之前就耗尽内存。

# 拉取node镜像
FROM node:10-alpine

# 设置镜像作者
LABEL MAINTAINER="qiyang.hqy@dtwave-inc.com"

# 设置国内阿里云镜像站、安装chromium 68、文泉驿免费中文字体等依赖库
RUN echo "https://mirrors.aliyun.com/alpine/v3.8/main/" > /etc/apk/repositories \
    && echo "https://mirrors.aliyun.com/alpine/v3.8/community/" >> /etc/apk/repositories \
    && echo "https://mirrors.aliyun.com/alpine/edge/testing/" >> /etc/apk/repositories \
    && apk -U --no-cache update && apk -U --no-cache --allow-untrusted add \
      zlib-dev \
      xorg-server \
      dbus \
      ttf-freefont \
      chromium \
      wqy-zenhei@edge \
      bash \
      bash-doc \
      bash-completion -f

# 设置时区
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# 设置环境变量
ENV NODE_ENV production

# 创建项目代码的目录
RUN mkdir -p /workspace

# 指定RUN、CMD与ENTRYPOINT命令的工作目录
WORKDIR /workspace

# 复制宿主机当前路径下所有文件到docker的工作目录
COPY . /workspace

# 清除npm缓存文件
RUN npm cache clean --force && npm cache verify
# 如果设置为true,则当运行package scripts时禁止UID/GID互相切换
# RUN npm config set unsafe-perm true

# 安装pm2
RUN npm i pm2 -g

# 安装依赖
RUN npm install

# 暴露端口
EXPOSE 3000

# 运行命令
ENTRYPOINT pm2-runtime start docker_pm2.json

Puppeteer 示例

方式1:puppeteer.js ,表示从对应URL处获取信息,并返回pdf流。具体代码如下:

    const puppeteer = require('puppeteer');
    const options = process.argv;
    var address, types;

    (async() => {
    if(options.length>=4){
        address=options[2];
        types=options[3];
    }

    const browser = await puppeteer.launch();
    const page = await browser.newPage();


    const userAgent = "Mozilla/5.0 (Linux; Android 8.1.0; MI 8 Build/OPM1.171019.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36";
    page.setUserAgent(userAgent);

    //await page.setViewport({ width: 1920, height: 1080 });

    await page.setViewport({ width: 480, height: 800,isMobile: true}); 

    await page.goto(address, {waitUntil: 'networkidle2'});
    if(types == 'pdf') {
        const pdf = await page.pdf({path: 'd://page.pdf', format: 'A4'});
        await browser.close();
        process.stdout.write(pdf);
    }else {
        await browser.close();
    }


    })();

方式2:setContent.js (window系统使用),表示从html文件获取源文件流,并返回pdf流。具体代码如下:

    const puppeteer = require('puppeteer');
    var fs = require('fs');
    const options = process.argv;
    var htmlContent;

    (async() => {
    htmlFilePath=options[2];

    const browser = await puppeteer.launch();
    const page = await browser.newPage();


    const userAgent = "Mozilla/5.0 (Linux; Android 8.1.0; MI 8 Build/OPM1.171019.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36";
    page.setUserAgent(userAgent);

    //await page.setViewport({ width: 1920, height: 1080 });

    await page.setViewport({ width: 480, height: 800,isMobile: true}); 

    //const buff2 = Buffer.from(htmlContent, 'base64')
    //const htmlContentResult = buff2.toString('utf-8')

    var contentHtml = fs.readFileSync(htmlFilePath, 'utf-8');

    //await page.goto('file://d:\\test2.html');

    await page.setContent(contentHtml);

    const pdf = await page.pdf({path: 'page.pdf', format: 'A4'});

    await browser.close();

    process.stdout.write(pdf);


    })();

setContent.js (linux系统使用)

    const puppeteer = require('puppeteer');
    var fs = require('fs');
    const options = process.argv;
    var htmlContent;

    (async() => {
        htmlFilePath=options[2];

        const browser = await puppeteer.launch({
            args: ['--no-sandbox', '--disable-setuid-sandbox'],
        });
        const page = await browser.newPage();


        const userAgent = "Mozilla/5.0 (Linux; Android 8.1.0; MI 8 Build/OPM1.171019.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36";
        page.setUserAgent(userAgent);

    //await page.setViewport({ width: 1920, height: 1080 });

        await page.setViewport({ width: 1800, height: 1800});

    //const buff2 = Buffer.from(htmlContent, 'base64')
    //const htmlContentResult = buff2.toString('utf-8')

        var contentHtml = fs.readFileSync(htmlFilePath, 'utf-8');

    //await page.goto('file://d:\\test2.html');

        await page.setContent(contentHtml);

        const pdf = await page.pdf({path: 'page.pdf', fullPage: true});

        await browser.close();

        process.stdout.write(pdf);


    })();x
  1. puppeteer.js对应的java调用代码如下:
/**
     * html转pdf,直接通过流输出到浏览器
     *
     * @param response    浏览器响应
     * @param fileName    文件名称
     * @param puppeteerjs 要采用哪个js文件执行
     * @param webSiteUrl  要生成pdf/图片的网页
     * @param types       类型  :pdf代表要生成pdf文件,jpg代表要生成jpg图片
     */
    public static void parseHtml2Pdf(HttpServletResponse response, String fileName, String puppeteerjs, String webSiteUrl, String types) {
        try {
            Runtime rt = Runtime.getRuntime();
            //Process p = rt.exec("node C:\\Users\\boshi\\Desktop\\iview-admin-master\\hn.js https://www.baidu.com pdf");
            Process p = rt.exec("node "+puppeteerjs+" "+webSiteUrl+" "+types);
            InputStream is = p.getInputStream();
            BufferedInputStream bf = new BufferedInputStream(is);
            byte[] data = IOUtils.toByteArray(bf);
            fileName = URLEncoder.encode(fileName, "UTF-8");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
            response.addHeader("Content-Length", "" + data.length);
            response.setContentType("application/octet-stream;charset=UTF-8");
            OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
            outputStream.write(data);
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  1. setContent.js 对应的java调用代码如下:
      public void parseHtml2Pdf(String htmlFileName) {
            try {
                Runtime rt = Runtime.getRuntime();
                Process p = rt.exec("node " + createPdfJsPath + "setContent.js " + this.getHtmlTempPath(htmlFileName));
                InputStream is = p.getInputStream();
                BufferedInputStream bf = new BufferedInputStream(is);
                byte[] data = IOUtils.toByteArray(bf);
                File file = new File(diseaseControlPdfPath + htmlFileName + ".pdf");
                IOUtils.write(data,new FileOutputStream(file));
            } catch (IOException e) {
                log.error("in parseHtml2Pdf has an error,e is ",e);
            }
        }
作者:Jeebiz  创建时间:2023-02-15 23:35
最后编辑:Jeebiz  更新时间:2024-03-12 09:16