消失的文字
前面所提的, 大致就requires抓取網頁html, BeautifulSoup分析html裏的標籤, 然後抓取內容並存檔. 是爬的蠻高興的. 但天底下怎麼會有這麼幸福的事呢!!
先看一下台灣證券交易所的網站
https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.htm
裏面有本月份每天的開盤, 最高, 最低, 收盤指數, 這不就是一般人作夢也想爬下來的資料嗎. 結果一切到原始碼檢視HTML, 竟然找不到這些數字.
為什麼台灣券証交易所要讓這些數字消失呢? 廢話, 當然是不想讓你用爬蟲程式爬下來啊.
那為什麼HTML裏沒這些數字, 而網頁卻看得到這些數字? 因為這些數字是經由javascript運算後, 才將結果交由瀏覽顯示出來. 而我們切到原始碼, 只看得到那些未運轉的程式碼.
原來, 以前所提的, 都只能爬取純html的網頁. 但若遇到需經由javascript運算的網頁, 就爬不下來了. 經由javascript運算這動作, 又稱為javascript渲染(render). 手動分析js render後的結果, 固然是一種方法, 但這方法太沒人性.
破解
有攻就有守, 攻防之間一直在持續. 上述js render 這招, 已經徹底失守. 如何破解呢. 當然是直接從瀏覽器已顯示出來的資料去爬取.
幽靈瀏覽器 PhantomJs [ˋfæntəm] 就是用來模擬瀏覽器執行後的結果, 再將結果取出. Phantomjs其實就是一個瀏覽器, 它可解析 html, css, javascript 等網頁常用的語法, 是一個精簡的webkit, 但是它沒有任何的使用者介面, 又稱為Headless(無頭)瀏覽器.
下載安裝
請到 https://phantomjs.org/download.html 下載最新的幽靈瀏覽器. Windows版請下載phantomjs-x.x.x-windows.zip檔案. 解開後, 將bin/phantomjs.exe copy到要執行的目錄
建立腳本
腳本, 是指揮phantomjs要執行的動作. 編寫腳本內容的語言, 採用javascript. 所以請注意, 現在寫的javascript, 是在寫腳本, 不是在寫網頁.
簡易腳本
請使用純文字編輯器編輯下面的程式碼, 並存成 first.js, 然後於dos模式下執行 phantomjs.exe first.js
console.log()裏面若有中文的話, 輸出會變成亂碼. 此時就需加入–output-encoding, 如下
phantomjs.exe –output-encoding=big5 first.js
var page=require('webpage').create(); page.open('http://www.uuxs.tw/ls/22_22102/', function(status){ console.log('Status:' + status); if(status == 'success'){ page.render('test.png'); } phantom.exit(); });
首先, 依據 ‘webpage’ 這個模組創建一個網頁(page), 然後指定page要開啟的網址.
在腳本裏要將訊息顯示出來, 就需使用console.log(). 最後記得一定要執行phantom.exit(), 否則的話無法結束.
當網頁開啟完成後, 會執行第二個參數所指定的函數. 此為callback函數, 這在javascript裏, 又稱為沙盒模式(SandBox).
page就是執行完成後的網頁. 可以使用page.render將這個網頁存成一個圖檔.
下載網頁時間
require(“system”)可以取得執行時所傳入的參數. 以下腳本, 請用如下指令執行
phantomjs.exe speed.js http://tw.yahoo.com
var page=require('webpage').create(); system=require("system"); if(system.args.length==1){ console.log("usage : phantom.exe speed.js url"); phantom.exit(); } url=system.args[1]; //url='http://www.uuxs.tw/ls/22_22102/'; t=Date.now(); page.open(url, function(status){ console.log('Status:' + status); if(status == 'success'){ t=Date.now()-t; console.log('Load time : '+t+'msec'); page.render('web.png'); } phantom.exit(); });
evaluate
evaluate可以在網頁內執行javascript. 下面藍色的部份, 為想要在網頁內執行的函數. 將此函數傳入evaluate()即可
var page=require('webpage').create(); url='http://www.uuxs.tw/ls/22_22102/'; page.open(url, function(status){ console.log('Status:' + status); if(status == 'success'){ title=page.evaluate(function(){ return document.title; }); console.log("Title : "+title); } phantom.exit(); });
請注意, 在網頁內若執行console.log()是無法列印出資料的(紅色部份). 此時必需註冊onConsoleMessage監聽程式(藍色部份), 如果一監聽到內部網頁有資料印出, 此監聽程式就會被觸發, 繼而印出裏面的內容
var page=require('webpage').create(); url='http://www.uuxs.tw/ls/22_22102/'; page.open(url, function(status){ console.log('Status:' + status); page.onConsoleMessage=function(msg){ console.log(msg); }; if(status == 'success'){ page.evaluate(function(){ console.log("網頁內部執行javascript"); }); } phantom.exit(); });
按下按鈕
上圖為Javascript的網頁, 在<head>先載入jQuery, 然後於javascript裏即可控制語言的選取, 按下Div按鈕後, 黃色區域就會變更為選取的語言. 所以可以使用這個網頁來測試按鈕是不是有被成功按下. 上圖的完整程式碼如下
<html> <head> <script src="https://code.jquery.com/jquery-3.2.1.min.js" type="text/javascript"></script> <title>Mahal超級網站</title> </head> <body> <select name="country" id="country"> <option value="TW">台灣</option> <option value="KO">韓國</option> <option value="JP">日本</option> </select> <div id="showDiv" sytle="background-color:#ffff00;">test</div> <label>結果 : </label><input type="text" name="showText" id="showText"/> </br> <button onclick="toText()">文字方塊</button> <button onclick="toDiv()" class="btn">Div</button> <button onclick="toClear()">清除</button> <script> function toText(){ f=document.getElementById("showText"); f.value=document.getElementById("country").value; } function toDiv(){ f=document.getElementById("showDiv"); f.innerHTML=document.getElementById("country").value; } function toClear(){ f=document.getElementById("showDiv"); f.innerHTML="" } $('#country')[0].selectedIndex=1; //一定要加[0] $('#showDiv').html("ok"); </script> </body> </html>
要使用腳本操控上面的按鈕, 需先載入jQuery. jQuery是第三方 javascript函數庫, 它將一些常用的功能, 使用javascript實作出來並包裝成一個函數庫, 其原始碼公開且不用費用. 使用jQuery抓取網頁裏的元素, 只需 $(‘#id’) 或 $(Tag.className)即可
下面代碼中, page.includeJs()會載入jQuery函數庫, 載入完成就會callback後面的函數. 而這個callback函數就會執行網頁內的button click動作, 並更改divShow的內容. 然後印出其值
需注意的是, includejs()使用新的執行緒執行, 所以phantom.exit()必需寫在此新的執行緒內. 如果寫在外部的主執行緒, 將會提早結束.
var page=require('webpage').create(); url='test.htm' page.open(url, function(status){ page.onConsoleMessage=function(msg){ console.log(msg); }; if(status == 'success'){ page.includeJs("https://code.jquery.com/jquery-3.2.1.min.js", function() { page.evaluate(function() { $('#country')[0].selectedIndex=2; $("button.btn").click(); console.log($("#showDiv").html()); }); phantom.exit() }); console.log("finished"); } });