爬蟲筆記:提高資料採集效率!代理池和執行緒池的使用

程式設計實驗室發表於2022-02-13

前言

爬蟲和反爬蟲是一對矛和盾,反爬蟲很常見的一個方法就是封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模組,它提供了ThreadPoolExecutorProcessPoolExecutor兩個類,實現了對threadingmultiprocessing的進一步抽象(這裡主要關注執行緒池),不僅可以幫我們自動排程執行緒,還可以做到:

  1. 主執行緒可以獲取某一個執行緒(或者任務的)的狀態,以及返回值。
  2. 當一個執行緒完成的時候,主執行緒能夠立即知道。
  3. 讓多執行緒和多程式的編碼介面一致。

所以來看看程式碼吧

程式碼

簡單用法

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方法的第二個引數是要傳給任務的引數列表,所以就是列表裡有多少個引數,就建立多少個任務~

經過測試非常穩,哈哈哈,還是標準庫的東西好用~

參考資料

相關文章