使用 PhantomJS 抓取網頁截圖

網頁截圖很簡單,按下 PrintScreen 就行了,可是如果要大量、批次的抓取,該怎麼辦呢?試試無介面瀏覽器,開多隻程式同時撈截圖吧!


撈截圖的工具很多,比較多人討論與使用的是 PhantomJS,他除了截圖外,還能做自動化測試,測試網頁效能。不過好景不常,今年 Chrome 59 新增無介面瀏覽器 (Headless) 功能,讓 Google Chrome 能直接以命令列執行,做 PhantomJS 能做的事啦。比起PhantomJS,Headless Chrome 使用新版本的渲染引擎,效能較好,不會一直吃記憶體。Google 給的說明如下:

Headless Chrome is similar to tools like PhantomJS. Both can be used for automated testing in a headless environment. The main difference between the two is that Phantom uses an older version of WebKit as its rendering engine while Headless Chrome uses the latest version of Blink.

除此之外,PhantomJS 的核心開發者 Vitaly Slobodin 表示不再維護這個 Project,因為大家最後會轉向使用 Headless Chrome。那麼既然 Headless Chrome這麼棒,為什麼還要寫這篇介紹 PhantomJS 呢?因為是怨氣阿!之前做研究被 PhantomJS 雷很久,他會莫名其妙 Crash,卡住的時候就需要自己 Kill,很麻煩…

所以就稍微紀錄之前怎麼做的,如果你也需要用到他,避免踩同樣的雷


甚麼是 PhantomJS

PhantomJS 是無介面瀏覽器 (Headless browser),背後的渲染引擎為 WebKit,適合進行自動化測試、捕捉截圖或監測網頁效能。因為沒有圖形介面,很容易整合至現有的測試框架,比如 Jasmine、QUnit、Mocha 等等,或是使用基於 PhantomJS 開發的測試框架 CasperJS。另一個特點是他能直接控制 DOM,方便你提取網頁中的元素內容,或是使用 JQuery 進行操作。新手上路請參考 PhantomJS 網頁上的 QuickStart,以下我們就安裝與網頁截圖的技巧做說明。


安裝與環境設定

先確認已安裝相依套件,接著從 bitbucket 下載最新的 2.11 版本,並設定路徑 (因為 Chrome Headless 的推出,PhantomJS 應該不會再更新了)

sudo apt-get -y install build-essential chrpath libssl-dev libxft-dev
sudo apt-get -y install libfreetype6 libfreetype6-dev
sudo apt-get -y install libfontconfig1 libfontconfig1-dev
cd ~
export PHANTOM_JS="phantomjs-2.1.1-linux-x86_64"
wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2
sudo tar xvjf $PHANTOM_JS.tar.bz2
sudo mv $PHANTOM_JS /usr/local/share
sudo ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin
sudo rm -rf $PHANTOM_JS.tar.bz2

設定好之後,就能透過命令列使用 PhantomJS 囉。下圖進入的是 PhantomJS 交談環境,如同 Python與 NodeJS,在這個模式輸入程式碼,能即時觀看結果。不過為了讓程式能重複使用,我們還是來編寫一隻 JS 檔。


程式碼

var fs = require('fs');
var system = require('system');

//load device spec
var messages = [];
var device_info = {
                    "deviceName" : "iphone6",
                    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1",
                    "windowSize":{
                        "width": 375,
                        "height": 667
                    }
                  }

// output messages and exit
function render_message(){
    if (messages.length>0)
        console.log(JSON.stringify(messages));
    phantom.exit();
}

// add message into list
function add_message(mtype, message){
    var package = {};
    package[mtype] = message;
    messages.push(package);
}

//input arg
if (system.args.length !== 2){
    add_message("error", "Usage: phantomjs get_screenshot.js [url]");
    render_message();
}else{
    var url = system.args[1];
}

//phantomjs onError
phantom.onError = function(message, trace) {
    add_message("error", "Phantomjs error: " + message);
    render_message();
};

//phantomjs main
function process(){

    //create page
    var page = require('webpage').create();

    //useragent setting
    page.settings.userAgent = device_info.userAgent;
    page.viewportSize = { 
                            width: device_info.windowSize.width, 
                            height: device_info.windowSize.height 
                        };
    page.clipRect = { 
                        top: 0, 
                        left: 0, 
                        width: device_info.windowSize.width, 
                        height: device_info.windowSize.height 
                    };

    //handler
    page.settings.resourceTimeout = 60000;
    page.onResourceTimeout = function(request) {
        add_message("error", "ResourceTimeout (60s)");
        render_message();
    };
    page.onAlert = function(msg) {
        add_message("error", "Alert detected");
        render_message();
    };
    page.onError = function (msg, trace) {
        add_message("warning", "Page error: " + msg);
    };
    //open url
    page.open(url, function(status) {
        //wait 5s for page loading
        window.setTimeout(function () {
            page.render("screenshot.png");
            add_message("success", true);
            render_message();
        }, 5000);
    });
}
process();

執行結果


使用說明

要抓取網頁截圖,必須考慮三個東西:使用者的瀏覽裝置螢幕解析度截圖範圍。對應到的 PhantomJS 中的設定值分別為 UserAgent、ViewportSize 與 ClipRect。

以這個例子來說,是讓 PhantomJS 模擬 iPhone 6 來開啟網頁。首先,為了讓網頁伺服器知道你用甚麼裝置瀏覽網頁,我們要設定 UserAgent (UA)。這個 UA是一個字串,記錄著你的瀏覽裝置 (電腦、手機或平板)、使用的瀏覽器 (Chrome、Firefox…),伺服器會透過 UserAgent 來判斷使用者的設備,進而決定要顯示手機版、電腦版網站,或是判斷是否阻擋爬蟲程式。UA 的列表你可以在網路上輕易找到,這裡我們使用 iPhone 6 的 UA 字串如下:

Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1

此外,UA 會用來識別連進網站的人 (Client)是不是機器人,像 Google 有很多爬蟲程式,無時無刻地在收集網頁資料,字串上就會看到 googlebot 囉

Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

接著我們要設定螢幕解析度,告訴瀏覽器渲染的畫面大小,以 iPhone 6 來說,他的解析度是:375*667。最後,我們決定截圖範圍,就和瀏覽器大小一樣為 375*667。如果沒有設定的話,會擷取整個頁面,圖片會變得很長。

我的爬蟲程式是以 Python 為主,PhantomJS 用來抓截圖以及抽 Feature,所以使用上是讓 Python 程式去開 PhantomJS。因此這支程式要帶有輸出,才知道截圖的結果為何。簡單分成三種狀況:

  • Success:成功截圖
  • Warning:PhantomJS 載入的頁面有 Error,但不影響截圖
  • Error:程式掛掉,或是連線逾時 (Timeout)

看一下程式碼就知道怎麼運作了。最後會輸出 JSON 格式的字串,讓 Python 程式讀取結果。而圖片有四種儲存格式可以選擇,包含 PNG, JPEG, GIF 與 PDF,這裡我存成 PNG 格式,頁面上有些空白的地方會變成透明。另外要注意的是,有些網站利用非同步載入頁面 (AJAX),使得截圖空白,需要設定 Timeout,延遲一下在截圖。

以下則是說明如何使用 subprocess 呼叫 PhantomJS:

import subprocess
cmd = "phantomjs snapshot.js URL"
try:
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
    res, err = p.communicate(timeout=120)
    result = json.loads(str(res,'utf-8'))
except Exception as e:
    pass

PhantomJS 裡頭有一個 Timoeout,是預防開網頁開太久卡住,而這個 Timeout 則是預防 PhantomJS 掛掉,讓程式能繼續執行。詳情可以參考 subprocess popen的用法


可能會遇到的問題

雖然在 Github 能找到許多人寫的 Bridge,把 PhantomJS 包成 NodeJS 或是 Python 套件直接使用,但因為 PhantomJS 有時候會莫名當掉,有些套件錯誤處理沒有做好,造成使用上有些麻煩。最好的方式就是自己寫一個,比較知道問題在哪

為了要加快爬蟲速度,我使用 Multi-thread,讓多個網頁能同時 Loading,可是 PhantomJS 同時開很容易就會陣亡。假如你像我一樣用 Python外部呼叫 PhantomJS,他掛掉後,會變成一個 Zombie Process (殭屍進程哈哈),因為沒有回傳訊息,Python 程式那邊等不到訊息,Thread 就會卡在那邊,接著就會發現程式越跑越慢…。後來在 popen 的 communicate 上加個 timeout 就解決問題,不過當掉的 PhantomJS 還是會存在,霸佔著資源,久了也會越來越慢…

這個問題一直沒有解決,直到 Google Chrome Headless 的出現,我的研究出現一絲曙光啦!使用之後,再與大家分享效果。


參考資料


平時會觀察網路行銷、雲端運算與資訊安全等議題,曾在趨勢科技擔任實習生、 GCP 專門家擔任技術文章寫手,也擔任過 C、JAVA、雲端運算等課程助教。