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