自動化 Web 性能分析之 Puppeteer 爬蟲實踐

原創不易,但願能關注下咱們,再順手點個贊~~

本文首發於政採雲前端團隊博客: 自動化 Web 性能分析之 Puppeteer 爬蟲實踐javascript

經過上篇文章《自動化 Web 性能優化分析方案》的分享想必你們對「百策系統」有了初步的瞭解。本文將向你們介紹自動化性能分析使用的核心庫——Puppeteer,並結合頁面登陸場景,介紹 Puppeteer 在百策系統中的應用。前端

Puppeteer 簡介

Puppeteer 是一個 Node 庫,它提供了一整套高級 API 來經過 DevTools 協議控制 Chromium 或 Chrome。正如其翻譯爲「操縱木偶的人」同樣, 你能夠經過 Puppeteer 的提供的 API 直接控制 Chrome,模擬大部分用戶操做來進行 UI 測試或者做爲爬蟲訪問頁面來收集數據。java

Puppeteer 用途

  • 生成頁面的屏幕截圖和 PDF。
  • 爬取 SPA 應用,並生成預渲染內容(即 SSR 服務端渲染)。
  • 自動執行表單提交、UI測試、鍵盤輸入等。
  • 建立最新的自動化測試環境,使用最新的 JavaScript 和瀏覽器功能,直接在最新版本的 Chrome 中運行測試。
  • 捕獲頁面的時間軸來幫助診斷性能問題。
  • 測試 Chrome 擴展程序。
  • 從頁面抓取所須要的內容。

Puppeteer 安裝

閱讀 Puppeteer 的 官方 API 你會發現滿屏的 asyncawait ,這些都是 ES7 的規範,因此你須要:git

  • Node.js 的版本不能低於 v7.6.0,由於須要支持 asyncawaitgithub

  • 須要最新的 Chrome Driver, 這個你在經過 npm 安裝 Puppeteer 的時候系統會自動下載的。npm

# 配置淘寶的 Puppeteer下載源,用於安裝 Chromium
# 國內環境若不配置,會卡在下載 Chromium ,你能夠這樣切換 npm 源

npm config set registry registry.npm.taobao.orgjson

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

npm i puppeteerc#

初探 Puppeteer:從頁面截圖開始

實現頁面截圖,首先咱們須要建立一個瀏覽器實例,而後打開一個頁面,加載指定的 URL,在打開的頁面上觸發截圖操做,最後再將瀏覽器關閉。所以,咱們須要用到如下 API: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,咱們打開就能夠看到以下截圖:

Image text

又探 Puppeteer:自動測試頁面性能

咱們知道 Web Performance 接口容許頁面中的 JavaScript 代碼能夠經過具體的函數測量當前網頁頁面或者 Web 應用的性能。爲能在頁面執行 JavaScript 從而來檢測頁面性能,咱們就須要用到如下 API:

  • page.evaluate(pageFunction[, ...args]) 在瀏覽器中執行此函數,返回一個 Promise 對象
const puppeteer = require('puppeteer');

// 檢測頁面url
const 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;
  }
})();
複製代碼
執行完以上代碼,咱們就能夠在終端看到檢測頁面的基本性能信息:

image

雙探 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(() => {
      return new Promise((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);
      })
    });
  }
})();
複製代碼
執行完以上代碼,咱們就能夠在終端看到爬取的筆記本電腦總數:

Image text

同時咱們能夠在當前路徑找到 notebook.json 文件,打開能夠看到全部爬取的筆記本電腦信息:

Image text

叒探 Puppeteer:「百策系統」實現模擬登陸

如下內容是對上次「百策系統」的分享《自動化 Web 性能優化分析方案》內容的後續補充,要是不瞭解「百策系統」的同窗能夠先補補課哈。

當「百策系統」分析須要登陸的頁面時,如何模擬用戶的登陸行爲呢?好比檢測咱們政採雲的後臺頁面,咱們就須要先分辨出當前頁面處於哪一個環境,其次跳轉至對應環境的登陸頁面,以後再輸入帳號密碼,待登陸完成後,跳轉至後臺頁面的 URL,再進行頁面後續的操做。那麼如何實現以上功能呢,這裏就須要用到如下 API:

  • browser.createIncognitoBrowserContext() 建立一個匿名瀏覽器上下文,這將不會與其餘瀏覽器上下文分享 cookies/cache
  • page.waitForSelector(selector[, options]) 等待指定的選擇器匹配的元素出如今頁面中
  • page.$eval(selector, pageFunction[, ...args]) 此方法在頁面內執行 document.querySelector,而後把匹配到的元素做爲第一個參數傳給 pageFunction
const puppeteer = require('puppeteer');

// 根據不一樣環境的頁面,返回對應環境下登陸的 url
const getLoginPath = target => {
  if (target.includes('-staging.zcygov.cn')) {
    return 'https://login-staging.zcygov.cn/user-login/';
  } else if (target.includes('test.zcygov.cn')) {
    return 'http://login.test.zcygov.cn/user-login/';
  } else {
    return 'https://login.zcygov.cn/user-login/';
  }
};

async function 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());
      // 報出錯誤信息
      throw new Error(`政採雲登陸失敗,${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');

(async function 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();
})();
複製代碼
執行完以上代碼,來看下實現效果:

Image text

結語

固然, Puppeteer 的強大不止於此,咱們能夠經過 Puppeteer 實現更多有意思的功能,好比使用 Puppeteer 來檢測頁面圖片是否使用懶加載,後續咱們會對其功能的實現進行的分享,也請持續關注咱們微信公衆號「政採雲前端團隊」以及關注咱們掘金帳號。

引用資料

招賢納士

招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪~ 若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變「5年工做時間3年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給ZooTeam@cai-inc.com

推薦閱讀

前端工程實踐之可視化搭建系統(一)

自動化 Web 性能優化分析方案

看完這篇,你也能把 React Hooks 玩出花

相關文章
相關標籤/搜索