基於redis實現高並發下的IP代理池可靠更換


業務需求

現需對某國外圖片網站進行大量爬取,為提高效率使用多進程,對多個子目錄下的圖片同時爬取。由於網站對單IP的下載量有限額,需要在額度耗盡時自動從代理池里更換新代理。IP的可用額度無法在本地計算或實時獲取,只有在耗盡時才能從目標網站得到異常通知。

業務分析

雖然是單機並發,但所面對的問題其實屬於分布式領域。由於網站並未對訪問頻率作出限制,所以只需考慮IP的下載總量即可,可讓所有進程都走同一個代理IP;又因為代理會隨時變更,所以應該在每次下載請求時實時獲取,這里使用redis維護代理地址比較合適。爬蟲用Python編寫,所以用Python的redis客戶端。

業務實現

假設代理池中有5個地址,192.168.1.1:1080~192.168.1.1:1084,首先我們隨機選一個,比如192.168.1.1:1080,作為初始代理。在redis中新建元素proxy和proxylist

127.0.0.1:6379> set proxy 192.168.1.1:1080
127.0.0.1:6379> RPUSH proxylist 192.168.1.1:1080 192.168.1.1:1081 192.168.1.1:1082 192.168.1.1:1083 192.168.1.1:1084

Python代碼

# 版本1

def getproxy():
    proxy = rconn.get('proxy').decode()
    return {'http':proxy, 'https':proxy}

def changeproxy():
    proxies = rconn.lrange('proxylist', 0, 5)
    current_proxy = getproxy().get('http')
    rconn.set(current_proxy, 'cooling', ex=100000) #建立一個key為當前代理IP的鍵,表示此IP已經進入冷卻,並用ex設定冷卻時間
    for proxy in proxies:
        if not rconn.get(proxy): #排除在冷卻時間內的IP
            res=rconn.set('proxy',proxy)
            return res
    
    return None

但此法不久后在實踐中遇到了一個問題:某次一個干凈的代理被設置成冷卻中了;后發現,因為任務是並發的,當1號請求返回異常並更改了代理后,使用了前代理的2號請求才返回出異常,於是又觸發了一次更換請求,相當於短時間內連續更換了兩次代理,造成了資源的浪費。

# 版本2

def getproxy():
    proxy = rconn.get('proxy').decode()
    return {'http':proxy, 'https':proxy}

def changeproxy():
    proxies = rconn.lrange('proxylist', 0, 5)
    current_proxy = getproxy().get('http')
    '''
    檢測是否有保護標志
    '''
    if rconn.get('protect_time'): 
        return True
    rconn.set(current_proxy, 'cooling', ex=100000) #建立一個key為當前代理IP的鍵,表示此IP已經進入冷卻,並用ex設定冷卻時間
    for proxy in proxies:
        if not rconn.get(proxy): #排除在冷卻時間內的IP
            res=rconn.set('proxy',proxy)
            '''
            在成功更換代理后,放置一個有效期為30s的保護標志,該標志存在期間禁止代理更換。這個有效期理論上最短要設置為一次請求報文的往返時間
            '''
            rconn.set('protect_time', 'yes', ex=30)
            return res
    
    return None

這種方法能避免有效代理被跳過,但如果代理池里不小心混入了臟代理,且被更換到了,那在這30s的保護時間內,臟代理也會被“保護”,即使時間不長,我們也要想辦法避免。筆者想了很久,有沒有在客戶端不傳任何參數的情況下解決這一點,包括錯誤計數器,進程標識,保護時間削減等等,但最后發現,唯一可靠的還是客戶端傳當前代理給代理服務器。

# 版本3(最終)

def getproxy():
    proxy = rconn.get('proxy').decode()
    return {'http':proxy, 'https':proxy}

def changeproxy(local_proxy):
    # local_proxy是客戶端發起請求並被返回異常時所用的代理IP
    proxies = rconn.lrange('proxylist', 0, 5)
    current_proxy = getproxy().get('http')
    if local_proxy!=current_proxy:
        return True
    else:
        rconn.set(current_proxy, 'cooling', ex=100000) #建立一個key為當前代理IP的鍵,表示此IP已經進入冷卻,並用ex設定冷卻時間
        for proxy in proxies:
            if not rconn.get(proxy): #排除在冷卻時間內的IP
                res=rconn.set('proxy',proxy)
                return res
        
        return None

可以看到,最終版本不僅更加簡潔,也解決了上述提到的問題。

------

如果要細究,最終版也是有漏洞的,因為整個更換代理的操作並不具備原子性,依舊可能造成代理被跳過(雖然概率極其微小)。而redis又沒有真正的事務,所以最好為changeproxy()再加一把鎖,或者對最終版做一處微小的修改:

    else:
        flag = rconn.set(current_proxy, 'cooling', ex=100000, nx=True) #nx參數令存在該鍵時就不建立,並返回false
        if not flag:
            return True
        for proxy in proxies:
            if not rconn.get(proxy): 
                res=rconn.set('proxy',proxy)
                return res


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM