使用代理服務器一直是爬蟲防BAN最有效的手段,但網上的免費代理往往質量很低,大部分代理完全不能使用,剩下能用的代理很多也只有幾分鐘的壽命,沒法直接用到爬蟲項目中。
下面簡單記錄一下我用scrapy+redis實現動態代理池的過程。
我對“動態代理池” 的需求
我的爬蟲項目需要7*24小時監控若干個頁面,考慮了一下希望代理池能滿足下面幾個要求:
- 始終保持一個相對穩定的代理數量
- 始終保持池內代理的高可靠率(希望90%的代理都能用)
- 盡可能減少對爬蟲項目代碼的更改
參考過的項目
找過一些現成的輪子,但發現或多或少都不太符合我的需求
kohn/HttpProxyMiddleware
一個“被動”選擇代理的方式。在scrapy middleware中加入大量代碼,讓爬蟲在代理失效后再去代理網站找代理,會有下面幾個問題:
- 大多數情況下始終使用一個代理去訪問,造成流量集中在一個IP上,可能導致代理IP被BAN。
- 切換代理的過程比較耗時,導致爬蟲性能下降比較厲害
- 所有邏輯都嵌套在了爬蟲項目里面,有兼容性和可移植性方面的問題(有多個爬蟲的話每個都要改一遍..)
- 基于python2寫的,我的項目在python3上..
jhao104/proxy_pool
這個幾乎和我想要的一樣,但還是因為下面幾點原因沒有使用:
- 基于SSDB,這邊沒有現成的SSDB,因為已經在用Redis了不想專門去裝一個
- 只管定時收集代理,在收集過程中做一次驗證。但事實上收集到的代理即便通過了驗證,也可能活不過10分鐘,時間一長代理池內就會有很多過期的代理,需要在爬蟲代碼中處理這些過期代理(爬蟲代碼調用delete刪除)。
設計思路和代碼
看了那么多方案,我就在想:能不能讓代理池具有自我檢查自我修復的功能?這樣我們的爬蟲就只需隨機拿一個代理用就可以了,就算恰巧拿到一個失效代理,只要做一次retry,代理池的修復機制可以保證retry的時候失效的代理已經被移走了,不會再次取到。
簡單規劃一下:
- 我們需要一個自檢程序,它每10S跑一次,保證代理池內所有代理至少在10S內有效
- 我們需要一個代理獲取的程序,當代理池內代理數量過低時(閾值定為<5),訪問免費代理網站補充新代理
- 我們需要一個調度程序,用來監控代理池數量并調用上面兩個程序,維持代理池平衡。
- 作為私有代理池就維護在redis了(SET),讓爬蟲直接從redis里取代理。
對應的代碼就有這么三個部分:
- 一個scrapy爬蟲去爬代理網站,獲取免費代理,驗證后入庫 (proxy_fetch)
- 一個scrapy爬蟲把代理池內的代理全部驗證一遍,若驗證失敗就從代理池內刪除 (proxy_check)
- 一個調度程序用于管理上面兩個爬蟲 (start.py)
強行畫個流程圖:
爬蟲部分全部用scrapy寫了,其實沒必要,用requests就夠了..但做的時候剛學scrapy,就順便練練手了..
start.py的調度方式比較粗暴,直接起兩個線程,在線程內用os.system調用scrapy爬蟲(畢竟輕量級代理池嘛..好吧其實就是我懶圖個方便..)
另外還有幾個調度策略需要說一下:
- 每次調用proxy_fetch后會自動設定一個“保護時間”10分鐘,在保護期內除非代理池只剩一個代理了,否則不會觸發proxy_fetch,避免頻繁調用。
- 加入了一個“刷新時間”24小時,保證每24小時內至少執行proxy_fetch一次
- 驗證程序設定timeout為5秒,5秒內沒訪問到測試頁面就認為驗證失敗
其他細節可以看源碼:arthurmmm/hq-proxies
部署和使用
需要先改一下配置文件hq-proxies.yml,把Redis的地址密碼之類的填上,改完后放到/etc/hq-proxies.yml下。
在配置文件中也可以調整相應的閾值和免費代理源和測試頁面。
測試頁面需要頻繁訪問,為了節省流量我在某云存儲上丟了個helloworld的文本當測試頁面了,云存儲有流量限制建議大家換掉。。驗證方式很粗暴,比較一下網頁開頭字符串。。
另外寫了個Dockerfile可以直接部署到Docker上(python3用的是Daocloud的鏡像),跑容器的時候記得把hq-proxies.yml映射到容器/etc/hq-proxies.yml下。
手工部署的話跑pip install -r requirements.txt
安裝依賴包
在scrapy中使用代理池的只需要添加一個middleware,每次爬取時從redis SET里用srandmember隨機獲取一個代理使用,代理失效和一般的請求超時一樣retry,代理池的自檢特性保證了我們retry時候再次拿到失效代理的概率很低。middleware代碼示例:
class DynamicProxyMiddleware(object):
def process_request(self, request, spider):
redis_db = StrictRedis(
host=LOCAL_CONFIG['REDIS_HOST'],
port=LOCAL_CONFIG['REDIS_PORT'],
password=LOCAL_CONFIG['REDIS_PASSWORD'],
db=LOCAL_CONFIG['REDIS_DB']
)
proxy = redis_db.sismember(PROXY_SET, proxy):
logger.debug('使用代理[%s]訪問[%s]' % (proxy, request.url))
request.meta['proxy'] = proxy
在其他爬蟲框架下使用也是類似的。
最后放一張萌萌噠日志菌觸發proxy_fetch時候的截圖: