無頭瀏覽器 Puppeteer 初探

螞蟻金服資料體驗技術發表於2017-10-17

作者簡介 felix 螞蟻金服·資料體驗技術團隊

我們日常使用瀏覽器的步驟為:啟動瀏覽器、開啟一個網頁、進行互動。而無頭瀏覽器指的是我們使用指令碼來執行以上過程的瀏覽器,能模擬真實的瀏覽器使用場景。

有了無頭瀏覽器,我們就能做包括但不限於以下事情:

  • 對網頁進行截圖儲存為圖片或 pdf
  • 抓取單頁應用(SPA)執行並渲染(解決傳統 HTTP 爬蟲抓取單頁應用難以處理非同步請求的問題)
  • 做表單的自動提交、UI的自動化測試、模擬鍵盤輸入等
  • 用瀏覽器自帶的一些除錯工具和效能分析工具幫助我們分析問題
  • 在最新的無頭瀏覽器環境裡做測試、使用最新瀏覽器特性
  • 寫爬蟲做你想做的事情~

無頭瀏覽器很多,包括但不限於:

  • PhantomJS, 基於 Webkit
  • SlimerJS, 基於 Gecko
  • HtmlUnit, 基於 Rhnio
  • TrifleJS, 基於 Trident
  • Splash, 基於 Webkit

本文主要介紹 Google 提供的無頭瀏覽器(headless Chrome), 他基於 Chrome DevTools protocol 提供了不少高度封裝的介面方便我們控制瀏覽器。

簡單的程式碼示例

為了能使用 async/await 等新特性,需要使用 v7.6.0 或更高版本的 Node.

啟動/關閉瀏覽器、開啟頁面

    // 啟動瀏覽器
    const browser = await puppeteer.launch({
        // 關閉無頭模式,方便我們看到這個無頭瀏覽器執行的過程
        // headless: false,
        timeout: 30000, // 預設超時為30秒,設定為0則表示不設定超時
    });

    // 開啟空白頁面
    const page = await browser.newPage();

    // 進行互動
    // ...

    // 關閉瀏覽器
    // await browser.close();
複製程式碼

設定頁面視窗大小

    // 設定瀏覽器視窗
    page.setViewport({
        width: 1376,
        height: 768,
    });
複製程式碼

輸入網址

    // 位址列輸入網頁地址
    await page.goto('https://google.com/', {
        // 配置項
        // waitUntil: 'networkidle', // 等待網路狀態為空閒的時候才繼續執行
    });
複製程式碼

儲存網頁為圖片

開啟一個網頁,然後截圖儲存到本地:

await page.screenshot({
    path: 'path/to/saved.png',
});
複製程式碼

完整示例程式碼

儲存網頁為 pdf

開啟一個網頁,然後儲存 pdf 到本地:

await page.pdf({
     path: 'path/to/saved.pdf',
    format: 'A4', // 儲存尺寸
});
複製程式碼

完整示例程式碼

執行指令碼

要獲取開啟的網頁中的宿主環境,我們可以使用 Page.evaluate 方法:

// 獲取視窗資訊
const dimensions = await page.evaluate(() => {
    return {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight,
        deviceScaleFactor: window.devicePixelRatio
    };
});
console.log('視窗資訊:', dimensions);

// 獲取 html
// 獲取上下文控制程式碼
const htmlHandle = await page.$('html');

// 執行計算
const html = await page.evaluate(body => body.outerHTML, htmlHandle);

// 銷燬控制程式碼
await htmlHandle.dispose();

console.log('html:', html);
複製程式碼

Page.$ 可以理解為我們常用的 document.querySelector, 而 Page.$$ 則對應 document.querySelectorAll

完整示例程式碼

自動提交表單

開啟谷歌首頁,輸入關鍵字,回車進行搜尋:

// 位址列輸入網頁地址
await page.goto('https://google.com/', {
    waitUntil: 'networkidle', // 等待網路狀態為空閒的時候才繼續執行
});

// 聚焦搜尋框
// await page.click('#lst-ib');
await page.focus('#lst-ib');

// 輸入搜尋關鍵字
await page.type('辣子雞', {
   delay: 1000, // 控制 keypress 也就是每個字母輸入的間隔
});

// 回車
await page.press('Enter');
複製程式碼

無頭瀏覽器 Puppeteer 初探

完整示例程式碼

複雜點的程式碼示例

每一個簡單的動作連線起來,就是一連串複雜的互動,接下來我們看兩個更具體的示例。

抓取單頁應用: 模擬餓了麼外賣下單

傳統的爬蟲是基於 HTTP 協議,模擬 UserAgent 傳送 http 請求,獲取到 html 內容後使用正則解析出需要抓取的內容,這種方式面對服務端渲染直出 html 的網頁時非常便捷。

但遇到單頁應用(SPA)時,或遇到登入校驗時,這種爬蟲就顯得比較無力。

而使用無頭瀏覽器,抓取網頁時完全使用了人機互動時的操作,所以頁面的初始化完全能使用宿主瀏覽器環境渲染完備,不再需要關心這個單頁應用在前端初始化時需要涉及哪些 HTTP 請求。

無頭瀏覽器提供的各種點選、輸入等指令,完全模擬人的點選、輸入等指令,也就再也不用擔心正則寫不出來了啊哈哈哈

當然,有些場景下,使用傳統的 HTTP 爬蟲(寫正則匹配) 還是比較高效的。

在這裡就不再詳細對比這些差異了,以下這個例子僅作為展示模擬一個完整的人機互動:使用移動版餓了麼點外賣。

先看下效果:

無頭瀏覽器 Puppeteer 初探

程式碼比較長就不全貼了,關鍵是幾行:

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone6 = devices['iPhone 6'];

console.log('啟動瀏覽器');
const browser = await puppeteer.launch();

console.log('開啟頁面');
const page = await browser.newPage();

// 模擬移動端裝置
await page.emulate(iPhone6);

console.log('位址列輸入網頁地址');
await page.goto(url);

console.log('等待頁面準備好');
await page.waitForSelector('.search-wrapper .search');

console.log('點選搜尋框');
await page.tap('.search-wrapper .search');

await page.type('麥當勞', {
    delay: 200, // 每個字母之間輸入的間隔
});

console.log('回車開始搜尋');
await page.tap('button');

console.log('等待搜素結果渲染出來');
await page.waitForSelector('[class^="index-container"]');

console.log('找到搜尋到的第一家外賣店!');
await page.tap('[class^="index-container"]');


console.log('等待選單渲染出來');
await page.waitForSelector('[class^="fooddetails-food-panel"]');


console.log('直接選一個菜品吧');
await page.tap('[class^="fooddetails-cart-button"]');

// console.log('===為了看清楚,傲嬌地等兩秒===');
await page.waitFor(2000);
await page.tap('[class^=submit-btn-submitbutton]');

// 關閉瀏覽器
await browser.close();
複製程式碼

關鍵步驟是:

  • 載入頁面
  • 等待需要點選的 DOM 渲染出來後點選
  • 繼續等待下一步需要點選的 DOM 渲染出來再點選

關鍵的幾個指令:

  • page.tap(或 page.click) 為點選
  • page.waitForSelector 意思是等待指定元素出現在網頁中,如果已經出現了,則立即繼續執行下去, 後面跟的引數為 selector 選擇器,與我們常用的 document.querySelector 接收的引數一致
  • page.waitFor 後面可以傳入 selector 選擇器、function 函式或 timeout 毫秒時間,如 page.waitFor(2000) 指等待2秒再繼續執行,例子中用這個函式暫停操作主要是為了演示

以上幾個指令都可接受一個 selector 選擇器作為引數,這裡額外介紹幾個方法:

  • page.$(selector) 與我們常用的 document.querySelector(selector) 一致,返回的是一個 ElementHandle 元素控制程式碼
  • page.$$(selector) 與我們常用的 document.querySelectorAll(selector) 一致,返回的是一個陣列

在有頭瀏覽器上下文中,我們選擇一個元素的方法是:

const body = document.querySelector('body');
const bodyInnerHTML = body.innerHTML;
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製程式碼

而在無頭瀏覽器裡,我們首先需要獲取一個控制程式碼,通過控制程式碼獲取到環境中的資訊後,銷燬這個控制程式碼。

// 獲取 html
// 獲取上下文控制程式碼
const bodyHandle = await page.$('body');
// 執行計算
const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle);
// 銷燬控制程式碼
await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);
複製程式碼

除此之外,還可以使用 page.$eval:

const bodyInnerHTML = await page.$eval('body', dom => dom.innerHTML);
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製程式碼

page.evaluate 意為在瀏覽器環境執行指令碼,可傳入第二個引數作為控制程式碼,而 page.$eval 則針對選中的一個 DOM 元素執行操作。

完整示例程式碼

匯出批量網頁:下載圖靈圖書

我在 圖靈社群 上買了不少電子書,以前支援推送到 mobi 格式到 kindle 或推送 pdf 格式到郵箱進行閱讀,不過經常會關閉這些推送渠道,只能留在網頁上看書。

對我來說不是很方便,而這些書籍的線上閱讀效果是伺服器渲染出來的(帶了大量標籤,無法簡單抽取出好的排版),最好的方式當然是直接線上閱讀並儲存為 pdf 或圖片了。

藉助瀏覽器的無頭模式,我寫了個簡單的下載已購買書籍為 pdf 到本地的指令碼,支援批量下載已購買的書籍。

使用方法,傳入帳號密碼和儲存路徑,如:

$ node ./demo/download-ituring-books.js '使用者名稱' '密碼' './books'
複製程式碼

注意:puppeteerPage.pdf() 目前僅支援在無頭模式中使用,所以要想看有頭狀態的抓取過程的話,執行到 Page.pdf() 這步會先報錯:

無頭瀏覽器 Puppeteer 初探

所以啟動這個指令碼時,需要保持無頭模式:

const browser = await puppeteer.launch({
    // 關閉無頭模式,方便我們看到這個無頭瀏覽器執行的過程
    // 注意若呼叫了 Page.pdf 即儲存為 pdf,則需要保持為無頭模式
    // headless: false,
});
複製程式碼

看下執行效果:

無頭瀏覽器 Puppeteer 初探

我的書架裡有20多本書,下載完後是這樣子:

無頭瀏覽器 Puppeteer 初探

完整示例程式碼

無頭瀏覽器還能做什麼?

無頭瀏覽器說白了就是能模擬人工在有頭瀏覽器中的各種操作。那自然很多人力活,都能使用無頭瀏覽器來做(比如上面這個下載 pdf 的過程,其實是人力開啟每一個文章頁面,然後按 ctrl+pcommand+p 儲存到本地的自動化過程)。

那既然用自動化工具能解決的事情,就不應該浪費重複的人力勞動了,除此之外我們還可以做:

  • 自動化工具 如自動提交表單,自動下載
  • 自動化 UI 測試 如記錄下正確 DOM 結構或截圖,然後自動執行指定操作後,檢查 DOM 結構或截圖是否匹配(UI 斷言)
  • 定時監控工具 如定時截圖發週報,或定時巡查重要業務路徑下的頁面是否處於可用狀態,配合郵件告警
  • 爬蟲 如傳統 HTTP 爬蟲爬不到的地方,就可配合無頭瀏覽器渲染能力來做
  • etc

感興趣的同學可以關注專欄或者傳送簡歷至'qingsheng.lqs####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章