業務需求
現需對某國外圖片網站進行大量爬取,為提高效率使用多進程,對多個子目錄下的圖片同時爬取。由於網站對單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
