前言
爬蟲和反爬蟲是一對矛和盾,反爬蟲很常見的一個方法就是封IP,一個IP短時間內頻繁訪問,可以做限流或者是加入黑名單,我之前的後臺開發相關部落格也有涉及這一塊。
不過今天說的是爬蟲,所以應對的方法就是用代理池,每次請求都用不同的IP就行,再加上UA模擬,完全是正常使用者的行為,可以避開限流和黑名單反爬。
然後爬蟲是一種IO密集型程式,如果全程單執行緒執行那會很慢,因此可以用多執行緒來提高資料採集效率,不過自己管理多執行緒太麻煩,所以我選擇了執行緒池~
代理池
一個完善的代理池,應該可以實現以下功能
- 批量採集代理(或者通過介面匯入我們購買的代理,不過偶爾用一用還是免費的就好)
- 採集到之後自動驗證代理有效性
- 將有效代理儲存起來
- 提供獲取隨機代理的介面
- 提供管理(刪除、增加)代理的介面
自己造輪子太麻煩了,用Python的初衷不就是”人生苦短,我用Python“嗎,並且社群也沒讓我們失望,開源好用的Python代理池專案有很多,這裡我選了一個在GitHub上有14k+ Stars的專案來用,名字叫ProxyPool
。
經過試用還不錯!
當然還有其他很多執行緒池專案,我沒測試,有興趣的同學可以看看參考資料的第一個連結。
部署執行
專案地址:https://github.com/jhao104/proxy_pool
官方文件提供了兩種部署方式,包括下載程式碼執行和docker,既然有docker那肯定選最方便的docker啦!
不過官方的docker命令還不夠方便,因為這個代理池還需要依賴Redis服務,這裡我寫了一個docker-compose配置來用:
version: "3"
services:
redis:
image: redis
expose:
- 6379
web:
restart: always
image: jhao104/proxy_pool
environment:
- DB_CONN=redis://redis:6379/0
ports:
- "5010:5010"
depends_on:
- redis
找個資料夾儲存一下,然後執行命令啟動docker容器
docker-compose up
這裡我配置的埠是5010跟官網一樣,有需要的同學可以自己修改~
專案啟動起來之後,瀏覽器訪問http://127.0.0.1:5010
,可以得到所有介面,各個介面顧名思義很容易理解。
{
"url": [
{
"desc": "get a proxy",
"params": "type: ''https'|''",
"url": "/get"
},
{
"desc": "get and delete a proxy",
"params": "",
"url": "/pop"
},
{
"desc": "delete an unable proxy",
"params": "proxy: 'e.g. 127.0.0.1:8080'",
"url": "/delete"
},
{
"desc": "get all proxy from proxy pool",
"params": "type: ''https'|''",
"url": "/all"
},
{
"desc": "return proxy count",
"params": "",
"url": "/count"
}
]
}
程式碼中使用
由於這個代理池提供了HTTP介面,理論上可以支援任何語言使用
這裡我用Python來寫
獲取隨機代理
這裡我寫了兩個方法,封裝了獲取隨機代理和刪除代理的操作
import requests
PROXY_POOL_URL = 'http://127.0.0.1:5010'
def get_proxy():
proxy = requests.get(f"{PROXY_POOL_URL}/get/").json().get("proxy")
return {'http': proxy, 'https': proxy}
def delete_proxy(proxy):
requests.get(f"{PROXY_POOL_URL}/delete/?proxy={proxy}")
獲取隨機Header
使用fake_useragent
這個庫來生成隨機的UserAgent
,模擬不同的使用者瀏覽器請求
from fake_useragent import UserAgent
def get_header():
return {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,th;q=0.6",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"User-Agent": ua.random
}
網路請求封裝
因為我們沒有買收費代理,所以使用的是代理池自動採集的免費代理,眾所周知免費代理的質量不好保證,所以我寫了重試功能,失敗次數超過最大重試次數之後就刪除這個代理,換個代理重新來~
最大重試次數可以配置MAX_RETRY_COUNT
變數
MAX_RETRY_COUNT = 5
def request_get(url) -> Tuple[Response, str]:
retry_count = 1
proxy = get_proxy()
while retry_count <= MAX_RETRY_COUNT:
logger.debug(f'第{retry_count}次請求 - 網址 {url} - 代理 {proxy.get("http")}')
try:
resp = requests.get(url, proxies=proxy, headers=get_header(), timeout=15)
return resp, proxy.get('http')
except Exception:
logger.error(f'請求失敗 - 網址 {url}')
retry_count += 1
# 刪除代理池中代理
logger.warning(f'全部{MAX_RETRY_COUNT}次請求都失敗 - 刪除代理 {proxy.get("http")}')
delete_proxy(proxy.get('http'))
return request_get(url)
這個函式返回的是一個(Response, str)
型別的元組,考慮到不同請求拿到的資料格式可能不一樣,所以沒有用resp.json()
或者resp.text
形式,可以呼叫這個函式拿到資料後自行處理。
同時還會返回一個str
型別的代理伺服器地址,是ip:port
形式。
呼叫方法就是這種形式:resp, proxy = request_get(url)
因為我封裝的這個request_get
函式只是最基礎的獲取資料,但拿到的資料不一定是正確可用的,比如觸發了限流或者黑名單,拿到的資料就是空的,這時候在呼叫這個函式拿到資料後可以加一次判斷,假如這個代理IP已經被封禁了,可以呼叫delete_proxy
方法刪除該代理。
執行緒池
爬蟲是一種IO密集型程式,如果全程單執行緒執行那會很慢,因此可以用多執行緒來提高資料採集效率,不過自己管理多執行緒太麻煩,所以我選擇了執行緒池~
執行緒池是一組預先例項化的空閒執行緒,準備好接受工作。為每個要非同步執行的任務建立一個新的執行緒物件是很昂貴的。使用執行緒池,你可以將任務新增到任務佇列,執行緒池為任務分配一個可用執行緒。執行緒池有助於避免建立或銷燬不必要的執行緒。
之前我用過threadpool
這個pip包實現執行緒池,感覺還不錯,但是拿來爬蟲有機率出現不明原因的假死,不知道哪裡出問題了,後面看網上資料說這個threadpool
更適合CPU密集形的操作…
PS:我看了
threadpool
的原始碼實現,牛哇421行程式碼就實現了執行緒池的功能~然後他是基於
threading
模組實現的,可以的
這次我改用Python標準庫自帶的執行緒池實現,事實上,Python裡有兩種“池”
multiprocessing.Pool
multiprocessing.pool.Threadpool
這兩種的異同:
multiprocessing.pool.ThreadPool
的行為方式與 multiprocessing.Pool
相同。不同之處在於 multiprocessing.pool.Threadpool
使用執行緒來執行 worker 的邏輯,而 multiprocessing.Pool
使用工作程式。
但這倆我暫時也不用,因為有更好的選擇。
從Python3.2
開始,標準庫為我們提供了concurrent.futures
模組,它提供了ThreadPoolExecutor
和ProcessPoolExecutor
兩個類,實現了對threading
和multiprocessing
的進一步抽象(這裡主要關注執行緒池),不僅可以幫我們自動排程執行緒,還可以做到:
- 主執行緒可以獲取某一個執行緒(或者任務的)的狀態,以及返回值。
- 當一個執行緒完成的時候,主執行緒能夠立即知道。
- 讓多執行緒和多程式的編碼介面一致。
所以來看看程式碼吧
程式碼
簡單用法
def crawl_data(page):
...
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
pool = ThreadPoolExecutor(8)
logger.info('執行緒池啟動')
tasks = [pool.submit(crawl_data, page) for page in range(1, 100)]
wait(tasks, return_when=ALL_COMPLETED)
logger.info('執行緒池結束')
上面程式碼解析:
crawl_data
函式是爬蟲函式,具體程式碼省略ThreadPoolExecutor(8)
表示建立執行緒池,同時8個執行緒並行- 然後用列表生成器,
pool.submit
方法用來把任務新增到執行緒池 wait
函式用來等待執行緒池執行結束。
除了pool.submit
方法之外,還支援map
方法批量新增任務
使用方法如下:
pool = ThreadPoolExecutor(8)
pool.map(crawl_data, range(1,100))
map
方法的第二個引數是要傳給任務的引數列表,所以就是列表裡有多少個引數,就建立多少個任務~
經過測試非常穩,哈哈哈,還是標準庫的東西好用~
參考資料
- https://suyin-blog.club/2021/2G4HXBY/#proxy-pool-推薦
- python threadpool 的前世今生:https://zhangchenchen.github.io/2017/05/18/python-thread-pool/
- https://www.delftstack.com/zh/howto/python/python-threadpool-differences/
- [python] ThreadPoolExecutor執行緒池:https://www.jianshu.com/p/b9b3d66aa0be