Skip to content

Nest 使用 puppeteer

puppeteer 使用场景

注意

  • 生成页面 PDF。截图

  • 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。

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

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

  • 捕获网站的 timeline trace,用来帮助分析性能问题。

  • 测试浏览器扩展。

安装

bash
pnpm i puppeteer

报错信息

  • 在 linux 中使用 PM2 来部署 nestjs 会报错
bash

Error: Could not find Chrome (ver. 121.0.6167.85). This can occur if either
1. you did not perform an installation before running the script (e.g. `npx puppeteer browsers install chrome`) or
2. your cache path is incorrectly configured (which is: /home/www/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.
  • 解决办法, 执行命令
bash

npx @puppeteer/browsers install chrome@stable
  • 下载完成后写入路径即可
ts
executablePath = "/www/chrome/linux-121.0.6167.85/chrome-linux64/chrome";

console.log("启动浏览器", url);
const browser = await puppeteer.launch({
  headless: false, // 无头模式 默认模式就是true
  executablePath: this.executablePath, // 运行绑定
  args: ["--no-sandbox", "--disable-setuid-sandbox"],
});

Nest 使用 puppeteer

安装爬虫模块

bash
nest g resource pachong

创建爬虫服务

ts
/*
测试网址: https://learnwebcode.github.io/practice-requests/
*/

import { Injectable } from "@nestjs/common";
import path from "path";
import puppeteer from "puppeteer";
import * as fs from "fs";
@Injectable()
export class PachongService {
  private _puppeteer: any;
  constructor() {
    this.init();
  }

  async init() {
    this._puppeteer = await puppeteer.launch({
      headless: true, // 无头模式
      defaultViewport: {
        width: 0,
        height: 0,
      },
      args: ["--no-sandbox", "--disable-setuid-sandbox"],
    });
  }

  // 第一步 创建page
  async newpage() {
    const page = await this._puppeteer.newPage();
    return page;
  }

  // 要开始干的事情
  // 1. 截图 增加fullPage 代表全屏不是一页屏幕
  async savescreen(url) {
    const page = await this.newpage();
    await page.goto(url);
    // 全屏保存
    const dir = path.join(__dirname, "../../../", "public/result.png");
    await page.screenshot({ path: dir, fullPage: true });
    // 关闭浏览器
    // await this._puppeteer.close();
    return "成功截图了";
  }

  // 2. 获取页面元素文字
  async gettext(url: string) {
    const page = await this.newpage();
    await page.goto(url);
    const names = await page.evaluate(async () => {
      // 这里面所有的代码都运行在浏览器里面所以你看不见console.log打印结果
      // 所以你传递不了 任何参数,只能写死了
      console.log("******");
      // 选择的文本
      let _selector = ".info strong";
      const domall = document.querySelectorAll(_selector);
      const resulttext: string[] = [];
      domall.forEach((item) => {
        resulttext.push(item.textContent);
      });
      return resulttext;
    });
    // 写入文件写在外面
    const dir = path.join(__dirname, "../../../", "public/names.text");
    await fs.writeFileSync(dir, names.join("\r\n"));
    return "成功获取了页面元素文字";
  }

  // 3 获取图片,用puppeteer的api提供的简便写法 $$ 代表多个 $代表单个
  async getimage(url: string) {
    const page = await this.newpage();
    await page.goto(url);
    // 获取到所有的图片
    const photos = await page.$$eval("img", (imgs) => {
      return imgs.map((img) => img.src);
    });
    // 开始下载图片
    for (const photo of photos) {
      const imagepage = await page.goto(photo);
      const dir = path.join(__dirname, "../../../", "public/");
      await fs.writeFileSync(
        dir + photo.split("/").pop(),
        await imagepage.buffer()
      );
    }
    return "成功获取了图片";
  }

  // 4 点击以后获取页面元素 找到点击元素的id,然后点击
  async getclick(url: string) {
    const page = await this.newpage();
    await page.goto(url);
    await page.click("#clickme");
    const clickedData = await page.$eval("#data", (el) => {
      return el.textContent;
    });
    return clickedData;
  }

  // 5 输入数据后跳转页面,在新页面里面获取数据
  async getinput(url: string) {
    const page = await this.newpage();
    await page.goto(url);
    await page.type("#ourfield", "blue", { delay: 100 }); // 输入框对应的元素
    await page.click("#ourform button"); // 点击按钮
    await page.waitForNavigation(); // 等待跳转
    // 新页面获取到数据
    const result = await page.$eval("#message", (el) => {
      return el.textContent;
    });
    return result;
  }

  // 6 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))

  async getHtml(url: string) {
    const page = await this.newpage();
    // 等待页面加载完成
    await page.goto(url, { waitUntil: "networkidle2" });

    // 等待特定选择器出现(可选,根据实际需求调整)
    await page.waitForSelector("#app");

    const elements = await page.evaluate(() => {
      // 获取所有带有特定类名的元素
      const items = document.querySelectorAll("html");
      return Array.from(items).map((item) => ({
        text: item.textContent,
        html: item.innerHTML,
        // 获取 Vue 组件的 data 属性
        data: (item as any).__vue__?.data,
      }));
    });
    // 获取渲染后的 HTML
    // const html = await page.content();

    const dir = path.join(__dirname, "../../../", "public/index.html");
    await fs.writeFileSync(dir, elements[0].html);

    return "成功获取了html";
  }
}
  • pachong.module.ts
ts
import { Module } from "@nestjs/common";
import { PachongService } from "./pachong.service";
import { PachongController } from "./pachong.controller";

@Module({
  controllers: [PachongController],
  providers: [PachongService],
  exports: [PachongService],
})
export class PachongModule {}

其他模块使用爬虫

控制器

  • xxx.controller.ts
ts
import { Controller, Post, Body } from "@nestjs/common";
import { UserService } from "./user.service";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 1. 截图
  @Post("jietu")
  async jitu(@Body() Body) {
    const result = await this.userService.jietu(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }

  // 2 获取文字
  @Post("gettext")
  async gettext(@Body() Body) {
    const result = await this.userService.gettext(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }

  // 3 获取图片
  @Post("getimage")
  async getimage(@Body() Body) {
    const result = await this.userService.getimage(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }

  // 4 点击后获取数据
  @Post("getclick")
  async getclick(@Body() Body) {
    const result = await this.userService.getclick(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }

  // 5 输入数据后跳转新页面获取数据
  @Post("getinput")
  async getinput(@Body() Body) {
    const result = await this.userService.getinput(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }
  // 6 获取vue页面
  @Post("getHtml")
  async getHtml(@Body() Body) {
    const result = await this.userService.getHtml(Body.url);
    return {
      data: {
        message: result,
      },
    };
  }
}

提供服务

  • xxx.service.ts
ts
import { Inject, Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PachongService } from "../pachong/pachong.service";
@Injectable()
export class UserService {
  // 注入配置文件
  @Inject()
  private readonly configService: ConfigService;
  // 注入爬虫
  @Inject()
  private readonly pachongService: PachongService;
  // 截图方法
  async jietu(url: string) {
    return await this.pachongService.savescreen(url);
  }

  // 执行js代码(采集原始方法)
  async gettext(url: string) {
    const names = await this.pachongService.gettext(url);
    return names;
  }

  // 获取图片保存
  async getimage(url: string) {
    const names = await this.pachongService.getimage(url);
    return names;
  }

  // 点击后获取页面元素
  async getclick(url: string) {
    const result = await this.pachongService.getclick(url);
    return result;
  }

  // 获取表单跳转页面后的新的元素内容

  async getinput(url: string) {
    const result = await this.pachongService.getinput(url);
    return result;
  }

  // 获取html
  async getHtml(url: string) {
    const result = await this.pachongService.getHtml(url);
    return result;
  }
}

补充

模拟不同的设备

Puppeteer 提供了模拟不同设备的功能,其中 puppeteer.devices 对象上定义很多设备的配置信息,这些配置信息主要包含 viewport 和 userAgent,然后通过函数 page.emulate 实现不同设备的模拟

ts
const puppeteer = require("puppeteer");
const iPhone = puppeteer.devices["iPhone 6"];
puppeteer.launch().then(async (browser) => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto("https://www.google.com");
  await browser.close();
});

跳转新的 tab 页处理

在点击一个按钮跳转到新的 Tab 页时会新开一个页面,这个时候我们如何获取改页面对应的 Page 实例呢?可以通过监听 Browser 上的 targetcreated 事件来实现,表示有新的页面创建:

ts
let page = await browser.newPage();
await page.goto(url);
let btn = await page.waitForSelector("#btn");
//在点击按钮之前,事先定义一个 Promise,用于返回新 tab 的 Page 对象
const newPagePromise = new Promise((res) =>
  browser.once("targetcreated", (target) => res(target.page()))
);
await btn.click();
//点击按钮后,等待新tab对象
let newPage = await newPagePromise;

文件的上传和下载

在自动化测试中,经常会遇到对于文件的上传和下载的需求,那么在 Puppeteer 中如何实现呢?

ts
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  //通过 CDP 会话设置下载路径
  const cdp = await page.target().createCDPSession();
  await cdp.send("Page.setDownloadBehavior", {
    behavior: "allow", //允许所有下载请求
    downloadPath: "path/to/download", //设置下载路径
  });
  //点击按钮触发下载
  await (await page.waitForSelector("#someButton")).click();
  //等待文件出现,轮训判断文件是否出现
  await waitForFile("path/to/download/filename");

  //上传时对应的 inputElement 必须是<input>元素
  let inputElement = await page.waitForXPath('//input[@type="file"]');
  await inputElement.uploadFile("/path/to/file");
  browser.close();
})();

模拟用户点击

ts
(async () => {
  const browser = await puppeteer.launch({
    slowMo: 100, //放慢速度
    headless: false,
    defaultViewport: { width: 1440, height: 780 },
    ignoreHTTPSErrors: false, //忽略 https 报错
    args: ["--start-fullscreen"], //全屏打开页面
  });
  const page = await browser.newPage();
  await page.goto("https://demo.youdata.com");
  //输入账号密码
  const uniqueIdElement = await page.$("#uniqueId");
  await uniqueIdElement.type("admin@admin.com", { delay: 20 });
  const passwordElement = await page.$("#password", { delay: 20 });
  await passwordElement.type("123456");
  //点击确定按钮进行登录
  let okButtonElement = await page.$("#btn-ok");
  //等待页面跳转完成,一般点击某个按钮需要跳转时,都需要等待 page.waitForNavigation() 执行完毕才表示跳转成功
  await Promise.all([okButtonElement.click(), page.waitForNavigation()]);
  console.log("admin 登录成功");
  await page.close();
  await browser.close();
})();
  • 其他 API

注意

那么 ElementHandle 都提供了哪些操作元素的函数呢?

elementHandle.click():点击某个元素 elementHandle.tap():模拟手指触摸点击 elementHandle.focus():聚焦到某个元素 elementHandle.hover():鼠标 hover 到某个元素上 elementHandle.type('hello'):在输入框输入文本

请求拦截

ts
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const blockTypes = new Set(["image", "media", "font"]);
  await page.setRequestInterception(true); //开启请求拦截
  page.on("request", (request) => {
    const type = request.resourceType();
    const shouldBlock = blockTypes.has(type);
    if (shouldBlock) {
      //直接阻止请求
      return request.abort();
    } else {
      //对请求重写
      return request.continue({
        //可以对 url,method,postData,headers 进行覆盖
        headers: Object.assign({}, request.headers(), {
          "puppeteer-test": "true",
        }),
      });
    }
  });
  await page.goto("https://demo.youdata.com");
  await page.close();
  await browser.close();
})();
  • 其他 API

注意

那 page 页面上都提供了哪些事件呢?

  • page.on('close') 页面关闭
  • page.on('console') console API 被调用
  • page.on('error') 页面出错
  • page.on('load') 页面加载完
  • page.on('request') 收到请求
  • page.on('requestfailed') 请求失败
  • page.on('requestfinished') 请求成功
  • page.on('response') 收到响应
  • page.on('workercreated') 创建 webWorker
  • page.on('workerdestroyed') 销毁 webWorker