【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

自动化 Web 性能分析之 Puppeteer 爬虫实践

  • 2021-01-28
  • 本文字数:7011 字

    阅读完需:约 23 分钟

自动化 Web 性能分析之 Puppeteer 爬虫实践

本文将向大家介绍自动化性能分析使用的核心库——Puppeteer,并结合页面登录场景,介绍 Puppeteer 在百策系统中的应用。

Puppeteer 简介


Puppeteer 是一个 Node 库,它提供了一整套高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。正如其翻译为“操纵木偶的人”一样, 你可以通过 Puppeteer 的提供的 API 直接控制 Chrome,模拟大部分用户操作来进行 UI 测试或者作为爬虫访问页面来收集数据。

Puppeteer 用途


  • 生成页面的屏幕截图和 PDF。

  • 爬取 SPA 应用,并生成预渲染内容(即 SSR 服务端渲染)。

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

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

  • 捕获页面的时间轴来帮助诊断性能问题。

  • 测试 Chrome 扩展程序。

  • 从页面抓取所需要的内容。


Puppeteer 安装


阅读 Puppeteer 的 官方 API 你会发现满屏的 asyncawait ,这些都是 ES7 的规范,所以你需要:


  • Node.js 的版本不能低于 v7.6.0,因为需要支持 asyncawait

  • 需要最新的 Chrome Driver, 这个你在通过 npm 安装 Puppeteer 的时候系统会自动下载的。


配置淘宝的 Puppeteer 下载源,用于安装 Chromium

国内环境若不配置,会卡在下载 Chromium ,你可以这样切换 npm 源

npm config set registry https://registry.npm.taobao.org

export PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors

npm i puppeteer


初探 Puppeteer:从页面截图开始


实现页面截图,首先我们需要创建一个浏览器实例,然后打开一个页面,加载指定的 URL,在打开的页面上触发截图操作,最后再将浏览器关闭。因此,我们需要用到以下 API:


  • puppeteer.launch([options])  启动浏览器实例

  • browser.newPage()  创建一个 Page 对象

  • page.goto(url[,options])  跳转至指定页面

  • page.screenshot([options])  进行页面截图

  • browser.close()   关闭 Chromium 及其所有页面


实现代码如下:


const puppeteer = require('puppeteer'); (async () => {  const browser = await puppeteer.launch({    // 是否运行浏览器无头模式(boolean)    headless: false,    // 是否自动打开调试工具(boolean),若此值为true,headless自动置为fasle    devtools: true,    // 设置超时时间(number),若此值为0,则禁用超时    timeout: 20000,  });
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
await page.screenshot({ // 截图保存路径(string) path: './one.png', // 是否保存完整页面(boolean) fullPage: true }); await browser.close();})();
复制代码


执行完以上代码,我们就可以在当前路径找到 one.png,我们打开就可以看到如下截图:



又探 Puppeteer:自动测试页面性能


我们知道 Web Performance 接口允许页面中的 JavaScript 代码可以通过具体的函数测量当前网页页面或者 Web 应用的性能。为能在页面执行 JavaScript 从而来检测页面性能,我们就需要用到以下 API:


  • page.evaluate(pageFunction[, ...args])  在浏览器中执行此函数,返回一个 Promise 对象


const puppeteer = require('puppeteer');
// 检测页面urlconst url = 'https://www.zhengcaiyun.cn';// 检测次数const times = 5;const record = [];
(async () => { for (let i = 0; i < times; i++) { const browser = await puppeteer.launch({headless: false}); const page = await browser.newPage(); await page.goto(url); // 等待保证页面加载完成 await page.waitFor(5000);
// 获取页面的 window.performance 属性 const timing = JSON.parse(await page.evaluate( () =>JSON.stringify(window.performance.timing) )); record.push(calculate(timing)); await browser.close(); }
let whiteScreenTime = 0, requestTime = 0;
for (let item of record) { whiteScreenTime += item.whiteScreenTime; requestTime += item.requestTime; } // 检测计算结果 const result = []; result.push(url); result.push(`页面平均白屏时间为:${whiteScreenTime / times} ms`); result.push(`页面平均请求时间为:${requestTime / times} ms`); console.log(result);
function calculate(timing) { const result = {}; // 白屏时间 result.whiteScreenTime = timing.responseStart - timing.navigationStart; // 请求时间 result.requestTime = timing.responseEnd - timing.responseStart; return result; }})();
复制代码


执行完以上代码,我们就可以在终端看到检测页面的基本性能信息:



双探 Puppeteer:爬取苏宁易购的商品信息


打开电商首页,输入想要的商品名称,点击搜索按钮,跳转至相应的商品列表页,然后一页页浏览,从而找到心仪的商品,这大概就是我们平时网购的样子。那么如何让浏览器自动执行以上步骤,同时还能抽空爬取每页的商品信息,顺便将信息导出至文件呢?为此,我们需要用到以下 API:


  • page.title()  获取页面标题

  • page.type(selector, text[, options])  获取输入框焦点并输入内容

  • page.click(selector[, options])  点击要选择的元素

  • page.waitForNavigation([options])  等待页面跳转

  • page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])   页面等待时间

  • fs.createWriteStream   对文件流进行写入

  • window.scrollBy(xnum, ynum)  页面向右、向下滑动的像素值


const fs = require('fs');const puppeteer = require('puppeteer');
// 本次模拟获取苏宁易购的数据,来抓取在售的所有笔记本电脑信息~(async () => { const browser = await (puppeteer.launch({ headless: false })); const page = await browser.newPage();
// 进入页面 // await page.goto('https://search.suning.com/笔记本电脑/'); await page.goto('https://www.suning.com'); // 获取页面标题 let title = await page.title(); console.log(title);
// 点击搜索框拟人输入“笔记本电脑” await page.type('#searchKeywords', '笔记本电脑', { delay: 500 });
// 点击搜索按钮 await page.click('.search-btn'); // await page.click('#searchKeywords'); // await page.type('#searchKeywords', String.fromCharCode(13));
// 等待页面跳转,注意:如果 click() 触发了一个跳转,会有一个独立的 page.waitForNavigation()对象需要等待 await page.waitForNavigation();
// 获取当前搜索项商品最大页数,为节约爬取时间,暂只爬取前5页数据 // const maxPage = await page.evaluate(() => { // return Number($('#bottomPage').attr('max')); // }) const maxPage = 5;
let allInfo = []; for (let i = 0; i < maxPage; i++) { // 因为苏宁页面的商品信息用了懒加载,所以需要把页面滑动到最底部,保证所有商品数据都加载出来 await autoScroll(page); // 保证每个商品信息都加载出来 await page.waitFor(5000); // 获取每个 const SHOP_LIST_SELECTOR = 'ul.general.clearfix'; const shopList = await page.evaluate((sel) => { const shopBoxs = Array.from($(sel).find('li div.res-info')); const item = shopBoxs.map(v => { // 获取每个商品的名称、品牌、价格 const title = $(v).find('div.title-selling-point').text().trim(); const brand = $(v).find('b.highlight').text().trim(); const price = $(v).find('span.def-price').text().trim(); return { title, brand, price, }; }); return item; }, SHOP_LIST_SELECTOR); allInfo = [...allInfo, ...shopList];
// 当当前页面并非最大页的时候,跳转到下一页 if (i < maxPage - 1) { const nextPageUrl = await page.evaluate(() => { const url = $('#nextPage').get(0).href; return url; }); await page.goto(nextPageUrl, { waitUntil:'networkidle0' }); // waitUntil对应的参数如下: // load - 页面的load事件触发时 // domcontentloaded - 页面的 DOMContentLoaded 事件触发时 // networkidle0 - 不再有网络连接时触发(至少500毫秒后) // networkidle2 - 只有2个网络连接时触发(至少500毫秒后) } } console.log(`共获取到${allInfo.length}台笔记本电脑信息`);
// 将笔记本电脑信息写入文件 writerStream = fs.createWriteStream('notebook.json'); writerStream.write(JSON.stringify(allInfo, undefined, 2), 'UTF8'); writerStream.end(); browser.close();
// 滑动屏幕,滚至页面底部 function autoScroll(page) { return page.evaluate(() => { returnnewPromise((resolve) => { var totalHeight = 0; var distance = 100; // 每200毫秒让页面下滑100像素的距离 var timer = setInterval(() => { var scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); resolve(); } }, 200); }) }); }})();
复制代码


执行完以上代码,我们就可以在终端看到爬取的笔记本电脑总数:


同时我们可以在当前路径找到 notebook.json 文件,打开可以看到所有爬取的笔记本电脑信息:



叒探 Puppeteer:“百策系统”实现模拟登录


以下内容是对上次“百策系统”的分享《自动化 Web 性能优化分析方案》内容的后续补充,要是不了解“百策系统”的同学可以先补补课哈。


当“百策系统”分析需要登录的页面时,如何模拟用户的登录行为呢?比如检测我们政采云的后台页面,我们就需要先分辨出当前页面处于哪个环境,其次跳转至对应环境的登录页面,之后再输入账号密码,待登录完成后,跳转至后台页面的 URL,再进行页面后续的操作。那么如何实现以上功能呢,这里就需要用到以下 API:


  • browser.createIncognitoBrowserContext()  创建一个匿名浏览器上下文,这将不会与其他浏览器上下文分享 cookies/cache

  • page.waitForSelector(selector[, options])  等待指定的选择器匹配的元素出现在页面中

  • page.$eval(selector, pageFunction[, ...args])  此方法在页面内执行 document.querySelector,然后把匹配到的元素作为第一个参数传给 pageFunction


const puppeteer = require('puppeteer');
// 根据不同环境的页面,返回对应环境下登录的 urlconst getLoginPath = target => { if (target.includes('-staging.zcygov.cn')) { return'https://login-staging.zcygov.cn/user-login/'; } elseif (target.includes('test.zcygov.cn')) { return'http://login.test.zcygov.cn/user-login/'; } else { return'https://login.zcygov.cn/user-login/'; }};
asyncfunction loginSimulation(url, options) { const browser = await puppeteer.launch(); // 创建一个匿名的浏览器上下文,这将不会与其他浏览器上下文分享 cookies/cache。 const context = await browser.createIncognitoBrowserContext(); const page = await context.newPage();
// waitUntil对应的参数如下: // load - 页面的load事件触发时 // domcontentloaded - 页面的 DOMContentLoaded 事件触发时 // networkidle0 - 不再有网络连接时触发(至少500毫秒后) // networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
// 若参数中有用户名密码,则先到登录页面进行登录再进行性能检测 if (options.username && options.password) { // 跳转至相应的登录页面 await page.goto(getLoginPath(url), { waitUntil: 'networkidle0' }); // 输入用户账号 await page.type('.login-form #username', options.username); // 输入用户密码 await page.type('.login-form #password', options.password); // 点击登录按钮 await page.click('.login-form .password-login'); // 等待页面跳转,注意:如果 click() 触发了一个跳转,会有一个独立的 page.waitForNavigation()对象需要等待 await page.waitForNavigation(); // 若跳转之后的页面仍处在登录页,说明登录出错 const pUrl = await page.url(); if (pUrl.includes('login')) { await page.waitForSelector('.form-content > .error-text > .text'); // 获取错误信息内容 const errorText = await page.$eval('.form-content > .error-text > .text', el => el.textContent.trim()); // 报出错误信息 thrownewError(`政采云登录失败,${errorText}`); } }};
复制代码


叕探 Puppeteer:搞定滑动解锁


目前有许多站点的登录页面都添加了滑动解锁校验,这无疑我们对页面信息的爬取增加了难度,但是技术都是在互相碰撞中进步的。我们不仅要直面这座大山,还要想着跨越过去,为此,我们需要用到以下 API:


  • CanvasRenderingContext2D.getImageData()   返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据

  • page.$(selector)   此方法在页面内执行 document.querySelector

  • page.mouse.down([options])  触发一个 mousedown 事件

  • page.mouse.move([options])  触发一个 mousemove 事件

  • page.mouse.up([options])  触发一个mouseup事件


const puppeteer = require('puppeteer');
(asyncfunction run() { const browser = await puppeteer.launch({ headless: false, defaultViewport: { width: 1200, height: 600 } }); page = await browser.newPage(); // 1.打开 bilibili 登录页面 await page.goto('https://passport.bilibili.com/login'); await page.waitFor(3000);
// 3.输入账号密码 await page.type('input#login-username','你的账号', { delay: 50 }); await page.type('input#login-passwd','你的密码', { delay: 50 });
// 4.点登陆按钮 await page.click('.btn.btn-login');
// 保证滑动弹窗加载出 await page.waitFor(3000);
// 获取像素差较大的最左侧横坐标 const diffX = await page.evaluate(() => { const fullbg = document.querySelector('.geetest_canvas_fullbg'); // 完成图片 const bg = document.querySelector('.geetest_canvas_bg'); // 带缺口图片 const diffPixel = 40; // 像素差
// 滑动解锁的背景图片的尺寸为 260*160 // 拼图右侧离背景最左侧距离为 46px,故从 47px 的位置开始检测 for(let i = 47; i < 260; i++) { for(let j = 1; j < 160; j++) { const fullbgData = fullbg.getContext("2d").getImageData(i, j, 1, 1).data; const bgData = bg.getContext("2d").getImageData(i, j, 1, 1).data; const red = Math.abs(fullbgData[0] - bgData[0]); const green = Math.abs(fullbgData[1] - bgData[1]); const blue = Math.abs(fullbgData[2] - bgData[2]); // 若找到两张图片在同样像素点中,red、green、blue 有一个值相差较大,即可视为缺口图片中缺口的最左侧横坐标位置 if(red > diffPixel || green > diffPixel || blue > diffPixel) { return i; } } } });
// 获取滑动按钮在页面中的坐标 const dragButton = await page.$('.geetest_slider_button'); const box = await dragButton.boundingBox(); // 获取滑动按钮中心点位置 const x = box.x + (box.width / 2); const y = box.y + (box.height / 2);
// 鼠标滑动至滑动按钮中心点 await page.mouse.move(x, y); // 按下鼠标 await page.mouse.down(); // 慢慢滑动至缺口位置,因起始位置有约 7px 的偏差,故终点值为 x + diffX - 7 for (let i = x; i < x + diffX - 7; i = i + 5) { // 滑动鼠标 await page.mouse.move(i, y); } // 假装有个停顿,看起来更像是人为操作 await page.waitFor(200); // 放开鼠标 await page.mouse.up();
await page.waitFor(5000); await browser.close();})();
复制代码


执行完以上代码,来看下实现效果:

结语


当然, Puppeteer 的强大不止于此,我们可以通过 Puppeteer 实现更多有意思的功能,比如使用 Puppeteer 来检测页面图片是否使用懒加载,后续我们会对其功能的实现进行的分享。



头图:Unsplash

作者:小妖

原文:https://mp.weixin.qq.com/s/tfC7SAJ2r2UclFUj4vE-tQ

原文:自动化 Web 性能分析之 Puppeteer 爬虫实践

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-01-28 23:282716

评论

发布
暂无评论
发现更多内容

浅谈数据中台

数据社

大数据 数据中台 数据仓库

程序员职业生涯的八点感想

池建强

程序员 职业

两边夹的应用二

孙苏勇

算法 两边夹 重排序 函数式接口 Lambda

最近看了两本书:The Rules of Life 和 Make Big Happen

霍太稳@极客邦科技

创业 团队管理 自我管理

一文讲清楚 MySQL 事务隔离级别和实现原理,开发人员必备知识点

古时的风筝

MySQL 数据库 事务隔离级别 mysql事务 数据库事务

阿里笔记之数据模型

迹_Jason

大数据

求稳不得

孙苏勇

职业 发展 职场

微服务架构深度解析与最佳实践 - 第六部分

kimmking

微服务 最佳实践 深度解析 高可用

两边夹的应用

孙苏勇

算法 积水问题 两边夹

平均响应1000ms到200ms,PHP和Go那家强?

拖地先生

php 架构 性能优化 后台开发 运维

一个运营经理人的工作两周年总结

霍太稳@极客邦科技

高效工作 身心健康 项目管理 自我管理

微服务架构深度解析与最佳实践 - 第七部分

kimmking

微服务 最佳实践 深度解析 高可用

聊聊:Python

谢烟客

Python 人工智能 编程

凡事必先骑上虎背

Steve

学习 态度 方法论

【译】Rust 开发者的2019

WasmEdge

程序员 rust

微服务架构深度解析与最佳实践 - 第五部分

kimmking

微服务 最佳实践 深度解析 高可用

微服务架构深度解析与最佳实践(全篇汇总)

kimmking

微服务 最佳实践 深度解析 高可用

越是困难,越是要做有分析判断能力的人

霍太稳@极客邦科技

创业 团队管理 个人成长

微服务架构深度解析与最佳实践 - 第四部分

kimmking

微服务 最佳实践 深度解析 高可用

黄金思维圈,养成透过现象看本质的能力

非著名程序员

读书笔记 程序员 程序人生 提升认知

微服务架构深度解析与最佳实践-第一部分

kimmking

微服务 最佳实践 深度解析 高可用

微服务架构深度解析与最佳实践-第二部分

kimmking

微服务 最佳实践 深度解析 高可用

小程序的当下和未来可能 | GMTC.2019深圳站演讲文稿

崔红保

小程序 uni-app

微服务架构深度解析与最佳实践 - 第三部分

kimmking

微服务 最佳实践 深度解析 高可用

你不是迷茫,只是缺乏目标

Steve

学习 身心健康 方法 自我管理

数据分析师应该了解的数据湖

数据社

大数据 数据仓库 数据湖 数据分析

亚马逊云 AWS LightSail 搭建高性能 LNMP 环境并安全部署 Wordpress

蛋仔小蚂蚁

nginx Wordpress 部署 SELinux 安全上下文配置 亚马逊云 AWS Lightsail 安全

2019 年

贾献华

2020 2019 总结 日历 计划

归去来兮:递归

曲镇

算法

一个独立开发者,他是如何做到月入 20 万的?

非著名程序员

程序员 独立开发者 副业赚钱 开发者 程序人生

申请鲲鹏920测试机试水+编译nginx

草宝虫

nginx 鲲鹏920 centos7 armv8

自动化 Web 性能分析之 Puppeteer 爬虫实践_语言 & 开发_政采云前端团队_InfoQ精选文章