python代理池搭建

熟悉爬蟲的,必定會熟悉各種反爬機制。今天就講一下自己如何建立ip代理池的。

一個合格的代理池必須擁有一個爬取代理IP的爬取器、一個驗證IP可否使用的校驗器、一個存儲IP的數據庫、調用這些的調度器以及可以供獲取IP的接口(這裏推薦flask,比較簡單)。

先來說說爬取器,首先要爬取的代理IP網站儘量是無需登錄的,其次是對代理IP更新較快的,前者加快代理池的效率,後者增加代理池的質量。這裏我對市面上部分代理網站進行爬取,當然一些常用的代理IP網站提供IP質量不高,比如西刺無憂66這些經常被爬取(西刺偶爾還會崩潰,估計是爬取的人有些多,網站保護性503)

    def crawl_xici():
        """
        西刺代理:http://www.xicidaili.com
        """
        url = "http://www.xicidaili.com/{}"

        items = []
        for page in range(1, 21):
            items.append(("wt/{}".format(page), "http://{}:{}"))
            items.append(("wn/{}".format(page), "https://{}:{}"))

        for item in items:
            proxy_type, host = item
            html = requests(url.format(proxy_type))
            if html:
                doc = pyquery.PyQuery(html)
                for proxy in doc("table tr").items():
                    ip = proxy("td:nth-child(2)").text()
                    port = proxy("td:nth-child(3)").text()
                    if ip and port:
                        yield host.format(ip, port)


    def crawl_zhandaye():
        """
        站大爺代理:http://ip.zdaye.com/dayProxy.html
        """
        url = 'http://ip.zdaye.com/dayProxy.html'
        html = requests(url)
        sttrs = re.findall('<H3 class="title"><a href="(.*?)">', html, re.S)
        for sttr in sttrs:
            new_url = url[:28] + sttr[9:]
            new_html = requests_other(new_url)
            get_div = re.search("<div class=\"cont\">(.*?)</div>", new_html, re.S).group(1)
            print(get_div)
            results = re.findall("<br>(.*?)@(.*?)#\[(.*?)\]", get_div, re.S)
            for result in results:
                yield "{}://{}".format(result[1].lower(), result[0])


    def crawl_66ip():
        """
        66ip 代理:http://www.66ip.cn
        19-04-30可用
        """
        url = (
            "http://www.66ip.cn/nmtq.php?getnum=100&isp=0"
            "&anonymoustype=0&area=0&proxytype={}&api=66ip"
        )
        pattern = "\d+\.\d+.\d+\.\d+:\d+"

        items = [(0, "http://{}"), (1, "https://{}")]
        for item in items:
            proxy_type, host = item
            html = requests(url.format(proxy_type))
            if html:
                for proxy in re.findall(pattern, html):
                    yield host.format(proxy)


    def crawl_kuaidaili():
        """
        快代理:https://www.kuaidaili.com
        每次30個
        19-04-13可用
        """
        url = "https://www.kuaidaili.com/free/inha/{}/"

        items = [p for p in range(1, 3)]
        for page in items:
            html = requests(url.format(page))
            if html:
                doc = pyquery.PyQuery(html)
                for proxy in doc(".table-bordered tr").items():
                    ip = proxy("[data-title=IP]").text()
                    port = proxy("[data-title=PORT]").text()
                    if ip and port:
                        yield "http://{}:{}".format(ip, port)

    def crawl_ip3366():
        """
        雲代理:http://www.ip3366.net
        每頁10個,驗證較快
        19-04-30可用
        """
        url = "http://www.ip3366.net/?stype=1&page={}"

        items = [p for p in range(1, 8)]
        for page in items:
            html = requests(url.format(page))
            if html:
                doc = pyquery.PyQuery(html)
                for proxy in doc(".table-bordered tr").items():
                    ip = proxy("td:nth-child(1)").text()
                    port = proxy("td:nth-child(2)").text()
                    schema = proxy("td:nth-child(4)").text()
                    if ip and port and schema:
                        yield "{}://{}:{}".format(schema.lower(), ip, port)

    def crawl_data5u():
        """
        無憂代理:http://www.data5u.com/
        每次14個,驗證時間比較新
        19-04-30可用
        """
        url = "http://www.data5u.com/free/index.html"

        html = requests(url)
        if html:
            doc = pyquery.PyQuery(html)
            for index, item in enumerate(doc(".wlist li .l2").items()):
                if index > 0:
                    ip = item("span:nth-child(1)").text()
                    port = item("span:nth-child(2)").text()
                    schema = item("span:nth-child(4)").text()
                    if ip and port and schema:
                        yield "{}://{}:{}".format(schema, ip, port)

    def crawl_iphai():
        """
        ip 海代理:http://www.iphai.com
        爬取國內高匿、國外高匿、國外普通各10個
        19-04-30可用
        """
        url = "http://www.iphai.com/free/{}"

        items = ["ng", "np", "wg", "wp"]
        for proxy_type in items:
            html = requests(url.format(proxy_type))
            if html:
                doc = pyquery.PyQuery(html)
                for item in doc(".table-bordered tr").items():
                    ip = item("td:nth-child(1)").text()
                    port = item("td:nth-child(2)").text()
                    schema = item("td:nth-child(4)").text().split(",")[0]
                    if ip and port and schema:
                        yield "{}://{}:{}".format(schema.lower(), ip, port)

解釋一下代碼,返回的代理一般都是(http/https)://ip:port格式的代理。這裏的requests是使用 asyncio、aiohttp做了個方法,來實現異步爬取。對於asyncio的介紹可以查看廖雪峯的教程。這個是自己寫的異步爬取,替換了之前的request.get方法,加快爬蟲效率。

import asyncio
import aiohttp

from settings import HEADERS, REQUEST_TIMEOUT, REQUEST_DELAY


LOOP = asyncio.get_event_loop()

async def _get_page(url, sleep):
    """
    獲取並返回網頁內容
    """
    async with aiohttp.ClientSession() as session:
        try:
            await asyncio.sleep(sleep)
            async with session.get(
                url, headers=HEADERS, timeout=REQUEST_TIMEOUT
            ) as resp:
                return await resp.text()
        except:
            return ""

def requests(url, sleep=REQUEST_DELAY):
    """
    請求方法,用於獲取網頁內容

    :param url: 請求鏈接
    :param sleep: 延遲時間(秒)
    """
    html = LOOP.run_until_complete(asyncio.gather(_get_page(url, sleep)))
    if html:
        return "".join(html)

做好異步爬取工作就應該完成代理池搭建的30%了,之後我們就要嘗試保存進數據庫了,這裏推薦使用redis數據庫,畢竟其中的有序集合類型(sorted set)非常適合代理池cookies池的搭建,因爲其中是有score的,也就是我們存入一個代理的同時也要給它一個分數,這方便我們之後對其校驗以及取代理IP的優先級。redis有序集合類型

# redis 地址
REDIS_HOST = "localhost"
# redis 端口
REDIS_PORT = 6379
# redis 密碼
REDIS_PASSWORD = None
# redis set key
REDIS_KEY = "myproxies"
# redis 連接池最大連接量
REDIS_MAX_CONNECTION = 20
# REDIS SCORE 最大分數
MAX_SCORE = 10
# REDIS SCORE 最小分數
MIN_SCORE = 0
# REDIS SCORE 初始分數
INIT_SCORE = 5

class RedisClient:
    """
    代理池依賴了 Redis 數據庫,使用了其`有序集合`的數據結構
    (可按分數排序,key 值不能重複)
    """
    def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
        conn_pool = redis.ConnectionPool(
            host=host,
            port=port,
            password=password,
            max_connections=REDIS_MAX_CONNECTION,
        )
        self.redis = redis.Redis(connection_pool=conn_pool)

    def add_proxy(self, proxy, score=INIT_SCORE):
        """
        新增一個代理,初始化分數 INIT_SCORE < MAX_SCORE,確保在
        運行完收集器後還沒運行校驗器就獲取代理,導致獲取到分數雖爲 MAX_SCORE,
        但實際上確是未經驗證,不可用的代理

        :param proxy: 新增代理
        :param score: 初始化分數
        """
        if not self.redis.zscore(REDIS_KEY, proxy):
            self.redis.zadd(REDIS_KEY, proxy, score)

    def reduce_proxy_score(self, proxy):
        """
        驗證未通過,分數減一
        :param proxy: 驗證代理
        """
        score = self.redis.zscore(REDIS_KEY, proxy)
        if score and score > MIN_SCORE:
            self.redis.zincrby(REDIS_KEY, proxy, -1)
        else:
            self.redis.zrem(REDIS_KEY, proxy)

    def increase_proxy_score(self, proxy):
        """
        驗證通過,分數加一
        :param proxy: 驗證代理
        """
        score = self.redis.zscore(REDIS_KEY, proxy)
        if score and score < MAX_SCORE:
            self.redis.zincrby(REDIS_KEY, proxy, 1)

    def pop_proxy(self):
        """
        返回一個代理
        """
        # 第一次嘗試取分數最高,也就是最新可用的代理
        first_chance = self.redis.zrangebyscore(REDIS_KEY, MAX_SCORE, MAX_SCORE)
        if first_chance:
            return random.choice(first_chance)

        else:
            # 第二次嘗試取 7-10 分數的任意一個代理
            second_chance = self.redis.zrangebyscore(
                REDIS_KEY, MAX_SCORE - 3, MAX_SCORE
            )
            if second_chance:
                return random.choice(second_chance)
            # 最後一次就隨便取咯
            else:
                last_chance = self.redis.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
                if last_chance:
                    return random.choice(last_chance)

    def get_proxies(self, count=1):
        """
        返回指定數量代理,分數由高到低排序
        :param count: 代理數量
        """
        proxies = self.redis.zrevrange(REDIS_KEY, 0, count - 1)
        for proxy in proxies:
            yield proxy.decode("utf-8")

    def count_all_proxies(self):
        """
        返回所有代理總數
        """
        return self.redis.zcard(REDIS_KEY)

    def count_score_proxies(self, score):
        """
        返回指定分數代理總數
        :param score: 代理分數
        """
        if 0 <= score <= 10:
            proxies = self.redis.zrangebyscore(REDIS_KEY, score, score)
            return len(proxies)
        return -1

    def clear_proxies(self, score):
        """
        刪除分數小於等於 score 的代理
        """
        if 0 <= score <= 10:
            proxies = self.redis.zrangebyscore(REDIS_KEY, 0, score)
            for proxy in proxies:
                self.redis.zrem(REDIS_KEY, proxy)
            return True
        return False

    def all_proxies(self):
        """
        返回全部代理
        """
        return self.redis.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)

這就是寫的對redis數據庫的操作,至於我們如何把爬取的IP放入,就需要改一下我們的爬取器了,封裝成一個類會比我寫這樣的方法好得多。

redis_conn = RedisClient()
all_funcs = []


def collect_funcs(func):
    """
    裝飾器,用於收集爬蟲函數
    """
    all_funcs.append(func)
    return func


class Crawler:
    """
        返回格式: http://host:port
    """
    @staticmethod
    def run():
        """
        啓動收集器
        """
        for func in all_funcs:
            for proxy in func():
                redis_conn.add_proxy(proxy)

    #添加之前寫的爬取ip方法,在每個方法之前聲明裝飾器
    @collect_funcs

#然後實例個對象
crawl = Crawl()

完成了數據庫和爬取器的搭建,就差不多完成七七八八了,現在就加驗證器,驗證器是爲了驗證代理IP是否可用,如何可以使用的話就給它加一分,如果不可以使用的話就減一分

import os
import asyncio

import aiohttp

from db import RedisClient

#驗證url
VALIDATOR_BASE_URL = "http://baidu.com"
#批量測試數量
VALIDATOR_BATCH_COUNT = 250

class Validator:
    def __init__(self):
        self.redis = RedisClient()

    async def test_proxy(self, proxy):
        """
        測試代理
        :param proxy: 指定代理
        """
        async with aiohttp.ClientSession() as session:
            try:
                if isinstance(proxy, bytes):
                    proxy = proxy.decode("utf8")
                async with session.get(
                    VALIDATOR_BASE_URL, proxy=proxy, timeout=REQUEST_TIMEOUT
                ) as resp:
                    if resp.status == 200:
                        self.redis.increase_proxy_score(proxy)
                    else:
                        self.redis.reduce_proxy_score(proxy)
            except:
                self.redis.reduce_proxy_score(proxy)

    def run(self):
        """
        啓動校驗器
        """
        proxies = self.redis.all_proxies()
        loop = asyncio.get_event_loop()
        for i in range(0, len(proxies), VALIDATOR_BATCH_COUNT):
            _proxies = proxies[i : i + VALIDATOR_BATCH_COUNT]
            tasks = [self.test_proxy(proxy) for proxy in _proxies]
            if tasks:
                loop.run_until_complete(asyncio.wait(tasks))

validator = Validator()

網上有許多檢驗IP的方法,諸如requests.get  telnet之類的,這裏利用的是aiohttp的session,其實這個檢驗都差不多的,只是在爬取IP那邊利用的異步,乾脆檢驗這裏也用異步吧。

之後寫個調度器,調度器就是運行項目後,在每隔一個時間段後運行某個方法,這裏我們設置循環時間,爬取是30分鐘,檢驗是15分鐘,然後啓動爬取器和校驗器的run方法

import time

import schedule

from crawler import crawler
from validator import validator

#爬取ip檢查時間(分)
CRAWLER_RUN_CYCLE = 30

#驗證ip檢查時間(分)
VALIDATOR_RUN_CYCLE = 15

def run_schedule():
    """
    啓動客戶端
    """
    # 啓動收集器
    schedule.every(CRAWLER_RUN_CYCLE).minutes.do(crawler.run).run()
    # 啓動驗證器
    schedule.every(VALIDATOR_RUN_CYCLE).minutes.do(validator.run).run()

    while True:
        try:
            schedule.run_pending()
            time.sleep(1)
        except KeyboardInterrupt:
            return

最後就是整個項目如何運行了,我們單純跑調度器肯定是不合適的,因爲這樣redis中有代理了,但是我們要在爬蟲項目中連接redis來獲取代理,這一點是比較麻煩的,而且每次運行都要在本地跑一次代理池,這樣肯定不符合程序員偷懶的初衷。正確的做法是做一個api接口,然後部署到雲端,讓雲服務器爬取代理IP,這樣我們就每次訪問api接口就能得到我們的數據了,下面來寫寫接口,這裏用的是Flask,因爲簡單

from flask import Flask, jsonify
from db import RedisClient
from scheduler import run_schedule


myapp = Flask(__name__)
redis_conn = RedisClient()

@myapp.route("/")
def index():
    return jsonify({"Welcome": "This is a proxy pool system."},
                   {"if there has problem": "Please communicate with QQ:976264593"})

@myapp.route("/pop")
def pop_proxy():
    proxy = redis_conn.pop_proxy().decode("utf8")
    if proxy[:5] == "https":
        return jsonify({"https": proxy})
    else:
        return jsonify({"http": proxy})

@myapp.route("/get/<int:count>")
def get_proxy(count):
    res = []
    for proxy in redis_conn.get_proxies(count):
        if proxy[:5] == "https":
            res.append({"https": proxy})
        else:
            res.append({"http": proxy})
    return jsonify(res)

@myapp.route("/count")
def count_all_proxies():
    count = redis_conn.count_all_proxies()
    return jsonify({"count": str(count)})

@myapp.route("/count/<int:score>")
def count_score_proxies(score):
    count = redis_conn.count_score_proxies(score)
    return jsonify({"count": str(count)})

@myapp.route("/clear/<int:score>")
def clear_proxies(score):
    if redis_conn.clear_proxies(score):
        return jsonify({"Clear": "Successful"})
    return jsonify({"Clear": "Score should >= 0 and <= 10"})

if __name__ == "__main__":
    # 啓動服務端 Flask app
    myapp.run(host='localhost', port=5000, debug=True)
    run_schedule()

這個接口比較簡單,實現了查看代理池數量,獲取代理IP(推薦pop因爲使用後就可以將代理IP刪除)當然有許多低分的代理我們也可以通過接口調用將其刪除。出於安全性,其實這個api應該加個校驗的,不然別人一直用你的代理池就白費功夫了。

好了,這就是python代理池搭建,如果有不懂可以提出來,博主也是小白一個

源碼鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章