Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

在抓取對方網站、APP 應用的相關數據時,經常會遇到一系列的方法阻止爬蟲。一方面是為了保證服務的質量,另一方面是保護數據不被獲取。常見的一些反爬蟲 和反反爬蟲的手段如下。

(1)IP 限制

IP 限制是很常見的一種反爬蟲的方式。服務端在一定時間內統計 IP 地址的訪問 次數,當次數、頻率達到一定閾值時返回錯誤碼或者拒絕服務。這種方式比較直接 簡單,但在 IPv4 資源越來越不足的情況下,很多用戶共享一個 IP 出口,典型的如“長 城寬帶”等共享型的 ISP。另外手機網絡中的 IP 地址也是會經常變化的,如果對這 些 IP 地址進行阻斷,則會將大量的正常用戶阻止在外。

對於大多數不需要登錄就可以進行訪問的網站,通常也只能使用 IP 地址進行限 制。比如“Freelancer 網站”,大量的公開數據可以被訪問,但同一個 IP 地址的訪問 是有一定的限制的。針對 IP 地址限制非常有效的方式是,使用大量的“高匿名”代 理資源。這些代理資源可以對源 IP 地址進行隱藏,從而讓對方服務器看起來是多個 IP 地址進行訪問。另一種限制方式是,根據業務需要,對國內、國外的 IP 地址進行 單獨處理,進而對國外的高匿名代理進行阻斷,例如使用海外的 IP 地址訪問“天眼 查網站”則無法訪問。

(2)驗證碼

驗證碼是一種非常常見的反爬蟲方式。服務提供方在 IP 地址訪問次數達到一定 數量後,可以返回驗證碼讓用戶進行驗證。這種限制在不需要登錄的網頁界面比較 常見,它需要結合用戶的 cookie 或者生成一個特殊標識對用戶進行唯一性判斷,以 防止同一個 IP 地址訪問頻率過高。驗證碼的存在形式非常多,有簡單的數字驗證碼、 字母數字驗證碼、字符圖形驗證碼,網站也可以用極驗驗證碼等基於用戶行為的驗 證碼。針對簡單驗證碼,可以使用打碼平臺進行破解。這種平臺通過腳本上傳驗證 的圖片,由打碼公司僱用的人工進行識別。針對極驗驗證等更復雜的驗證碼,可以嘗試模擬用戶的行為繞過去,但通常比較煩瑣,難度較大。谷歌所用的驗證碼更為 複雜,通常是用戶端結合雲端進行手工打碼,但會帶來整體成本較高的問題。要想繞過這些驗證碼的限制,一種思路是在出現驗證碼之前放棄訪問,更換 IP 地址。ADSL 撥號代理提供了這種可能性。ADSL 通過撥號的方式上網,需要輸入 ADSL 賬號和密碼,每次撥號就更換一個 IP 地址。不同地域的 IP 地址分佈在多個地 址段,如果 IP 地址都能使用,則意味著 IP 地址量級可達千萬。如果我們將 ADSL 主機作為代理,每隔一段時間主機撥號一次(換一個 IP),這樣可以有效防止 IP 地 址被封禁。這種情況下,IP 地址的有效時限通常很短,通常在 1 分鐘以下。結合大 量的 ADSL 撥號代理可以達到並行獲取大量數據的可能。如果網站使用了一些特殊 的唯一性的標識,則很容易被對方網站識別到,從而改進反爬蟲策略,面對這種情 況,單獨切換 IP 地址也會無效。遇到這種情況,必須要搞清楚標識的生成方式,進 而模擬真實用戶的訪問。

(3)登錄限制

登錄限制是一種更加有效的保護數據的方式。網站或者 APP 可以展示一些基礎的數據,當需要訪問比較重要或者更多的數據時則要求用戶必須登錄。例如,在天 眼查網站中,如果想要查看更多的信息,則必須用賬號登錄;“知乎”則是必須在登 錄後才能看到更多的信息。登錄後,結合用戶的唯一標識,可以進行計數,當訪問 頻度、數量達到一定閾值後即可判斷為爬蟲行為,從而進行攔截。針對“登錄限制” 的方法,可以使用大量的賬號進行登錄,但成本通常比較高。

針對微信小程序,可以使用 wx.login()方法,這種方式不需要用戶的介入,因而 不傷害用戶的體驗。小程序調用後會獲取用戶的唯一標識,後端可以根據這個唯一 標識進行反爬蟲的判斷。

(4)數據偽裝

在網頁上,我們可以監聽流量,然後模擬用戶的正常請求。mitmproxy 等工具可 以監聽特定網址的訪問(通常是 API 的地址),然後將需要的數據存儲下來。基於 Chrome Headless 的工具也可以監聽到流量並進行解析。在這種情況下,某些網站會 對數據進行一些偽裝來增加複雜度。例如,在某網站上展示的價格為 945 元,在 DOM 樹中是以 CSS 進行了一些偽裝。要想得到正確的數值,必須對 CSS 的規則進行一些 計算才行,某網站上展示的價格如圖 1-1 所示。

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-1 某網站上展示的價格

該網站使用特殊的字體對數據進行了偽裝。例如,3400,對應顯示的是 1400, 如圖 1-2 所示。如果能夠找到所有的字體對應的關係,則可以逆向出正確的價格。

某電影網站使用特殊的字符進行數據隱藏,這種不可見的字符會增加複雜度, 但還是可以通過對應的 UTF-8 字符集找到對應關係,從而得到正確的值,如圖 1-3 所示。

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-2 3400 顯示為 1400

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-3 網站用特殊字符進行偽裝

對於這種偽裝,可以人工分析目標網站的前端代碼,對 CSS、JavaScript 和字符進行分析,推導出計算公式。在這種情況下,使用爬蟲必須要非常小心,因為很可 能目標網站進行改版後,規則已經發生了變化,抓取到的數據便會無效。在爬蟲程序的維護上可以增加一些數據有效性的檢查,通過自動化或者人工的方式進行檢查。例如,針對機票數據可以檢查價格是否在一個合理的區間範圍內,如果超出,則認為規則已經變化。更為複雜的方案是可以藉助 OCR 技術,對所需要的區域進行識別, 然後對比抓取到的結果。

(5)參數簽名

設計良好的 API 通常都要對參數使用簽名(sign)來驅避非法請求,常見於手機 APP。APP 通過加密算法對請求的參數進行運算,從而得到一個簽名。這個簽名通常 和時間戳相關,並且在請求中附加上時間戳。在請求的參數固定的情況下,能夠在一小段時間內生效。當請求發送到服務端後,服務端對參數、時間戳進行驗證,比較簽名是否一致。如果不一致,則判斷為非法請求。這樣做的好處是,可以保護請 求,即便是被抓包,在很短時間內這個請求就會失效。獲取 APP 端的加密算法一般較為困難,通常需要進行反編譯才能獲得加密算法。然而現階段絕大多數 APP 已經 被加殼(典型的如 360 加固、愛加密等),要進行反編譯都很困難。另一種保護措施 是,將加密算法放到原生代碼中進行編譯,通常這些代碼是 C 或 C++代碼。由於原生代碼相對於 Java 代碼更難進行逆向工程,所以這給反編譯又帶來了更多的麻煩。

針對這種參數簽名的方法,沒有太好的途徑能夠來解決,在逆向反編譯無果的 情況下,可以試著找尋有沒有其他的入口,例如,HTML5、微信小程序等。如果它 們請求了相同的 API,則很有可能在源代碼中包含了加密算法。幸運的是,基於 JavaScript 開發的應用非常容易逆向分析,能夠很快地獲取加密算法,從而繞過 APP 的保護機制。如果這些方法都不奏效,則可以考慮模擬用戶操作應用,通過抓包的方式採集到流量中的信息。但這種方式效率較低,如果要發出多個併發的請求,往往需要多個設備同時進行。

(6)隱藏驗證

更復雜的反爬蟲的方式之一是,隱藏驗證。例如,在網站的防護上,通過 JavaScript 請求一些特殊的網址,可以得到一些特定的令牌(token),這樣每次請求時即可生成

不同的令牌。甚至有些網站會在不可見的圖片加上一些特殊的請求參數,從而識別 是否是真正的瀏覽器用戶。這種情況下,想直接獲取 API 進行請求通常行不通或者 非常困難,只能通過 Chrome Headless 等工具模擬用戶的行為,從而規避這種情況。

(7)阻止調試

在分析某旅遊網站時發現,一旦打開瀏覽器的控制檯界面,就會無限觸發瀏覽器的 debugger 指令。深入研究代碼發現,該網站在一個名為 leonid-tq-jq-v3-min.js 中 給所有的構造函數都加上了 debugger 這個關鍵字,導致任何對象的生成都會觸發調試器。這樣做的目的是阻止意外的腳本或程序進行跟蹤調試,從而保護代碼。這種情況下,可以構建一個修改過的 js 文件,去掉 debugger 關鍵字,使用 mitmproxy 轉發流量並攔截 leonid-tq-jq-v3-min.js,將改後的 js 文件返回給瀏覽器,從而繞過這個限制,某旅遊網調試界面如圖 1-4 所示。

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-4 某旅遊網調試界面



代理服務器

代理服務器是爬蟲工具的基本武器,既可以隱藏真實的訪問來源,又可以繞過 大部分網站都會有的 IP 地址的訪問頻度的限制。常見的代理有 HTTP 代理和 HTTPS 代理兩種,根據匿名程度的不同,可以將代理級別分為以下 5 種。

(1)高匿名代理

高匿名代理會將數據包原封不動地轉發,從服務端來看,就像是真的一個普通客戶端在訪問,而記錄的 IP 地址是代理服務器的 IP 地址,可以對很好地隱藏訪問源, 所以這種代理為爬蟲工具首選。

(2)普通匿名代理

普通匿名代理會在數據包上做一些改動,代理服務器通常會加入的 HTTP 頭有 HTTP_VIA 和 HTTP_X_FORWARDED_FOR 兩種。根據這些 HTTP 頭,服務端可以發現這是一個代理服務器,並且可以追蹤到客戶端的真實 IP 地址。

(3)透明代理

透明代理不僅改動了數據包,還會告訴服務器客戶端的真實 IP 地址,因此在抓 取數據時應該避免使用這種代理服務器。

網上有一些免費代理列表網站會定期掃描互聯網,從而獲取一些代理服務器的信息,然後將這些信息公佈出來。這些代理服務器的有效期可能比較短,也容易被濫用,質量通常較差,所以需要客戶端自己篩選出可用的代理。

在代理的種類上,HTTP 代理多,HTTPS 代理較少。在互聯網倡導 HTTPS 的 趨勢下,單純使用 HTTP 代理是無法訪問 HTTPS 網址的。大部分往往網站會同時保留 HTTPS 和 HTTP 的訪問,所以可以試著將 HTTPS 網址改為 HTTP(協議),一個 原則是,如果網站的 HTTP 可以用,則不要使用 HTTPS。原因是 HTTPS 需要多次握 手,速度比較慢,經過代理之後會顯得更慢。HTTP 則會快很多,而且代理服務器可選資源較多,HTTP 代理列表如圖 1-5 所示,HTTPS 代理列表如圖 1-6 所示。

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-5 HTTP 代理列表

Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-6 HTTPS 代理列表

(4)洋蔥代理

洋蔥代理(The Onion Router,TOR)是用於訪問匿名網絡的軟件,可以防止傳 輸到互聯網上的流量被其他人過濾、嗅探或分析。洋蔥代理在國內無法使用,如果 需要抓取國外的網站,可以在海外的服務器上搭建洋蔥代理,通過它提供的 Socks5 代理端口進行匿名訪問。洋蔥代理的 IP 地址可以進行受控的切換,從而得到不同的 出口 IP 地址。但遺憾的是,洋蔥代理要經過多層的加密和跳轉,延遲時間很長,也不穩定,出口的 IP 地址也並不是隨機地在全球出口選擇,而是固定在一定的區間內, 因而洋蔥代理在多併發、高速的場合下並不適用。

(5)付費代理資源

如果能夠做好代理的質量篩選,那麼大部分場景下免費代理資源都是夠用的。付費代理資源通常用在需要更為穩定的訪問場合或者免費資源不夠用的情況下。ADSL 撥號代理可以提供大量的國內 IP 資源,還可以指定省份。ADSL 撥號代理服 務器可以每隔幾秒鐘就更換IP地址,所以服務器看到的是來自不同的IP地址的訪問。由於使用該 IP 地址的時間不長,不大可能被服務器屏蔽,所以通常數據抓取質量比 較穩定,能夠持續使用。獲得這些代理的方式有以下兩種:

  • 代理列表。服務商會提供一個代理列表訪問地址,每次可以提取一定數量 的代理服務器。這些代理服務器通過 ADSL 代理獲得,它們通常存活時間 不長,根據服務商的不同,一般存活時間在兩三分鐘之內。客戶端必須不 斷地刷新代理服務器列表以取得新的代理列表數據。
  • 服務提供商會提供一個固定的訪問地址和賬號,通過訪問這個固定的地址, 可以得到不停更換的出口 IP 地址。代理商在服務期內會通過二次代理隨機 地將請求分發到不同的代理服務器上。這種方式對於客戶端來說訪問是透 明的,適用於無法通過編程獲得代理服務器列表的應用。

另外,ADSL 撥號代理也可以自行搭建,方法是購買具有 ADSL 撥號網絡的服務器資源,使用腳本定時撥號,等待一段時間後掛斷,從而獲得不斷變化的 IP 地址。

構建自己的代理池

網絡上存在著大量的代理列表可以免費獲取,雖然有效性通常少於 10%,但基

於龐大的數量(通常每日可獲得上萬個),也會有近千個代理可以用。在 GitHub 上 有很多抓取這類代理的項目,但質量良莠不齊,很難滿足需要。經過對比後,我選 擇了 ProxyBroker 這個項目。

ProxyBroker 是一個開源項目,可以從多個源異步查找公共代理並同時檢查它們 的有效性。它比較小巧,代碼不復雜且易於擴展,不依賴於 Redis 等第三方依賴,非 常專注地做好了抓取代理這件事。

特點:

  • 從大約 50 個來源中找到 7000 多個代理工作。
  • 支持協議:HTTP/HTTPS,Socks4/5;還支持 CONNECT 方法的 80 端口和 23 端口(SMTP)。
  • 代理可以按照匿名級別、響應時間、國家和 DNSBL 中的狀態進行過濾。
  • 支持充當代理服務器,將傳入的請求分發到外部代理,使用自動代理輪換。 檢查所有代理是否支持 cookie 和 Referer(如需要,還檢查 POST 請求)。 自動刪除重複的代理。 異步獲取。ProxyBroker 支持命令行操作,可以作為一個單獨的工具使用。

(1)查詢可用代理

使用下面的命令可查詢到前10個美國的高匿名代理,並且支持HTTP和HTTPS。












$ proxybroker find --types HTTP HTTPS --lvl High --countries US –strict -l 10 <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> /<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>

(2)抓取列表並輸出到文件中

使用下面的命令可查詢前 10 個美國的高匿名代理到 proxies.txt 文件中,但是不 執行代理種類和連通性的檢查。













$ proxybroker grab --countries US --limit 10 --outfile ./proxies.txt $ cat proxies.txt <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> <proxy> /<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>

(3)作為代理服務器使用

ProxyBroker 可以作為代理服務器使用。在這種模式下可以很方便地進行 IP 地址 的自動切換,對應用程序透明,對於一些既有的應用程序來說,使用代理服務器來 隱藏身份十分方便。

用法:在一個終端窗口中啟動代理服務器。



$ proxybroker serve --host 127.0.0.1 --port 8888 --types HTTP HTTPS –lvl High Server started at http://127.0.0.1:8888

在另一個終端窗口中,使用這個代理地址訪問 ifconfig.co,即可得到你的代理服 務器地址,而不是你的 IP 地址。

當前網絡的 IP 地址:



$ curl ifconfig.co 202.56.38.130

使用高匿名代理後的 IP 地址:



$ curl -x http://localhost:8888 ifconfig.co 191.103.88.21 

更多的命令及選項可以通過執行 proxybroker --help 獲取。

(4)擴展

若命令行提供的功能並不符合我們的需求,可以對其核心進行擴展以滿足我們 的需求。下面這個例子來自 ProxyBroker 官方代碼,目的是顯示找到的代理的詳細信息:




























import asyncio from proxybroker import Broker async def show(proxies): while True: proxy = await proxies.get() if proxy is None: break print('Found proxy: %s' % proxy) 
proxies = asyncio.Queue() broker = Broker(proxies) tasks = asyncio.gather( broker.find(types=['HTTP', 'HTTPS'], limit=10), show(proxies))
loop = asyncio.get_event_loop() loop.run_until_complete(tasks) $ python3 proxy-broker.py Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> Found proxy: <proxy> /<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>/<proxy>

更多的例子可以參考 ProxyBroker 官方文檔。

(5)構建自己的代理列表池

我們想構造一個代理池,它僅包含一系列不斷刷新的高匿名代理,以方便客戶端的使用。這個代理池僅僅提供代理服務器的地址,並不需要處理額外的事情,客 戶端拿到這些代理服務器地址後,需要對這個列表按照自己的需求進行處理。例如, 對代理進行篩選,對代理服務器的有效性進行評估,對代理服務器進行質量排序, 定時刷新代理列表,等等。

某些代理池軟件設計得較為複雜,將代理的篩選、評價邏輯放到了代理池內部 進行處理,暴露給客戶端的好像是使用一個代理地址,雖然這在一定程度上簡化了 客戶端的邏輯,但由於各個客戶端對代理的使用不盡相同,因此往往限制了客戶端 以佳的方式來使用代理列表。

當然,簡化的代理池也存在一些優點和弊端:

  • 客戶端可能有重複的邏輯,但這種邏輯可以通過代碼共享、包共享等方式 消除。
  • 有些客戶端無法修改源代碼,無法植入代理使用的邏輯。

在設計上,通過爬蟲的方式獲取的代理失效得都比較快,因此我們可以將 ProxyBroker 獲取的代理服務器地址源源不斷地放到 Redis 緩存中,以提供一個含有大量代理地址的列表。首先,對於每個代理,我們需要設置一天的有效期(或者更短),以便能夠自動清除過期的代理。其次,我們需要提供一個簡單的 HTTP 代理服務器,以便能夠為應用程序提供一個代理服務器列表的訪問入口。

通過 ProxyBroker 獲取代理:




























#Proxy-pool-gather.py import asyncio import datetime import logging from proxybroker import Broker import redis
r = redis.Redis(host='localhost', encoding="UTF-8", decode_responses=True) expire_time_s = 60 * 60 * 24 #一天後過期 async def save(proxies): while True: proxy = await proxies.get() if proxy is None: break if "HTTP" not in proxy.types: continue if "High" == proxy.types["HTTP"]: print(proxy) row = '%s://%s:%d' % ("http", proxy.host, proxy.port) r.set(row, 0, ex=expire_time_s)
while True: proxies = asyncio.Queue() broker = Broker(proxies, timeout=2, max_tries=2, grab_timeout=3600) tasks = asyncio.gather(broker.find(types=['HTTP', 'HTTPS']),save(proxies)) loop = asyncio.get_event_loop() loop.run_until_complete(tasks)

HTTP 服務器展示代理列表:

















#Proxy-http-server.py from flask import Flask from flask_restful import Resource, Api import redis app = Flask(__name__) api = Api(app) r = redis.Redis(host='localhost', encoding="UTF-8", decode_responses=True) class Proxy(Resource): def get(self): return r.keys("*")
api.add_resource(Proxy, '/proxy.json')
if __name__ == '__main__': app.run(host="0.0.0.0", port=8000)

在一個終端中運行python3 proxy-pool-gather.py後可以看到代理已經開始抓取工 作。在另一個終端中運行 python3 proxy-http-server.py,訪問 http://localhost:8000/proxy.json 會返回代理列表,如圖 1-7 所示。


Python爬蟲和反爬蟲的鬥爭!掌握這個10K工作不是問題

圖 1-7 代理列表

這時就已經建立好一個代理池供爬蟲工具使用。

(6)增加國內的代理網站

ProxyBroker 提供的代理網站,大多數來自國外的代理列表;在國內,有些網站 因被屏蔽而獲取不到代理資源。針對這種情況,可以把 ProxyBroker 部署到國外的服 務器上以便於尋找代理資源。

增加代理列表網站的解析相對比較容易,在 providers.py 文件中提供了所有的代 理列表網站的解析方法。以快代理為例,增加它的解析非常方便,只需增加一個類, 並且在 PROVIDERS 變量中註冊這個類的實例即可:









class Kuaidaili(Provider): domain = "kuaidaili.com" async def _pipe(self): urls = ["http://www.kuaidaili.com/free/inha/%d" % n for n in range(1, 21)] urls += ["http://www.kuaidaili.com/free/outha/%d" % n for n in range(1, 21)] await self._find_on_pages(urls) PROVIDERS = [ ...... Kuaidaili(), ] 

添加了國內的代理後,再將代理服務器部署到國外的服務器上,一般能夠獲取 大約一萬條的代理資源信息。


分享到:


相關文章: