Python+Redis維護一個代理池

對一個網(wǎng)站過高頻率的爬取可能會導(dǎo)致ip被加入黑名單紧显,這樣以后對該網(wǎng)站的訪問都會被拒絕,這個時候只能利用代理ip了层皱。網(wǎng)上有大量的公開免費代理性锭,然而大多是不能用的,也可以購買付費代理使用叫胖,但是手動設(shè)置ip非常麻煩草冈,并且可能由于其他人爬取同樣站點而被封禁,或者代理服務(wù)器故障等原因?qū)е耰p不可用,手動篩選可用ip也是非常麻煩的事情怎棱。

為了解決這個問題方淤,我們可以維護一個代理池,定期自動檢查以剔除其中的不可用代理蹄殃,并不斷爬取新的代理投入使用,項目地址你踩。

1. 結(jié)構(gòu)設(shè)計


1.1 存儲模塊

代理應(yīng)該是不重復(fù)的诅岩,并且要標(biāo)識可用情況,比較好的方法是使用redis的有序集合存儲带膜,每一個元素都是一個代理吩谦,形式為 ip:端口號 ,此外,每一個元素都有一個分數(shù)膝藕,用來標(biāo)識可用情況式廷,分數(shù)規(guī)則設(shè)置為:

  • 分數(shù)100為可用,定期檢測時代理可用就設(shè)置為100芭挽,不可用就將分數(shù)減1滑废,直到減為0后刪除
  • 新獲取代理設(shè)置為10,測試可行立即置為100袜爪,否則減1

1.2 獲取模塊

定期從各大代理網(wǎng)站爬取代理蠕趁,免費付費都可以,成功之后保存到數(shù)據(jù)庫

1.3 測試模塊

負責(zé)定期檢測數(shù)據(jù)庫中的代理辛馆,可以設(shè)置檢測鏈接俺陋,檢測在對應(yīng)網(wǎng)站是否可用

1.4 接口模塊

提供一個web api來對外提供隨機的可用代理,用flask實現(xiàn)

2. 模塊實現(xiàn)


2.1 存儲模塊(db.py)

第三方庫:

  • redis
import redis
from random import choice

# 分數(shù)設(shè)置
MAX_SCORE = 100
MIN_SCORE = 0
INIT_SCORE = 10

# 連接信息
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PASSWORD = None
REDIS_KEY = 'proxies'

# 數(shù)據(jù)庫最大存儲的代理數(shù)量
POOL_UPPER_THRESHLD = 10000


class RedisClient:

    def __init__(self, host=REDIS_HOST, port=REDIS_PORT, passwd=REDIS_PASSWORD):
        """初始化redis對象"""
        self.db = redis.Redis(host=host, port=port, password=passwd, decode_responses=True)

    def add(self, proxy, score=INIT_SCORE):
        """添加一個代理昙篙,設(shè)置初始分數(shù)"""
        if not self.db.zscore(REDIS_KEY, proxy):
            return self.db.zadd(REDIS_KEY, {proxy: score})

    def random(self):
        """首先隨機獲取最高分的有效代理腊状,不存在則按排名獲取"""
        result = self.db.zrangebyscore(REDIS_KEY, MAX_SCORE, MAX_SCORE)
        if len(result):
            return choice(result)
        else:
            # 從分數(shù)前50的代理中隨機獲取一個
            result = self.db.zrevrange(REDIS_KEY, 0, 50)
            if len(result):
                return choice(result)
            else:
                Exception('無可用代理')

    def decrease(self, proxy):
        """代理分數(shù)-1,小于指定閾值則刪除"""
        score = self.db.zscore(REDIS_KEY, proxy)
        if score and score > MIN_SCORE:
            print(proxy, score, '-1')
            return self.db.zincrby(REDIS_KEY, proxy, -1)
        else:
            print(proxy, score, '移除')
            return self.db.zrem(REDIS_KEY, proxy)

    def max(self, proxy):
        """更新代理分數(shù)到最大值"""
        print(proxy, MAX_SCORE)
        return self.db.zadd(REDIS_KEY, MAX_SCORE, proxy)

    def count(self):
        """獲取代理數(shù)量"""
        return self.db.zcard(REDIS_KEY)

    def all(self):
        """獲取全部代理"""
        return self.db.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)

2.2 獲取模塊(getter.py)

第三方庫:

  • beautifulsoup4
  • requests
from bs4 import BeautifulSoup

from db import RedisClient, POOL_UPPER_THRESHLD
# requests獲取一個網(wǎng)頁的方法封裝到了一個工具類里
from utils import get_page


class ProxyMetaClass(type):
    """元類苔可,初始化類時獲取所有以crawl_開頭的方法"""

    def __new__(mcs, name, bases, attrs):
        count = 0
        attrs['CrawlFunc'] = []
        for k, v in attrs.items():
            if 'crawl_' in k:
                attrs['CrawlFunc'].append(k)
                count += 1
        attrs['CrawlFuncCount'] = count
        return type.__new__(mcs, name, bases, attrs)


class Crawler(metaclass=ProxyMetaClass):

    def get_proxies(self, crawl_func):
        """執(zhí)行指定方法來獲取代理"""
        proxies = []
        for proxy in eval("self.{}()".format(crawl_func)):
            proxies.append(proxy)
        return proxies

    def crawl_kuaidaili(self, page_count=10):
        """獲取快代理網(wǎng)站的免費代理"""
        start_url = 'https://www.kuaidaili.com/free/inha/{}/'
        urls = [start_url.format(page) for page in range(1, page_count + 1)]
        for url in urls:
            html = get_page(url)
            if html:
                soup = BeautifulSoup(html, 'lxml')
                trs = soup.find('tbody').find_all('tr')
                for tr in trs:
                    ip = tr.find_all('td')[0].string
                    port = tr.find_all('td')[1].string
                    yield ':'.join([ip, port])


class Getter:
    def __init__(self):
        """初始化數(shù)據(jù)庫類和代理爬蟲類"""
        self.redis = RedisClient()
        self.crawler = Crawler()

    def is_over_threshold(self):
        """判斷數(shù)據(jù)庫是否已經(jīng)存滿"""
        if self.redis.count() >= POOL_UPPER_THRESHLD:
            return True
        return False

    def run(self):
        """開始抓取各個代理網(wǎng)站的免費代理存入數(shù)據(jù)庫"""
        print('開始獲取...')
        if not self.is_over_threshold():
            for i in range(self.crawler.CrawlFuncCount):
                crawl_func = self.crawler.CrawlFunc[i]
                proxies = self.crawler.get_proxies(crawl_func)
                for proxy in proxies:
                    print(proxy)
                    self.redis.add(proxy)


if __name__ == '__main__':
    a = Getter()
    a.run()

這里借助元類實現(xiàn)了自動定義屬性 CrawlFunc 來記錄Crawler類中所有以 crawl_ 開頭的方法缴挖,CrawlFuncCount 記錄這些方法的數(shù)量,這樣以后有新的代理網(wǎng)站可以獲取代理時硕蛹,只需要添加 crawl_XXX 方法以相同的方式返回解析后的數(shù)據(jù)就行了醇疼, get_proxies 會依次執(zhí)行傳入的方法列表中的方法, 可以非常方便的擴展法焰。

然后就是實現(xiàn)Getter類來管理存儲部分秧荆,run方法中先判斷是否到達存儲閾值,未滿則通過 crawler.CrawlFunc 獲取所有爬取方法交給 get_proxies 去執(zhí)行埃仪,然后將返回結(jié)果存入數(shù)據(jù)庫乙濒。這樣存儲部分的工作就完成了,實例化Getter類并調(diào)用run方法即可爬取代理并存入數(shù)據(jù)庫。

2.3 測試模塊

第三方庫:

  • aiohttp
import asyncio
import aiohttp
import time

from db import RedisClient


# 目標(biāo)網(wǎng)址
TEST_URL = 'http://www.baidu.com'
# 正確的響應(yīng)碼列表
TRUE_STATUS_CODE = [200]
# 同時測試一組代理的數(shù)量
BATCH_TEST_SIZE = 50


class Tester:

    def __init__(self):
        """初始化數(shù)據(jù)庫管理對象"""
        self.redis = RedisClient()

    async def test_one_proxy(self, proxy):
        """對目標(biāo)網(wǎng)站測試一個代理是否可用"""
        conn = aiohttp.TCPConnector(ssl=False)
        async with aiohttp.ClientSession(connector=conn) as session:
            try:
                if isinstance(proxy, bytes):
                    # 解碼為字符串
                    proxy = proxy.decode('utf-8')
                real_proxy = 'http://' + proxy
                async with session.get(TEST_URL, proxy=real_proxy, timeout=15) as response:
                    if response.status in TRUE_STATUS_CODE:
                        # 代理可用
                        self.redis.max(proxy)
                        print(proxy, 100)
                    else:
                        # 代理不可用
                        self.redis.decrease(proxy)
                        print(proxy, -1, "狀態(tài)碼錯誤")
            except Exception as e:
                self.redis.decrease(proxy)
                print(proxy, -1, e.args)

    async def start(self):
        """啟動協(xié)程颁股, 測試所有代理"""
        try:
            proxies = self.redis.all()
            for i in range(0, len(proxies), BATCH_TEST_SIZE):
                test_proxies = proxies[i: i+BATCH_TEST_SIZE]
                tasks = [self.test_one_proxy(proxy) for proxy in test_proxies]
                # 并發(fā)完成一批任務(wù)
                await asyncio.gather(*tasks)
                time.sleep(5)
        except Exception as e:
            print('測試器發(fā)生錯誤', e.args)

    def run(self):
        """開始運行"""
        asyncio.run(self.start())
        # # python3.7之前的寫法
        # loop = asyncio.get_event_loop()
        # loop.run_until_complete(self.start())


if __name__ == '__main__':
    a = Tester()
    a.run()

由于代理數(shù)量非常大么库,同步請求的requests很難高效率的完成這個任務(wù),我們使用異步請求庫aiohttp甘有,同時用到協(xié)程asyncio來成批的檢測代理诉儒。

測試網(wǎng)站這里設(shè)置為百度,使用時可以根據(jù)目標(biāo)不同設(shè)置對應(yīng)網(wǎng)站的網(wǎng)址亏掀。還定義了一個正確的狀態(tài)碼列表忱反,目前只有200,某些網(wǎng)站可能會有重定向等操作滤愕,可以自行添加狀態(tài)碼温算。

2.4 API模塊

第三方庫:

  • flask
from flask import Flask, g
import json

from db import RedisClient


app = Flask(__name__)


def get_conn():
    if not hasattr(g, 'redis'):
        g.redis = RedisClient()
    return g.redis


@app.route('/')
def index():
    return '<h2>hello</h2>'


@app.route('/get/')
def get_proxy():
    """獲取隨機可用代理"""
    conn = get_conn()
    try:
        proxy = conn.random()
        result = json.dumps({'status': 'success', 'proxy': proxy})
    except Exception as e:
        result = json.dumps({'status': 'failure', 'info': e})
    finally:
        return result


@app.route('/count/')
def get_count():
    """獲取代理總數(shù)"""
    conn = get_conn()
    return str(conn.count())


if __name__ == '__main__':
    app.run()

這里使用flask做了一個簡單的API,包含歡迎頁间影,獲取隨機代理頁和獲取代理總數(shù)頁注竿,獲取代理頁返回json格式的數(shù)據(jù)。

3. 整合運行


模塊寫好了魂贬,接下來只需要以多進程的方式將他們運行起來即可

from multiprocessing import Process

from api import app
from getter import Getter
from tester import Tester


# 周期
TESTER_CYCLE = 20
GETTER_CYCLE = 20

# 模塊開關(guān)
TESTER_ENABLE = True
GETTER_ENABLE = True
API_ENABLE = True


class Run:
    def run_tester(self, cycle=TESTER_CYCLE):
    """定時檢測代理可用情況"""
        tester = Tester()
        while True:
            print('開始測試')
            tester.run()
            time.sleep(cycle)

    def run_getter(self, cycle=GETTER_CYCLE):
        """定時獲取代理"""
        getter = Getter()
        while True:
            print('開始抓取代理')
            getter.run()
            time.sleep(cycle)

    def run_api(self):
    """啟動API接口"""
        app.run()

    def run(self):
        print('代理池開始運行')
        if TESTER_ENABLE:
            tester_process = Process(target=self.run_tester)
            tester_process.start()
        if GETTER_ENABLE:
            getter_process = Process(target=self.run_getter)
            getter_process.start()
        if API_ENABLE:
            api_process = Process(target=self.run_api)
            api_process.start()


if __name__ == '__main__':
    a = Run()
    a.run()

運行此文件便可以開始整個代理池的運行:

控制臺輸出

獲取代理

4. 使用實例


import requests
import json


def get_proxy():
    """嘗試獲取代理"""
    try:
        r = requests.get('http://localhost:5000/get/')
        r.raise_for_status
        r.encoding = r.apparent_encoding
        return r.text
    except ConnectionError:
        return None


r = get_proxy()
if r:
    # 加載返回的json數(shù)據(jù)
    data = json.loads(r)
    if data['status'] == 'success':
        proxy = data['proxy']
        # 構(gòu)造代理字典巩割,根據(jù)請求鏈接不同自動設(shè)置
        proxies = {
            'http': 'http://' + proxy,
            'https': 'http://' + proxy
        }
        try:
            # 使用代理訪問網(wǎng)站
            r = requests.get('http://www.baidu.com/', proxies=proxies)
            r.status_code
            r.encoding = r.apparent_encoding
            print(r.text)
        except Exception as e:
            print(e.args)
    else:
        print(data['info'])
else:
    print('未正常獲取代理')
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市随橘,隨后出現(xiàn)的幾起案子喂分,更是在濱河造成了極大的恐慌,老刑警劉巖机蔗,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒲祈,死亡現(xiàn)場離奇詭異,居然都是意外死亡萝嘁,警方通過查閱死者的電腦和手機梆掸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牙言,“玉大人酸钦,你說我怎么就攤上這事≡弁鳎” “怎么了卑硫?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚕断。 經(jīng)常有香客問我欢伏,道長,這世上最難降的妖魔是什么亿乳? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任硝拧,我火速辦了婚禮径筏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘障陶。我一直安慰自己滋恬,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布抱究。 她就那樣靜靜地躺著恢氯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鼓寺。 梳的紋絲不亂的頭發(fā)上酿雪,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音侄刽,去河邊找鬼。 笑死朋凉,一個胖子當(dāng)著我的面吹牛州丹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杂彭,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼墓毒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了亲怠?” 一聲冷哼從身側(cè)響起所计,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎团秽,沒想到半個月后主胧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡习勤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年踪栋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片图毕。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡夷都,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出予颤,到底是詐尸還是另有隱情囤官,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布蛤虐,位于F島的核電站党饮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏笆焰。R本人自食惡果不足惜劫谅,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捏检,春花似錦荞驴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至能犯,卻和暖如春鲫骗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背踩晶。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工执泰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人渡蜻。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓术吝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親茸苇。 傳聞我的和親對象是個殘疾皇子排苍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 做過爬蟲的應(yīng)該都知道,在爬取反爬比較強的網(wǎng)站如果同一時間獲取的數(shù)據(jù)量過大就會導(dǎo)致封IP,例如豆瓣,搜狗之類的...
    NGUWQ閱讀 1,925評論 0 1
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹 對...
    cosWriter閱讀 11,109評論 1 32
  • feisky云計算、虛擬化與Linux技術(shù)筆記posts - 1014, comments - 298, trac...
    不排版閱讀 3,866評論 0 5
  • 目錄 ·大型網(wǎng)站軟件系統(tǒng)的特點 ·大型網(wǎng)站架構(gòu)演化發(fā)展歷程 ·初始階段的網(wǎng)站架構(gòu) ·需求/解決問題 ·架構(gòu) ·應(yīng)用...
    zhyang0918閱讀 2,672評論 0 16
  • 幾兄妹終于聚在一起了学密。 談到了協(xié)會和展覽館的發(fā)展淘衙。我和拓哥都堅持一個理念:協(xié)會招收會員必定要有門檻,只招志趣相投有...
    可可愛愛的可可姐閱讀 178評論 0 0