本文首發於個人我的博客,同步發佈於掘金專欄,非商業轉載請註明出處,商業轉載請閱讀原文連接裏的法律聲明。javascript
web是一個開放的平臺,這也奠基了web從90年代初誕生直至今日將近30年來蓬勃的發展。然而,正所謂成也蕭何敗也蕭何,開放的特性、搜索引擎以及簡單易學的html、css技術使得web成爲了互聯網領域裏最爲流行和成熟的信息傳播媒介;但現在做爲商業化軟件,web這個平臺上的內容信息的版權卻毫無保證,由於相比軟件客戶端而言,你的網頁中的內容能夠被很低成本、很低的技術門檻實現出的一些抓取程序獲取到,這也就是這一系列文章將要探討的話題—— 網絡爬蟲 。css
有不少人認爲web應當始終遵循開放的精神,呈如今頁面中的信息應當毫無保留地分享給整個互聯網。然而我認爲,在IT行業發展至今天,web已經再也不是當年那個和pdf一爭高下的所謂 「超文本」信息載體 了,它已是以一種 輕量級客戶端軟件 的意識形態的存在了。而商業軟件發展到今天,web也不得不面對知識產權保護的問題,試想若是原創的高質量內容得不到保護,抄襲和盜版橫行網絡世界,這其實對web生態的良性發展是不利的,也很難鼓勵更多的優質原創內容的生產。html
未受權的爬蟲抓取程序是危害web原創內容生態的一大元兇,所以要保護網站的內容,首先就要考慮如何反爬蟲。java
最簡單的爬蟲,是幾乎全部服務端、客戶端編程語言都支持的http請求,只要向目標頁面的url發起一個http get請求,便可得到到瀏覽器加載這個頁面時的完整html文檔,這被咱們稱之爲「同步頁」。python
做爲防守的一方,服務端能夠根據http請求頭中的User-Agent
來檢查客戶端是不是一個合法的瀏覽器程序,亦或是一個腳本編寫的抓取程序,從而決定是否將真實的頁面信息內容下發給你。git
這固然是最小兒科的防護手段,爬蟲做爲進攻的一方,徹底能夠僞造User-Agent
字段,甚至,只要你願意,http的get方法裏, request header的 Referrer
、 Cookie
等等全部字段爬蟲均可以垂手可得的僞造。github
此時服務端能夠利用瀏覽器http頭指紋,根據你聲明的本身的瀏覽器廠商和版本(來自 User-Agent
),來鑑別你的http header中的各個字段是否符合該瀏覽器的特徵,如不符合則做爲爬蟲程序對待。這個技術有一個典型的應用,就是 PhantomJS
1.x版本中,因爲其底層調用了Qt框架的網絡庫,所以http頭裏有明顯的Qt框架網絡請求的特徵,能夠被服務端直接識別並攔截。web
除此以外,還有一種更加變態的服務端爬蟲檢測機制,就是對全部訪問頁面的http請求,在 http response
中種下一個 cookie token
,而後在這個頁面內異步執行的一些ajax接口裏去校驗來訪請求是否含有cookie token,將token回傳回來則代表這是一個合法的瀏覽器來訪,不然說明剛剛被下發了那個token的用戶訪問了頁面html卻沒有訪問html內執行js後調用的ajax請求,頗有多是一個爬蟲程序。ajax
若是你不攜帶token直接訪問一個接口,這也就意味着你沒請求過html頁面直接向本應由頁面內ajax訪問的接口發起了網絡請求,這也顯然證實了你是一個可疑的爬蟲。知名電商網站Amazon就是採用的這種防護策略。redis
以上則是基於服務端校驗爬蟲程序,能夠玩出的一些套路手段。
現代瀏覽器賦予了JavaScript強大的能力,所以咱們能夠把頁面的全部核心內容都作成js異步請求 ajax
獲取數據後渲染在頁面中的,這顯然提升了爬蟲抓取內容的門檻。依靠這種方式,咱們把對抓取與反抓取的對抗戰場從服務端轉移到了客戶端瀏覽器中的js運行時,接下來講一說結合客戶端js運行時的爬蟲抓取技術。
剛剛談到的各類服務端校驗,對於普通的python、java語言編寫的http抓取程序而言,具備必定的技術門檻,畢竟一個web應用對於未受權抓取者而言是黑盒的,不少東西須要一點一點去嘗試,而花費大量人力物力開發好的一套抓取程序,web站做爲防守一方只要輕易調整一些策略,攻擊者就須要再次花費同等的時間去修改爬蟲抓取邏輯。
此時就須要使用headless browser了,這是什麼技術呢?其實說白了就是,讓程序能夠操做瀏覽器去訪問網頁,這樣編寫爬蟲的人能夠經過調用瀏覽器暴露出來給程序調用的api去實現複雜的抓取業務邏輯。
其實近年來這已經不算是什麼新鮮的技術了,從前有基於webkit內核的PhantomJS,基於Firefox瀏覽器內核的SlimerJS,甚至基於IE內核的trifleJS,有興趣能夠看看這裏和這裏 是兩個headless browser的收集列表。
這些headless browser程序實現的原理實際上是把開源的一些瀏覽器內核C++代碼加以改造和封裝,實現一個簡易的無GUI界面渲染的browser程序。但這些項目廣泛存在的問題是,因爲他們的代碼基於fork官方webkit等內核的某一個版本的主幹代碼,所以沒法跟進一些最新的css屬性和js語法,而且存在一些兼容性的問題,不如真正的release版GUI瀏覽器運行得穩定。
這其中最爲成熟、使用率最高的應該當屬 PhantonJS 了,對這種爬蟲的識別我以前曾寫過一篇博客,這裏再也不贅述。PhantomJS存在諸多問題,由於是單進程模型,沒有必要的沙箱保護,瀏覽器內核的安全性較差。另外,該項目做者已經聲明中止維護此項目了。
現在Google Chrome團隊在Chrome 59 release版本中開放了headless mode api,並開源了一個基於Node.js調用的headless chromium dirver庫,我也爲這個庫貢獻了一個centos環境的部署依賴安裝列表。
Headless Chrome可謂是Headless Browser中獨樹一幟的大殺器,因爲其自身就是一個chrome瀏覽器,所以支持各類新的css渲染特性和js運行時語法。
基於這樣的手段,爬蟲做爲進攻的一方能夠繞過幾乎全部服務端校驗邏輯,可是這些爬蟲在客戶端的js運行時中依然存在着一些破綻,諸如:
if(navigator.plugins.length === 0) {
console.log('It may be Chrome headless');
}
複製代碼
if(navigator.languages === '') {
console.log('Chrome headless detected');
}
複製代碼
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl');
var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
var vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
if(vendor == 'Brian Paul' && renderer == 'Mesa OffScreen') {
console.log('Chrome headless detected');
}
複製代碼
if(!Modernizr['hairline']) {
console.log('It may be Chrome headless');
}
複製代碼
var body = document.getElementsByTagName('body')[0];
var image = document.createElement('img');
image.src = 'http://iloveponeydotcom32188.jg';
image.setAttribute('id', 'fakeimage');
body.appendChild(image);
image.onerror = function(){
if(image.width == 0 && image.height == 0) {
console.log('Chrome headless detected');
}
}
複製代碼
基於以上的一些瀏覽器特性的判斷,基本能夠通殺市面上大多數 Headless Browser
程序。在這一層面上,其實是將網頁抓取的門檻提升,要求編寫爬蟲程序的開發者不得不修改瀏覽器內核的C++代碼,從新編譯一個瀏覽器,而且,以上幾點特徵是對瀏覽器內核的改動其實並不小,若是你曾嘗試過編譯Blink內核或Gecko內核你會明白這對於一個「腳本小子」來講有多難~
更進一步,咱們還能夠基於瀏覽器的 UserAgent 字段描述的瀏覽器品牌、版本型號信息,對js運行時、DOM和BOM的各個原生對象的屬性及方法進行檢驗,觀察其特徵是否符合該版本的瀏覽器所應具有的特徵。
這種方式被稱爲 瀏覽器指紋檢查 技術,依託於大型web站對各型號瀏覽器api信息的收集。而做爲編寫爬蟲程序的進攻一方,則能夠在 Headless Browser 運行時裏預注入一些js邏輯,僞造瀏覽器的特徵。
另外,在研究瀏覽器端利用js api進行 Robots Browser Detect 時,咱們發現了一個有趣的小技巧,你能夠把一個預注入的js函數,假裝成一個Native Function,來看看下面代碼:
var fakeAlert = (function(){}).bind(null);
console.log(window.alert.toString()); // function alert() { [native code] }
console.log(fakeAlert.toString()); // function () { [native code] }
複製代碼
爬蟲進攻方可能會預注入一些js方法,把原生的一些api外面包裝一層proxy function做爲hook,而後再用這個假的js api去覆蓋原生api。若是防護者在對此作檢查判斷時是基於把函數toString
以後對[native code]
的檢查,那麼就會被繞過。因此須要更嚴格的檢查,由於bind(null)
僞造的方法,在toString
以後是不帶函數名的,所以你須要在toString
以後檢查函數名是否爲空。
這個技巧有什麼用呢?這裏延伸一下,反抓取的防護者有一種Robot Detect
的辦法是在js運行時主動拋出一個alert
,文案能夠寫一些與業務邏輯相關的,正常的用戶點肯定按鈕時一定會有一個1s甚至更長的延時,因爲瀏覽器裏alert
會阻塞js代碼運行(實際上在v8裏他會把這個isolate
上下文以相似進程掛起的方式暫停執行),因此爬蟲程序做爲攻擊者能夠選擇以上面的技巧在頁面全部js運行之前預注入一段js代碼,把alert
、prompt
、confirm
等彈窗方法所有hook僞造。若是防護者在彈窗代碼以前先檢驗下本身調用的alert
方法仍是不是原生的,這條路就被封死了。
目前的反抓取、機器人檢查手段,最可靠的仍是驗證碼技術。但驗證碼並不意味着必定要強迫用戶輸入一連串字母數字,也有不少基於用戶鼠標、觸屏(移動端)等行爲的行爲驗證技術,這其中最爲成熟的當屬Google reCAPTCHA,基於機器學習的方式對用戶與爬蟲進行區分。
基於以上諸多對用戶與爬蟲的識別區分技術,網站的防護方最終要作的是封禁ip地址
或是對這個ip的來訪用戶施以高強度的驗證碼策略。這樣一來,進攻方不得不購買ip代理池來抓取網站信息內容,不然單個ip地址很容易被封致使沒法抓取。抓取與反抓取的門檻被提升到了ip代理池
經濟費用的層面。
除此以外,在爬蟲抓取技術領域還有一個「白道」的手段,叫作robots
協議。你能夠在一個網站的根目錄下訪問/robots.txt
,好比讓咱們一塊兒來看看github的機器人協議,Allow
和Disallow
聲明瞭對各個UA爬蟲的抓取受權。
不過,這只是一個君子協議,雖具備法律效益,但只可以限制那些商業搜索引擎的蜘蛛程序,你沒法對那些「野爬愛好者」加以限制。
對網頁內容的抓取與反制,註定是一個魔高一尺道高一丈的貓鼠遊戲,你永遠不可能以某一種技術完全封死爬蟲程序的路,你能作的只是提升攻擊者的抓取成本,並對於未受權的抓取行爲作到較爲精確的獲悉。
這篇文章中提到的對於驗證碼的攻防其實也是一個較爲複雜的技術難點,在此留一個懸念,感興趣能夠加關注期待後續文章進行詳細闡述。
另外,歡迎對抓取方面感興趣的朋友關注個人一個開源項目webster, 項目以Node.js 結合Chrome headless模式實現了一個高可用性網絡爬蟲抓取框架,藉以chrome對頁面的渲染能力, 能夠抓取一個頁面中 全部的js及ajax渲染的異步內容;並結合redis實現了一個任務隊列,使得爬蟲程序能夠方便的進行橫向、縱向的分佈式擴展。部署起來很方便,我已經爲webster
提供了一個官方版的基礎運行時docker鏡像,若是你想先睹爲快也能夠試試這個webster demo docker鏡像。