一、背景
- 背景:由于内部的图片加载时间过长,后续经过压缩优化后,有效降低了前后的请求速度和图片大小,现在要收集每个图片的请求时间。
- 整体思路:采用浏览器自动化运行访问并收集数据
二、方案
采用Performance API进行收集(无法统计单个请求的时间)- 采用puppteer控制浏览器进行收集,通过timing API进行获取
三、开发
启动浏览器并获取接口时间
前期调研过发现puppteer无法获取content_download数据。这里采用playwright 进行操控浏览器。
首先先安装playwright ,选择不安装浏览器,我们会启动自己本地的浏览器来进行爬取。
kotlin
npm init playwright@latest
先启动浏览器
js
const playwright = require('playwright');
const CHROME_PATH = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; // chrome路径
// 根据用户的数据进行chrome启动
const ctx = playwright.chromium.launch({
channel: 'chrome',
headless: false, // 把无头模式关闭
executablePath: CHROME_PATH, // chrome的可执行文件路径
devtools: true,
})
// 上面的ctx就是一个浏览器实例,可以打开多个页面
// 开启了一个页面
const page = await browser.newPage();
// 跳转到对应的链接
page.goto('跳转到你的链接')
上面就已经成功启动了本地的chrome,但是会发现页面的状态是没有登录的,那可以不可以用本地的cookie呢,答案是可以的,首先要找到浏览器的cookie存放在哪,可以在 chrome://version 中看到个人资料路径就是你的数据存放地。

在playwright也是可以根据用户的数据存放地进行启动的。
js
const PROFILE_PATH = '~/Library/Application Support/Google/Chrome/';
// 只需要启动的时候launchPersistentContext 使用函数传递数据地址即可
const ctx= playwright.chromium.launchPersistentContext(PROFILE_PATH,{
...config
})
再次启动就能看到有带上用户数据了,下一步就是该去获取接口数据了,可以利用request事件进行监听,然后利用timing API进行获取时间。

js
page.on('request', request => {
const log = {
time: JSON.stringify(request.timing()),
url: request.url(),
// dns解析时间
dns: request.timing().domainLookupEnd - request.timing().domainLookupStart,
// 建立TCP链接的时间,包含SSL握手时间
tcp: request.timing().connectEnd - request.timing().connectStart,
// SSL握手时间
ssl: request.timing().connectEnd - request.timing().secureConnectionStart,
// 发出请求耗时+首个字节时间+下载耗时
request_TTFB_download: request.timing().responseStart - request.timing().requestStart,
// 上面的所有时间
total: request.timing().responseEnd,
}
console.table(log)
})
根据上面的代码和时间就能获取到对应的阶段时间。但是实际的运行结果却是0或者-1,我们查阅文档发现为-1的时候代表该值无效。

那么可以猜测这个是请求已发出就触发了该事件,那当然没有具体的时间,我们需要等待请求完成后再进行时间的获取。
- 方法一:查阅文档我们发现Response中的API有finished函数可以等待完成,代码稍做改造

js
page.on('request', async request => {
request.response().then(async response => {
if(!response) return;
await response.finished()
// log
})
})
- 方法二:可以直接使用requestfinished事件进行监听回调,requestfinished会在请求结束后触发
改造后就能成功获取时间了

下一个问题就是这个页面的图片是懒加载的,我们要等待到当前可视区域的所有的图片请求完成后,让页面往下滚动一屏幕,直到最后一个图片请求完成。那我们如何知道当前页面图片已经加载完成了呢,我们的请求都发出去了,只需要判断当前页面是否还存在未完成的请求即可,如果都完成了就开始向下翻页,直到页面最底完成最后一个请求。
首先可以通过 waitForRequest 这个API进行检查
js
// 先等待1s,让之前的tcp连接断开
await page.waitForTimeout(1000)
// domain 是来判断对应的域名
page.waitForRequest(request => request.url().includes(domain), { timeout });
有了判断条件接下来就是当条件满足的时候去滚动对应的页面,由于这个项目是有iframe的,需要等待iframe先加载完成。没有iframe的话直接等待对应的元素加载完成获取即可
js
const frame = await page.waitForSelector('iframe')
// 获取iframe的节点的元素句柄的内容框架
const frameContent = await frame.contentFrame()
接下来就是等待滚动区域
js
await frameContent.waitForSelector('你的选择器')
// 这里需要当前的一屏幕高度,滚动总高度,当前滚动高度
const heightInfo = await frameContent.evaluate((selector) => {
const element = document.querySelector(selector);
return element ? {
scrollHeight: element.scrollHeight,
scrollTop: element.scrollTop,
clientHeight: element.clientHeight,
} : null;
}, '你的选择器');
有了这些信息后只要当没有请求的时候,就让页面进行滚动就可以直到完成并打开下一页。
现在已经获取到了时间了,那么我们就需要统计数据到excel表格里面。
生成数据
由于我们需要对比前后的数据,我们将每个链接作为一个sheet,对比的时候只需要获取对应sheet的值即可,每列的数据按下面的格式通过xlsx库输出到excel即可
js
const log = {
time: JSON.stringify(request.timing()),
url: request.url(),
// dns解析时间
dns: request.timing().domainLookupEnd - request.timing().domainLookupStart,
// 建立TCP链接的时间,包含SSL握手时间
tcp: request.timing().connectEnd - request.timing().connectStart,
// SSL握手时间
ssl: request.timing().connectEnd - request.timing().secureConnectionStart,
// 发出请求耗时+首个字节时间+下载耗时
request_TTFB_download: request.timing().responseStart - request.timing().requestStart,
// 上面的所有时间
total: request.timing().responseEnd,
}
const dataList = []
dataList.push(log)
初始化文件
js
const xlsx = require('xlsx')
const FILE_NAME = '你要的初始化文件名'
//检查文件是否存在
function checkFileExist() {
return fs.existsSync(FILE_NAME)
}
function createFile() {
if (!checkFileExist()) {
const workbook = xlsx.utils.book_new();
const worksheet = xlsx.utils.json_to_sheet([]);
xlsx.utils.book_append_sheet(workbook, worksheet, 'sheet1');
xlsx.writeFile(workbook, FILE_NAME);
}
}
try {
createFile()
} catch {
}
数据覆盖和写入
js
function appendFile(data, sheetName, fileName = FILE_NAME) {
const workbook = xlsx.readFile(fileName);
const worksheet = xlsx.utils.json_to_sheet(data);
// 如果表里有重名的需要先删掉再覆盖
if (workbook.Sheets[sheetName]) {
// 删除表
delete workbook.Sheets[sheetName];
// 更新 SheetNames 数组
workbook.SheetNames = workbook.SheetNames.filter(sheetNameItem => sheetNameItem !== sheetName);
// 保存更新后的工作簿
xlsx.writeFile(workbook, fileName, {bookType: 'xlsx'});
}
xlsx.utils.book_append_sheet(workbook, worksheet, sheetName);
xlsx.writeFile(workbook, fileName)
}
总结
playwright或者puppteer可以做的事情不止这么点,我们要多发现工作中的一些重复而且繁琐的事情,让脚本代替我们完成,这样可以节省时间去干别的更加有意义的事情