前言
眾所周知代理 ip 因為配置簡單而且廉價,經(jīng)常用來作為反反爬蟲的手段,但是穩(wěn)定性一直是其詬病。篩選出優(yōu)質(zhì)的代理 ip 并不簡單,即使付費購買的代理 ip 源,賣家也不敢保證 100% 可用;另外代理 ip 的生命周期也無法預(yù)知,可能上一秒能用,下一秒就撲街了?;谶@些原因,會給使用代理 ip 的爬蟲程序帶來很多不穩(wěn)定的因素。要排除代理 ip 的影響,通常的做法是建一個代理 ip 池,每次請求前來池子取一個 ip,用完之后歸還,保證池子里的 ip 都是可用的。本文接下來就探討一下,如何使用 Redis 構(gòu)建代理 ip 池,實現(xiàn)自動更新,自動擇優(yōu)。
整體流程
由上圖所示,左側(cè)是形成了整個流程的閉環(huán),從爬蟲程序以獨占的方式拿到一個代理 ip 到爬取完成歸還 ip。這個流程其實是不太嚴謹?shù)模绻老x程序異常中斷,就會導(dǎo)致 ip 無法歸還,就會導(dǎo)致這個 ip 無法循環(huán)利用。但是由于代理 ip 本身的特點,量多而且循環(huán)利用的價值并不大,所以這種情況就let it go。
上面也提到 ip 是以獨占的方式獲取,如果是去爬兩個毫不相關(guān)的網(wǎng)站,本來一個 ip 就可以,可現(xiàn)在需要兩個。為了資源最大化使用,這里引入了頻道 ip 池和總代理 ip 池。兩個網(wǎng)站就當做兩個頻道,各自獨占,互不相關(guān);總池子就是保存所有的 ip,每個頻道都共享。假設(shè)只有一個 ip:1.1.1.1 在總池子,爬 A 網(wǎng)站會把它從總池子取到 A 頻道的 ip 池,然后 A 爬蟲程序從 A 頻道 ip 池取出 1.1.1.1 進行使用,這時 1.1.1.1 依然在總池子里,但 A 頻道的 ip 池已經(jīng)不包含 1.1.1.1 了;爬 B 網(wǎng)站也是一樣的流程拿到 1.1.1.1,只是從 B 自己的頻道池獲取。下面就詳細說說總池子和頻道池子。
總代理 ip 池
總池子的作用就是共享所有可用的 ip,但是僅作為存儲 ip 的池子并不能實現(xiàn)自動擇優(yōu)啊,這里的擇優(yōu)通常是希望延遲低速度快的 ip 更容易被篩選出,所以我們希望池子中的 ip 是根據(jù)它們的延時升序排列,借助 Redis 的 Sorted Sets
數(shù)據(jù)結(jié)構(gòu)即可實現(xiàn),用延時表示 score,ip 表示 member。
使用 ZADD
添加新 ip 或更新 ip 的延遲:
> ZADD proxy_global_ips 200 1.1.1.1:8080 100 2.2.2.2:80 300 3.3.3.3:8888
(integer) 3
使用 ZRANGE
獲取 ip,可以指定獲取的個數(shù),比如取兩個:
> ZRANGE proxy_global_ips 0 1 WITHSCORES
1) "2.2.2.2:80"
2) "100"
3) "1.1.1.1:8080"
4) "200"
頻道 ip 池
頻道 ip 池的作用是為了最大化使用總池子中的 ip,并且隔離其他頻道的 ip 池。由于一個 ip 使用次數(shù)過多是有很大的概率被目標網(wǎng)站屏蔽掉,所以這里也需要進行擇優(yōu),應(yīng)該優(yōu)先篩選出使用次數(shù)少的 ip,同理也是使用 Sorted Sets
,使用次數(shù)表示 score,ip 表示 member,這里與總池子明顯的不同之處是 key 不是固定的,需要把頻道名稱組合進去,這樣保證頻道之間的隔離,如頻道 abc 的 key:proxy_channel_abc_ips
。
由于頻道池子中的 ip 是要以獨占的方式取出,我們需要一個 ZPOP
的方法,奈何 Redis 本身沒有,還好可以通過 Lua 模擬,在一個原子操作下取出 ip,然后刪除:
> eval "local el = redis.call('zrange', KEYS[1], 0, 0, 'WITHSCORES'); redis.call('zrem', KEYS[1], el[1]); return el;" 1 proxy_channel_abc_ips
往頻道 ip 池添加 ip:
> ZADD proxy_channel_abc_ips INCR 0 1.1.1.1:8080
這里與總池子不同的是多了一個 INCR
選項,這是 Redis 3.0.2 版本后才支持的新特性,即指定在 ZADD 時發(fā)生 member 沖突采取的處理方式,INCR
顧名思義是沖突后累加 score 的方式,為什么要用這個選項,看看下面這個流程:
- 在頻道池子中只有 1.1.1.1,使用次數(shù)為 10;總池子也有 1.1.1.1,而且排在第一個
- 線程 A 取出 1.1.1.1
- 線程 B 從頻道池子取 ip,沒取到,從總池子補充 ip 到頻道池子:
ZADD proxy_channel_abc_ips 0 1.1.1.1
;取出 1.1.1.1
- 線程 A 歸還 1.1.1.1:
ZADD proxy_channel_abc_ips 11 1.1.1.1
- 線程 B 歸還 1.1.1.1:
ZADD proxy_channel_abc_ips 1 1.1.1.1
第 5 步結(jié)束后,ip 1.1.1.1 的計數(shù)被錯誤地重置為 1,而不是我們預(yù)期的 12。使用 INCR
選項就可以避免這個尷尬,其實這也只能保證最終計數(shù)正確,中途還是會有些非預(yù)期的情況,如:
- 在頻道池子中有 1.1.1.1,使用次數(shù)為 10,還有 2.2.2.2,使用次數(shù)為 2;總池子也有 1.1.1.1,而且排在第一個
- 線程 A 取出 1.1.1.1
- 線程 B 取出 2.2.2.2
- 線程 C 從頻道池子取 ip,沒取到,從總池子補充 ip 到頻道池子:
ZADD proxy_channel_abc_ips 0 1.1.1.1
;取出 1.1.1.1
- 線程 C 歸還 1.1.1.1:
ZADD proxy_channel_abc_ips INCR 1 1.1.1.1
- 線程 B 歸還 2.2.2.2:
ZADD proxy_channel_abc_ips INCR 3 2.2.2.2
- 線程 D 來池子取 ip,按使用次數(shù)少的被分配了 1.1.1.1,這就不是我們期望的,1.1.1.1 實際已經(jīng)用了 12 次,我們更希望 2.2.2.2 被取出
如果要避免這個問題,一個簡單粗暴的辦法就是增加頻道池子的容量,讓 ip 數(shù)永遠大于并發(fā)的線程數(shù)。
更新
與 ip 有關(guān)的兩個屬性:延時(爬取頁面所花的時間)和使用次數(shù)。上面只講到了根據(jù)它們自動擇優(yōu),這里的就來說下它們是如何更新的。延時和使用次數(shù)的更新需要爬蟲程序的配合,程序中要記錄時間和遞增使用次數(shù),在歸還 ip 時要將最新值帶回給總池子和頻道池子。上面頻道 ip 池的例子也有提及,每次歸還 ip 都要將最新的使用次數(shù)帶上,其次還要將 ip 的延時更新到總池子里面。如果歸還 ip 時出現(xiàn)使用失敗的情況,就要將該 ip 從總池子里刪除掉,保證該 ip 不會再被使用,至于當前的頻道池不用歸還就行了。其他頻道池不作任何處理,因為 ip 在當前頻道不可用,一般都是因為被屏蔽,其他頻道依然可以使用,即使確實都不能使用,也會在其他頻道歸還 ip 時被刪除。
這兩個屬性其實也可以都在 Redis 中更新,在獲取 ip 時,使用 Hashs
保存 ip 對應(yīng)的獲取時間和使用次數(shù);在歸還時從 Hashs
中取出時間計算出延時,取出使用次數(shù)并加 1,再分別更新到總池子和頻道池子中。而且這還能避免上面提到的獲取 ip 不符合預(yù)期的問題。
總結(jié)
放在 Redis 中更新的方法也有弊端,延時會包含獲取和歸還的傳輸時間,如果爬蟲程序獲取一個 ip 多次使用,會造成使用次數(shù)統(tǒng)計偏少。當然也可以通過在程序中多次調(diào)用 Redis 更新 ip 的屬性來解決,這樣增加了整個流程的復(fù)雜性,需要自己權(quán)衡。
個人還是傾向在程序中記錄,最后更新到 Redis 中。這個方案邏輯確實不夠嚴謹,但是出現(xiàn)問題也不會導(dǎo)致嚴重后果。程序的健壯性也不是不允許出現(xiàn) bug,而是出現(xiàn) bug 有很好的容錯性。
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
您可能感興趣的文章:- 超強、超詳細Redis數(shù)據(jù)庫入門教程
- 30個php操作redis常用方法代碼例子
- redis 隊列操作的例子(php)
- redis常用命令、常見錯誤、配置技巧等分享
- 64位Windows下安裝Redis教程
- Flyway數(shù)據(jù)庫版本控制的教程詳解
- 利用Python爬取可用的代理IP
- 利用TaskManager爬取2萬條代理IP實現(xiàn)自動投票功能
- php代碼檢查代理ip的有效性
- 通過Python爬蟲代理IP快速增加博客閱讀量