在抓取對方網站、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 所示。
圖 1-1 某網站上展示的價格
該網站使用特殊的字型對資料進行了偽裝。例如,3400,對應顯示的是 1400, 如圖 1-2 所示。如果能夠找到所有的字型對應的關係,則可以逆向出正確的價格。
某電影網站使用特殊的字元進行資料隱藏,這種不可見的字元會增加複雜度, 但還是可以通過對應的 UTF-8 字符集找到對應關係,從而得到正確的值,如圖 1-3 所示。
圖 1-2 3400 顯示為 1400
圖 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 所示。
圖 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 所示。
圖 1-5 HTTP 代理列表
圖 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 US 0.33s [HTTP: High] 8.9.31.195:8080> <Proxy US 0.71s [HTTP: High] 104.139.71.46:64663> <Proxy US 0.81s [HTTP: High] 47.75.64.102:80> <Proxy US 0.89s [HTTP: High] 50.93.200.237:2018> <Proxy US 0.93s [HTTP: High] 207.246.69.83:8080> <Proxy US 0.28s [HTTP: High] 47.75.48.149:80> <Proxy US 0.28s [HTTP: High] 47.75.39.130:80> <Proxy US 0.30s [HTTP: High] 47.75.97.82:80> <Proxy US 0.60s [HTTPS] 205.202.42.230:8083> <Proxy US 0.40s [HTTP: High] 47.75.126.109:80>(2)抓取列表並輸出到檔案中
使用下面的命令可查詢前 10 個美國的高匿名代理到 proxies.txt 檔案中,但是不 執行代理種類和連通性的檢查。
$ proxybroker grab --countries US --limit 10 --outfile ./proxies.txt $ cat proxies.txt <Proxy US 0.00s [] 75.128.59.155:80> <Proxy US 0.00s [] 8.46.64.42:1080> <Proxy US 0.00s [] 206.71.228.193:8841> <Proxy US 0.00s [] 69.59.84.76:14471> <Proxy US 0.00s [] 98.142.237.108:80> <Proxy US 0.00s [] 104.139.73.239:19330> <Proxy US 0.00s [] 216.54.3.252:27723> <Proxy US 0.00s [] 24.227.184.162:1080> <Proxy US 0.00s [] 8.9.31.195:8080> <Proxy US 0.00s [] 206.189.85.147:1080>(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 HK 0.24s [HTTP: High] 47.90.87.225:88> Found proxy: <Proxy HK 0.24s [HTTP: High] 47.52.231.140:8080> Found proxy: <Proxy HK 0.25s [HTTP: High] 47.89.41.164:80> Found proxy: <Proxy ID 0.32s [HTTP: Transparent] 222.124.145.94:8080> Found proxy: <Proxy US 0.40s [HTTP: Transparent] 47.254.22.115:8080> Found proxy: <Proxy US 0.37s [HTTP: High] 40.78.60.44:8080> Found proxy: <Proxy ID 0.37s [HTTP: High] 103.240.109.171:53281> Found proxy: <Proxy MX 0.38s [HTTP: High] 201.167.56.18:53281> Found proxy: <Proxy EC 0.51s [HTTP: High] 190.214.0.154:53281> Found proxy: <Proxy RU 0.54s [HTTP: High] 46.173.191.51:53281>更多的例子可以參考 ProxyBroker 官方文件。
(5)構建自己的代理列表池
我們想構造一個代理池,它僅包含一系列不斷重新整理的高匿名代理,以方便客戶端的使用。這個代理池僅僅提供代理伺服器的地址,並不需要處理額外的事情,客 戶端拿到這些代理伺服器地址後,需要對這個列表按照自己的需求進行處理。例如, 對代理進行篩選,對代理伺服器的有效性進行評估,對代理伺服器進行品質排序, 定時重新整理代理列表,等等。
某些代理池軟體設計得較為複雜,將代理的篩選、評價邏輯放到了代理池內部 進行處理,暴露給客戶端的好像是使用一個代理地址,雖然這在一定程度上簡化了 客戶端的邏輯,但由於各個客戶端對代理的使用不盡相同,因此往往限制了客戶端 以佳的方式來使用代理列表。
當然,簡化的代理池也存在一些優點和弊端:
客戶端可能有重複的邏輯,但這種邏輯可以通過程式碼共享、包共享等方式 消除。有些客戶端無法修改原始碼,無法植入代理使用的邏輯。在設計上,通過爬蟲的方式獲取的代理失效得都比較快,因此我們可以將 ProxyBroker 獲取的代理伺服器地址源源不斷地放到 Redis 快取中,以提供一個含有大量代理地址的列表。首先,對於每個代理,我們需要設定一天的有效期(或者更短),以便能夠自動清除過期的代理。其次,我們需要提供一個簡單的 HTTP 代理伺服器,以便能夠為應用程式提供一個代理伺服器列表的訪問入口。
通過 ProxyBroker 獲取代理:
#Proxy-pool-gather.py import asyncio import datetime import logging from proxybroker import Broker import redisr = 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 所示。
圖 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(), ]添加了國內的代理後,再將代理伺服器部署到國外的伺服器上,一般能夠獲取 大約一萬條的代理資源資訊。