Redis教程之代理ip池設(shè)計(jì)方法詳解
前言
眾所周知代理 ip 因?yàn)榕渲煤?jiǎn)單而且廉價(jià),經(jīng)常用來(lái)作為反反爬蟲(chóng)的手段,但是穩(wěn)定性一直是其詬病。篩選出優(yōu)質(zhì)的代理 ip 并不簡(jiǎn)單,即使付費(fèi)購(gòu)買(mǎi)的代理 ip 源,賣(mài)家也不敢保證 100% 可用;另外代理 ip 的生命周期也無(wú)法預(yù)知,可能上一秒能用,下一秒就撲街了?;谶@些原因,會(huì)給使用代理 ip 的爬蟲(chóng)程序帶來(lái)很多不穩(wěn)定的因素。要排除代理 ip 的影響,通常的做法是建一個(gè)代理 ip 池,每次請(qǐng)求前來(lái)池子取一個(gè) ip,用完之后歸還,保證池子里的 ip 都是可用的。本文接下來(lái)就探討一下,如何使用 Redis 構(gòu)建代理 ip 池,實(shí)現(xiàn)自動(dòng)更新,自動(dòng)擇優(yōu)。
整體流程
由上圖所示,左側(cè)是形成了整個(gè)流程的閉環(huán),從爬蟲(chóng)程序以獨(dú)占的方式拿到一個(gè)代理 ip 到爬取完成歸還 ip。這個(gè)流程其實(shí)是不太嚴(yán)謹(jǐn)?shù)?,如果爬蟲(chóng)程序異常中斷,就會(huì)導(dǎo)致 ip 無(wú)法歸還,就會(huì)導(dǎo)致這個(gè) ip 無(wú)法循環(huán)利用。但是由于代理 ip 本身的特點(diǎn),量多而且循環(huán)利用的價(jià)值并不大,所以這種情況就let it go。
上面也提到 ip 是以獨(dú)占的方式獲取,如果是去爬兩個(gè)毫不相關(guān)的網(wǎng)站,本來(lái)一個(gè) ip 就可以,可現(xiàn)在需要兩個(gè)。為了資源最大化使用,這里引入了頻道 ip 池和總代理 ip 池。兩個(gè)網(wǎng)站就當(dāng)做兩個(gè)頻道,各自獨(dú)占,互不相關(guān);總池子就是保存所有的 ip,每個(gè)頻道都共享。假設(shè)只有一個(gè) ip:1.1.1.1 在總池子,爬 A 網(wǎng)站會(huì)把它從總池子取到 A 頻道的 ip 池,然后 A 爬蟲(chóng)程序從 A 頻道 ip 池取出 1.1.1.1 進(jìn)行使用,這時(shí) 1.1.1.1 依然在總池子里,但 A 頻道的 ip 池已經(jīng)不包含 1.1.1.1 了;爬 B 網(wǎng)站也是一樣的流程拿到 1.1.1.1,只是從 B 自己的頻道池獲取。下面就詳細(xì)說(shuō)說(shuō)總池子和頻道池子。
總代理 ip 池
總池子的作用就是共享所有可用的 ip,但是僅作為存儲(chǔ) ip 的池子并不能實(shí)現(xiàn)自動(dòng)擇優(yōu)啊,這里的擇優(yōu)通常是希望延遲低速度快的 ip 更容易被篩選出,所以我們希望池子中的 ip 是根據(jù)它們的延時(shí)升序排列,借助 Redis 的 Sorted Sets
數(shù)據(jù)結(jié)構(gòu)即可實(shí)現(xiàn),用延時(shí)表示 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,可以指定獲取的個(gè)數(shù),比如取兩個(gè):
> 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 池。由于一個(gè) ip 使用次數(shù)過(guò)多是有很大的概率被目標(biāo)網(wǎng)站屏蔽掉,所以這里也需要進(jìn)行擇優(yōu),應(yīng)該優(yōu)先篩選出使用次數(shù)少的 ip,同理也是使用 Sorted Sets
,使用次數(shù)表示 score,ip 表示 member,這里與總池子明顯的不同之處是 key 不是固定的,需要把頻道名稱(chēng)組合進(jìn)去,這樣保證頻道之間的隔離,如頻道 abc 的 key:proxy_channel_abc_ips
。
由于頻道池子中的 ip 是要以獨(dú)占的方式取出,我們需要一個(gè) ZPOP
的方法,奈何 Redis 本身沒(méi)有,還好可以通過(guò) Lua 模擬,在一個(gè)原子操作下取出 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
這里與總池子不同的是多了一個(gè) INCR
選項(xiàng),這是 Redis 3.0.2 版本后才支持的新特性,即指定在 ZADD 時(shí)發(fā)生 member 沖突采取的處理方式,INCR
顧名思義是沖突后累加 score 的方式,為什么要用這個(gè)選項(xiàng),看看下面這個(gè)流程:
- 在頻道池子中只有 1.1.1.1,使用次數(shù)為 10;總池子也有 1.1.1.1,而且排在第一個(gè)
- 線程 A 取出 1.1.1.1
- 線程 B 從頻道池子取 ip,沒(méi)取到,從總池子補(bǔ)充 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 的計(jì)數(shù)被錯(cuò)誤地重置為 1,而不是我們預(yù)期的 12。使用 INCR
選項(xiàng)就可以避免這個(gè)尷尬,其實(shí)這也只能保證最終計(jì)數(shù)正確,中途還是會(huì)有些非預(yù)期的情況,如:
- 在頻道池子中有 1.1.1.1,使用次數(shù)為 10,還有 2.2.2.2,使用次數(shù)為 2;總池子也有 1.1.1.1,而且排在第一個(gè)
- 線程 A 取出 1.1.1.1
- 線程 B 取出 2.2.2.2
- 線程 C 從頻道池子取 ip,沒(méi)取到,從總池子補(bǔ)充 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 來(lái)池子取 ip,按使用次數(shù)少的被分配了 1.1.1.1,這就不是我們期望的,1.1.1.1 實(shí)際已經(jīng)用了 12 次,我們更希望 2.2.2.2 被取出
如果要避免這個(gè)問(wèn)題,一個(gè)簡(jiǎn)單粗暴的辦法就是增加頻道池子的容量,讓 ip 數(shù)永遠(yuǎn)大于并發(fā)的線程數(shù)。
更新
與 ip 有關(guān)的兩個(gè)屬性:延時(shí)(爬取頁(yè)面所花的時(shí)間)和使用次數(shù)。上面只講到了根據(jù)它們自動(dòng)擇優(yōu),這里的就來(lái)說(shuō)下它們是如何更新的。延時(shí)和使用次數(shù)的更新需要爬蟲(chóng)程序的配合,程序中要記錄時(shí)間和遞增使用次數(shù),在歸還 ip 時(shí)要將最新值帶回給總池子和頻道池子。上面頻道 ip 池的例子也有提及,每次歸還 ip 都要將最新的使用次數(shù)帶上,其次還要將 ip 的延時(shí)更新到總池子里面。如果歸還 ip 時(shí)出現(xiàn)使用失敗的情況,就要將該 ip 從總池子里刪除掉,保證該 ip 不會(huì)再被使用,至于當(dāng)前的頻道池不用歸還就行了。其他頻道池不作任何處理,因?yàn)?ip 在當(dāng)前頻道不可用,一般都是因?yàn)楸黄帘?,其他頻道依然可以使用,即使確實(shí)都不能使用,也會(huì)在其他頻道歸還 ip 時(shí)被刪除。
這兩個(gè)屬性其實(shí)也可以都在 Redis 中更新,在獲取 ip 時(shí),使用 Hashs
保存 ip 對(duì)應(yīng)的獲取時(shí)間和使用次數(shù);在歸還時(shí)從 Hashs
中取出時(shí)間計(jì)算出延時(shí),取出使用次數(shù)并加 1,再分別更新到總池子和頻道池子中。而且這還能避免上面提到的獲取 ip 不符合預(yù)期的問(wèn)題。
總結(jié)
放在 Redis 中更新的方法也有弊端,延時(shí)會(huì)包含獲取和歸還的傳輸時(shí)間,如果爬蟲(chóng)程序獲取一個(gè) ip 多次使用,會(huì)造成使用次數(shù)統(tǒng)計(jì)偏少。當(dāng)然也可以通過(guò)在程序中多次調(diào)用 Redis 更新 ip 的屬性來(lái)解決,這樣增加了整個(gè)流程的復(fù)雜性,需要自己權(quán)衡。
個(gè)人還是傾向在程序中記錄,最后更新到 Redis 中。這個(gè)方案邏輯確實(shí)不夠嚴(yán)謹(jǐn),但是出現(xiàn)問(wèn)題也不會(huì)導(dǎo)致嚴(yán)重后果。程序的健壯性也不是不允許出現(xiàn) bug,而是出現(xiàn) bug 有很好的容錯(cuò)性。
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流。
相關(guān)文章
Redis集群的三種部署方式及三種應(yīng)用問(wèn)題的處理
這篇文章主要介紹了Redis集群的三種部署方式及三種應(yīng)用問(wèn)題的處理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04Redis在項(xiàng)目中常見(jiàn)的12種使用場(chǎng)景示例和說(shuō)明
Redis是一個(gè)開(kāi)源的高性能鍵值對(duì)數(shù)據(jù)庫(kù),它以其內(nèi)存中數(shù)據(jù)存儲(chǔ)、鍵過(guò)期策略、持久化、事務(wù)、豐富的數(shù)據(jù)類(lèi)型支持以及原子操作等特性,在許多項(xiàng)目中扮演著關(guān)鍵角色,以下是整理的12個(gè)Redis在項(xiàng)目中常見(jiàn)的使用場(chǎng)景舉例說(shuō)明和解釋2024-06-06基于redis實(shí)現(xiàn)token驗(yàn)證用戶(hù)是否登陸
這篇文章主要為大家詳細(xì)介紹了基于redis實(shí)現(xiàn)token驗(yàn)證用戶(hù)是否登陸,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08Redis使用ZSET實(shí)現(xiàn)消息隊(duì)列的項(xiàng)目實(shí)踐
本文主要介紹了Redis使用ZSET實(shí)現(xiàn)消息隊(duì)列的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07Redis和Nginx實(shí)現(xiàn)限制接口請(qǐng)求頻率的示例
限流就是限制API訪問(wèn)頻率,當(dāng)訪問(wèn)頻率超過(guò)某個(gè)閾值時(shí)進(jìn)行拒絕訪問(wèn)等操作,本文主要介紹了Redis和Nginx實(shí)現(xiàn)限制接口請(qǐng)求頻率的示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02