特征識別反爬蟲
我們可以將爬蟲的爬取過程分為網絡請求、文本獲取和數據提取3個部分。
信息校驗型反爬蟲主要出現在網絡請求階段,這個階段的反爬蟲理念以預防為主要目的,盡可能拒絕爬蟲程序的請求。
動態渲染和文本混淆則出現在文本獲取及數據提取階段,這個階段的反爬蟲理念以保護數據為主要目的,盡可能避免爬蟲獲得重要數據。
特征識別反爬蟲是指通過客戶端的特征、屬性或用戶行為特點來區分正常用戶和爬蟲程序的手段
本章我們要介紹的特征識別反爬蟲也是以預防為主要目的,直指爬蟲出現的源頭。
接下來,我們一起學習特征識別反爬蟲的原理和繞過技巧
WebDriver 識別
我們之前了解到,爬蟲程序可以借助渲染工具從動態網頁中獲取數據。
“借助”其實是通過對應的瀏覽器驅動 (即 WebDriver) 向瀏覽器發出指令的行為。
也就是說,開發者可以根據客戶端是否包含瀏覽器驅動這一特征來區分正常用戶和爬蟲程序
開發者如何檢測客戶端是否包含瀏覽器驅動呢?
哪些渲染工具有這些特征呢?本節我們將探討瀏覽器驅動的相關知識
- 打開網頁
- 定位按鈕並點擊
- 從頁面中提取文章內容
- 打印文章內容
from selenium.webdriver import Chrome import time browser = Chrome() browser.get('http://www.porters.vip/features/webdriver.html') # 定位按鈕並點擊 browser.find_element_by_css_selector('.btn.btn-primary.btn-lg').click() # 定位到文章內容元素 elements = browser.find_element_by_css_selector('#content') time.sleep(1) # 打印文章內容 print(elements.text) browser.close()
結果
請不要使用自動化測試工具訪問網頁
代碼運行后得到的結果與頁面顯示的結果不同,這次又遇到了什么樣的反爬蟲呢?
既然使用 Selenium 套件無法獲得目標數據,那我們就用 Puppeteer 試試,對應的代碼如下
import asyncio from pyppeteer import launch async def main(): browser = await launch() page = await browser.newPage() await page.goto('http://www.porters.vip/features/webdriver.html') # 定位按鈕元素並點擊 await page.click('.btn.btn-primary.btn-lg') # 等待 1 秒 await asyncio.sleep(1) # 網頁截圖保存 await page.screenshot({'path': 'webdriver.png'}) await browser.close() asyncio.get_event_loop().run_until_complete(main())
結果同樣顯示 請不要使用自動化測試工具訪問網頁
結果說明使用 Puppeteer 也無法獲得目標數據。
根據網頁給出的提示信息,我們知道網頁將這兩次請求所用的工具判定為“自動化測試工具”
要想獲得目標數據,就要找到網頁判定客戶端是否為“自動化測試工具”的依據,然后再考慮解決辦法
Web Driver 識別原理
仔細觀察網頁中的代碼,我們注意到 HTML 代碼中的按鈕設定了 onmousemove 事件
該事件綁定了名為 verify webdriver 的 JavaScript 方法
function verify_webdriver(){ var webr = navigator.webdriver; elements = document.getElementById('content'); if (webr){ elements.innerHTML = "請不要使用自動化測試工具訪問網頁"; }else{ elements.innerHTML = "\
原來這個方法使用了 Navigator 對象 (即 windows. navigator對象) 的 webdriver 屬性來判斷客戶端是否通過 WebDriver 驅動瀏覽器。
如果檢測到客戶端的 webariver 屬性,則在文章內容標簽處顯示 “請不要使用自動化測試工具訪問網頁”, 否則顯示正確的文章內容。
由於 Selenium 通過 WebDriver 驅動瀏覽器,客戶端的 webdriver 屬性存在, 所以無法獲得目標數據。在 Puppeteer 文檔中介紹到, Puppeteer 根據 Devtools 協議控制 Chrome 瀏覽器或Chromium 測覽器,雖然沒有提到是否使用 WebDriver, 但事實證明 Puppeteer 也存在 webdriver 屬性
Navigator 對象,它的屬性列表中就有 webdriver 的介紹
開發者正是利用 Navigator 對象完成的對客戶端是否使用 WebDriver 的判斷。
平時大家在網上查閱文章時見到的類似“ Selenium 檢測”或“ Chrome 檢測”等詞,指的就是 Webdriver 識別。
WebDriver 識別的繞過方法
要注意的是, navigator. webdriver 只適用於使用 Web Drive r的渲染工具,對於 Splash 這種使
用 WebKit 內核開發的渲染工具來說是無效的。我們可以用 Splash 獲取目標數據, Splash腳
本如下
function main(splash, args) assert (splash: go(args ur1)) assert( splash: wait(0.5)) --定位按鈕 local bton= splash: select('btn. btn-primary btn-lg') assert(splash: wait(1)) --鼠標懸停 bton:mouse_hover() --點擊按鈕 bton:mouse_click() assert(splash:wait(1)) return { -- 返回頁面截圖 png = splash:png(), }
模態框中顯示的是文章內容。這說明只要我們使用的渲染工具沒有 wearier屬性,就能獲得
目標數據
WebDriver 檢測的結果有3種,分別是true、 false 和 undefined。
當我們使用的渲染工具有webdriver屬性時,navigator. webdriver 的返回值就是true
反之則會返回fa1se或者 undefine
了解了 WebDriver 識別的原理和返回值后,我們就能想出應對的辦法了。既然 WebDriver 的識別依賴 navigator, webariver 的返回值,那么我們在觸發 verify_webdriver() 方法前將navigator.wearier 的值改為 false 或者 undefined 即可。
Selenium 套件和 Puppeteer 都提供了運行 JavaScript 代碼的方法,接下來我們就嘗試使用 JavaScript 修改 navigator. webdriver 的值
Selenium 套件對應的 Python 代碼為
from selenium.webdriver import Chrome import time browser = Chrome() browser.get('http://www.porters.vip/features/webdriver.html') # 編寫修改 navigator.webdriver 的值為 JavaScript 代碼 script = 'Object.defineProperty(navigator, "webdriver", {get: () => false,});' # 運行 JavaScript 代碼 browser.execute_script(script) time.sleep(1) # 定位按鈕並點擊 browser.find_element_by_css_selector('.btn.btn-primary.btn-lg').click() # 定位到文章內容元素 elements = browser.find_element_by_css_selector('#content') time.sleep(1) # 打印文章內容 print(elements.text) browser.close()
這個時候頁面結果就被打印出來了
這說明使用 Javascript 修改 navigator, webdriver 屬性值的方法是可行的
要注意的是,這種修改該屬性值的方法只在當前頁面有效,當瀏覽器打開新標簽或新窗口時需要
重新執行改變 navigator. webdriver 值的 Javascript 代碼除此之外,還有一種方法可以繞過 navigator. webdriver 的檢測 mitmproxy (詳見htps:∥
mitmproxy.org/) 是一個開源的交互式 HTTPS 代理,客戶端可以使用它提供的 API 來過濾 JavaScript 文件中檢測 navigator. webdriver屬性值的代碼mitmproxy 在此過程中作為瀏覽器和服務器的中間人,每一次請求和響應都會經過 mitmproxy
正是由於 mitmproxy 中間人的角色,所以設置好過濾規則后,無論是重新打開標簽還是新窗口
都不會重置 navigator.webdriver 的屬性值
瀏覽器特征
判斷客戶端身份的特征條件不僅有 WebDirver,還包括客戶端的操作系統信息和硬件信息等
開發者將這些特征值作為區分正常用戶和爬蟲程序的條件
除了 Navigator 對象的 serAgent、 cookieEnable、 platform、 plugins 等屬性外, Screen對象(即 window. screen對象)的一些屬性也可以作為判斷依據。
比如將瀏覽器請求頭中的 User- Agent 值與 navigator. userAgent 屬性值進行對比
結合 navigator. platform 就可以判斷客戶端是否使用隨機切換的 User-Agent
我們可以通過一個實際的例子來驗證這種想法
<script> console.log("userAgent:" + navigator.userAgent); console.log("platform:" + navigator.platform); </script>
然后用瀏覽器打開該 HTML 文件,接着喚起開發者工具並切換到 Console面板。此時可以看到
userAgent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 platform:Win32
在瀏覽器請求頭中的 User- Agent 值與 navigator. userAgent 屬性值是相同的,如果值不同則將以客戶端視為爬蟲程序。
User-gent中的操作系統顯示為 Win32 ,如果 navigator.platfom屬性值與此不符,那么也可以將該客戶端視為爬蟲程序。
WebDriver 示例反爬蟲
我們使用 Puppeteer 截圖,Puppeteer 允許設置瀏覽器窗口的寬和高
import asyncio from pyppeteer import launch async def main(): browser = await launch() page = await browser.newPage() await page.goto('http://www.porters.vip/features/browser.html') await page.setViewport({'width': 1000, 'height': 1000}) await page.screenshot({'path': 'browser.png'}) await browser.close() asyncio.get_event_loop().run_until_complete(main())
運行后得到圖片
我們來對比一下 3 種渲染工具訪問示例網址頁面后得到的特征屬性值,不同之處
首先是 User-Agent 屬性, Splash 和 Puppeteer 都有明顯的標識,所以 User-Agent 屬性值可以作為客戶端特征。
接着看屏幕分辨率,3 種工具的分辨率都不同,所以屏幕分辨率也可以作為客戶端特征。
核心數量方面,由於阿里雲 ECS 是 1 核,所以 Splash 顯示為 1 核。一般個人計算機的核心數量為 2 個以上,除非客戶端的計算機運行在虛擬機中或是年代久遠的。因此,CPU 核心數量同樣可以作為客戶端特征
不同渲染工具的瀏覽器插件數量也是不相同的,雖然插件數量與渲染工具關聯並不大(這個
要受插件安裝影響),但這個屬性值可以作為客戶端特征。事實上,只要有可能出現不同結果的屬性,就可以作為客戶端特征,所以時區屬性值也包含在內
屬性值可以作為特征並不代表服務器端通過單個屬性值就能確認客戶端身份,它們只是服務器端判斷客戶端身份的依據之一
要注意的是,這些屬性的值可以通過 JavaScript進行更改,所以這種特征識別方式得到的結果是不可靠的
訪問頻率限制統過實戰
訪問頻率指的是單位時間內客戶端向服務器端發出網絡請求的次數,它是描述網絡請求頻繁程度的量
正常用戶瀏覽網頁的頻率不會像爬蟲程序那么高,開發者可以將訪問頻率過高的客戶端視為爬蟲程序
任務:連續10次訪問目標網頁,要求響應狀態碼為200。
這個任務看起來挺簡單的,我們可以直接用 Requests庫發起請求
import requests for i in range(10): res = requests.get('http://www.porters.vip/features/rate.html') print(res.status_code)
結果
200 200 200 200 200 200 503 503 503 503
如果加上 time.sleep(1) 就能保證每次請求都是 200
實際上,爬蟲總是希望請求評率越搞越好,這樣才能夠在最短的時間內完成爬取任務
剛才使用的 time.sleep(1) 這種降低請求頻率的方法並不是爬蟲工程師最好的選擇
面對根據 IP 地址實現的訪問頻率限制反爬蟲,我們可以使用多台機器共同爬取
假如數據總量為 5 萬條,目標網站限速為 1r/s ,使用 time.sleep(1) 這種方式完成爬取任務需要耗費的時間約為13.9小時。
此時將爬取機器從 1 台增加到10台 (10個IP) 那么爬取時間就會降低到 1.39 小時。
這種使用多台機器共同爬取的方法稱為多機爬取,如果這些機器分布在不同的地域,並且它們使用的是相同的 URL 隊列組合就是分布式爬蟲。
分布式爬蟲分為 對等分布式 和 主從分布式
使用分布式爬蟲后,就可以在單位時間內發起更多的請求。
這種方式能夠有效地應對訪問頻率限制,但經濟成本很高
除了增加機器外,還可以使用 IP 切換的方式提高訪問頻率。
假如用一台機器作為代理,輪流使用本機 IP 和代理 IP 發起請求,就能夠將請訪問頻率提高 1 倍, 9 個代理能夠將訪問頻率提升9倍
想要在 1 台機器上提高訪問頻率,可以使用多個 IP 代理。
IP 代理其實是維護一個 IP 池,爬蟲程序每次發出請求時都從 IP 池中取出 1 個 IP 作為代理
要注意的是 IP 池中的 IP 地址需要由真實的機器(通常是服務器)提供代理服務,我們將這些
提供代理服務的機器的P地址收集起來,匯聚成一個“池”,所以叫作 IP 池。可以自己搭建用於 IP 代理的服務器,也可以直接從提供代理服務的商家購買 IP
訪問頻率限制的原理
開發者認為訪問頻率過高的是爬蟲程序。
要限制爬蟲程序的請求頻率,首先就是要找到並確定客戶端的身份標識,然后根據標識記錄該客戶端的請求次數,並且拒絕單位時間內請求次數過多的客戶端請求。
提到客戶端身份標識,我們想到的第一個答案就是 IP 地址。
可以用 Nginx 實現根據 IP 地址限制爬蟲訪問頻率的功能
瀏覽器指紋知識擴展
除了 IP 地址之外,用於確定客戶端身份的標識還有登錄后的用戶憑證(如 Cookie 或 Token )和
覽器指紋Cookie 和 Token 通常由后端程序生成,所以對該標識的限制任務也由后端程序完成。
后端程序會維護用戶身份標識和單位時間內的請求次數隊列。
每次客戶端發起請求時,后端程序會將請求攜帶的 Cookie 或 Token 信息與隊列中的用戶身份標識進行對比。
如果隊列中沒有該用戶標識記錄或單位時間內請求次數未達到閾值,則響應該請求,並且將隊列中對應的請求次數進行累加,反之則拒絕該請求后端實現訪問頻率限制的邏輯
一些成熟的 web 框架就附帶訪問頻率限制功能,比如 django rest framework,它提供了用於限速的模塊 Throttling。
該模塊允許對已登錄和未登錄的用戶進行訪問頻率限制
訪間頻率限制的單位時間可以是每秒、每分鍾、每小時、每天等。對於使用 Cookie 或Token作為依據的訪問頻率限制方法,我們只需要申請足夠多的賬號,獲取每個賬號登錄后得到的 Cookie 植或 Token 值,就可以像搭建 IP 池一樣搭建一個用戶身份憑證池。
每次請求時從憑證池中取出一個 Cookie 值或 Token 值,並在代碼中使用該值偽造用戶身份。
瀏覽器指紋也稱為客戶端指紋,是指由多種客戶端特征信息組成的字符串結果。
組成瀏覽器指紋的特征信息包括硬件信息(如屏幕的分辨率和色值、CPU的核心數與類型等)、瀏覽器信息如之前提到的 platform、插件列表和 User-Agent屬性值等)和不可重復信息(如 IP 地址、已登錄用戶的Cookie等)。
其中不可重復信息實際上是可以人為改變的。
這些信息組合成的字符串結果的重復概率比較低,但如果是某個網咖或者學校統一采購的計算機,不同設備就很有可能得到相同的指紋信息,因為它們的硬件配置相同,而且在同一個網段,所以重復的概率就會增加
考慮到這個問題,有人提出利用 UUID、 Canvas 和 Webgl 技術獲得“唯一”指紋。
UUID 是通用唯一識別碼( Universally Unique Identifier)的縮寫,是一種軟件建構標准,亦為開
放軟件基金會在分布式計算環境領域的一部分。UUID 的規范為RFC4122,該規范給出了 UUID 的組成部分和生成算法建議。
UUID由以下幾部分組成。
- 60位的當前時間戳。
- 時鍾序列
- 全局唯一的 IEEE 機器識別號,如果有網卡,從網卡 MAC 地址獲得,沒有網卡以其他方式獲得
最終生成的 UUD
開發者可以將 UUID 寫人 Cookie,由服務器端驗證請求中的 Cookie 值即可。
但如果客戶端關閉 Cookie,那么指紋就失效了。
Canvas 是 HTML5 新增的組件,開發者可以使用 JavaScript 在網頁上繪制圖案和動效。
由 Canvas 繪制的圖片可以進行 Base64 編碼,得到很長的字符串,業內將這樣的字符串稱為 Canvas 指紋。
Canvas 不依賴 Cookie,所以即使客戶端關閉 Cookie 也不會影響服務器端獲取 Canvas 生成的指紋。
<canvas id="test-canvas" width="500 height="200">
不同的瀏覽器一般使用不同的圖像處理引擎、圖像導出選項、圖像壓縮級別,即使是使用相同的繪制代碼,得出的結果也會有所差別。
從操作系統角度來看,不同系統擁有的字體有可能是不同的,字體的渲染差異也會影響 Canvas 繪圖結果。
由於 Canvas 的這些特性,開發者認為由 Canvas 繪制成的圖片值也是不重復的。
要使用 Canvas 生成指紋,我們需要完成繪圖、圖片數據讀取和數據壓縮等任務
Canvas 瀏覽器指紋展示頁主要用於顯示 Canvas 繪圖的圖片數據和該數據的 MD5 加密值
單一的 Canvas 指紋、 WebGL 指紋和 Navigato r對象屬性都不能作為客戶端的身份標識,但將這些指紋與屬性值組合在一起,就能夠降低指紋重復的概率。
Fingerprint.js(詳見hps:/ fingerprints. com/zh)
是一個開源的指紋檢測庫,該庫通過 JavaScript 從瀏覽器中收集信息,然后提取可用數據,並將數據加密成一個獨特的識別碼。Fingerprint.js 使用最先進的識別方法,包括畫布指紋追蹤、音頻采樣、 WebGL
指紋識別、字體檢查和瀏覽器插件探測等
隱藏鏈接反爬蟲
隱藏鏈接反爬蟲指的是在網頁中隱藏用於檢測爬蟲程序的鏈接的手段。
被隱藏的鏈接不會顯示在頁面中,正常用戶無法訪問,但爬蟲程序有可能將該鏈接放入待爬隊列,並向該鏈接發起請求。
開發者可以利用這個特點區分正常用戶和爬蟲程序
比如爬蟲程序找到的鏈接有 /detail/ 和 /details/,那么開發者將會 /details 穿插在鏈接中
並設置樣式為 display: none
該 CSS 樣式的作用是隱藏標簽,所以我們在頁面只看到 6 件商品,但爬蟲程序卻提取到 8 件商品的URL。
根據兩個這個現象,我們可以大膽猜測這種網站的反爬蟲邏輯:
只要客戶端訪問 URL 為 /details/ 的接口,就將該客戶端視為爬蟲,並且拒絕來自該 IP 的請求
本章總結
無論是爬蟲程序還是我們使用的工具,都有可能存在一些特性,開發者可以根據這些特性來區分
正常用戶和爬蟲程序。要注意的是,這些特性並非是不可改變的。
爬蟲工程師可以根據一些現象猜測目標網站使用的反爬蟲手段,然后做出應對