python爬虫之代理池的维护

代理池的维护

1.原因:

网上有大量公开的免费代理和付费代理,但无论是哪种代理,我们都不能保证是可用的,因为可能有各种原因导致代理不可用,这肯定会影响爬虫的效率,所以我们需要提前做筛选,将不可用的代理删除掉,保留可用代理,所以我们需要搭建一个高效的代理池

2代理池基本模架构:

在这里插入图片描述

存储模块:

负责存储抓取下来的代理,保证代理不重复,标识代理的可用情况,动态实时处理每个代理,采用Redis的Sorted Set,即有序集合来存储代理

获取模块:

需要定时在各大代理网站去抓取代理,代理的形式都是IP加端口,尽量从不同来源获取,并保存到数据库

接口模块:

需要API来提供对外服务的接口。如设置一个web API接口,接口通过连接数据库并通过web形式返回可用的代理

检测模块:

需要定时检测数据库中的代理。
1。需要设置一个检测链接,最好是爬取哪个网站就检测哪个网站,这样更加有针对性
2. 需要标识每一个代理的状态,如设置分数标识。

3.代理池的实现:
  • 存储模块:

    这里我们使用Redis的有序集合来存储代理,代理存储的形式为ip+端口的形式存储,且有序集合的每一个元素都有一个分数字段,这个分数字段表示此这个代理的可用程度,从0-100分,有序集合会根据每一个元素的分数对集合进行排序
    当检测到代理分数小于0分时,此代理则被移除代理池,当检测到这个代理可用是时,分数则只为100,这样可以保证所有可用代理有更大的机会被获取。当检测到这个代理不可用时,分数减1,减到0就被移除。
    当新的 代理被获取时,这个新代理的分数被置为10分。
    代码实现如下:

    MAX_SCORE = 100
    MIN_SCORE = 0
    INITIAL_SCORE = 10
    REDIS_HOST = 'localhost'
    REDIS_PORT = 6379
    REDIS_PASSWORD = None
    REDIS_KEY = 'Proxies'
    
    import redis,re
    from random import choice
    
    class RedisClient(object):
        def __init__(self,host=REDIS_HOST,port=REDIS_PORT,password=REDIS_PASSWORD):
            '''
            初始化
            :param host:Redis 地址
            :param port: Redis 端口
            :param password: Redis 密码
            '''
            self.db = redis.StrictRedis(host=host,port=port,password=password,decode_responses=True)
    
        def add(self,proxy,score=INITIAL_SCORE):
            '''
            添加代理,设置分数为初始值
            :param proxy: 代理
            :param score: 分数
            :return: 添加结果
            '''
            if not re.match('\d+\.\d+\.\d+\.\d+\:\d+', proxy):
                print('代理不符合规范', proxy, '丢弃')
                return
            if not self.db.zscore(REDIS_KEY,proxy):  #查询在REDIS_KEY的有序集合中有无proxy
                return self.db.zadd(REDIS_KEY,score,proxy)
    
        def random(self):
            '''
            随机获取有效代理,受限尝试获取最高分数代理,如果最高分数代理不存在,则按照排名获取,否则异常
            :return: 随机代理
            '''
            result = self.db.zrangebyscore(REDIS_KEY,MAX_SCORE,MAX_SCORE)
            print(result)
            if len(result):
                return choice(result)
            else:
                result = self.db.zrevrange(REDIS_KEY,0,100)
                if len(result):
                    return choice(result)
                else:
                    print('None useful proxy')
    
        def decrease(self,proxy):
            '''
            代理值减1分,分数小于最小值,则代理删除
            :param proxy: 代理
            :return: 修改后的代理分数
            '''
            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 exists(self,proxy):
            '''
            判断是否存在
            :param proxy:代理
            :return: 是否存在
            '''
            return not self.db.zscore(REDIS_KEY,proxy) == None
    
        def max(self,proxy):
            '''
            将代理设置为MAX_SCORE
            :param proxy: 代理
            :return: 设置结果
            '''
            print('代理', proxy, '可用,设置为',MAX_SCORE)
            return self.db.zadd(REDIS_KEY,MAX_SCORE,proxy)
    
        def count(self):
            '''
            获取数量
            :return:数量
            '''
            return self.db.zcard(REDIS_KEY)
    
        def all(self):
            '''
            获取全部代理
            :return: 全部代理列表
            '''
            return self.db.zrangebyscore(REDIS_KEY,MIN_SCORE,MAX_SCORE)
    
  • 获取模块:

    我们将获取代理的每个方法统一定义为craw开头,这样扩展的时候只需要添加craw开头的方法即可,这里实现了几个实例,如proxy360,goubanjia等网站。
    这里的一个难点是如何动态的获取所有以craw开头的方法呢?
    这里借助了元类mateclass来实现,对此有疑惑的同学需要先去了解一下python的元类
    我们通过在元类中的__new__()方法中的attrs参数来获取类的所有方法信息,也就是以craw开头的方法,只要遍历attrs属性即可。
    代码实现如下:
    下属代码中的get_page()方法是获取页面源代码的方法,可以通过requests库来实现。

    from pyquery import PyQuery as pq
    #ProxyMetaclass继承type,为一个元类
    class ProxyMeteclass(type):
        '''
        将所有已crawl开头的方法定义成了一个属性,动态获取到所有以crawl开头的方法列表
        '''
        def __new__(cls,name,bases,attrs):
            count = 0
            attrs['Crawfunc'] = []
            for k,v in attrs.items():
                if 'crawl_' in k:
                    attrs['__CrawlFunc__'].append(k)
                    count += 1
            attrs['__CrawFuncCount__'] = count
            return type.__new__(cls,name,bases,attrs)
    
    class Crawler(object,metaclass=ProxyMeteclass):
        def get_proxies(self,callback):
            proxies = []
            for proxy in eval("self.{}".format(callback)): #eval()方法可以执行括号内的表达式
                print('成功获取到代理',proxy)
                proxies.append(proxy)
            return proxies
    
        def craw_daili66(self,page_count=4):
            '''
            获取代理66
            :param page_count:页码
            :return: 代理
            '''
            start_url = 'http://www.66ip.cn{}.html'
            urls = [start_url.format(page) for page in range(1,page_count+1)]
            for url in urls:
                print('Crawling',url)
                html = get_page.get_page(url)
                if html:
                    doc = pq(html)
                    trs = doc('.containerbox table tr:gt(0)').items()
                    for tr in trs:
                        ip = tr.find('td:nth-child(1)').text()
                        port = tr.find('td:nth-child(2)').text()
                        yield ':'.join([ip,port])
    
        def craw_daili360(self):
            '''
            获取代理360
            :return: 代理
            '''
            url = 'http://www.proxy360.cn/Region/China'
            print('Crawling',url)
            html = get_page.get_page(url)
            if html:
                doc = pq(html)
                trs = doc('div[name="list_proxy_ip"]').items()
                for tr in trs:
                    ip = tr.find('.tbBottomLine:nth-child(1)').text()
                    port = tr.find('.tbBottomLine:nth-child(2)').text()
                    yield ':'.join([ip,port])
    
        def craw_goubanjia(self):
            '''
            获取代理goubanjia
            :return: 代理
            '''
            url = 'http://www.goubanjia.cn/free/gngn/index.shtml'
            html = get_page.get_page(url)
            if html:
                doc = pq(html)
                trs = doc('td.ip').items()
                for tr in trs:
                    tr.find('p').remove()
                    yield tr.text().replace(' ','')
    

    在上述代码定义完获取网页代理的方法后,我们还需要定义一类来动态的调用所有以craw开头的方法,然后获取抓取到的代理,将其加入到数据库中存储起来。
    代码的实现:

    from  代理的使用.代理池的维护 import save_proxy as sp
    from  代理的使用.代理池的维护 import get_proxy as gp
    
    POOL_UPPER_THRESHOLD = 10000
    
    class Getter():
        def __init__(self):
            self.redis = sp.RedisClient()
            self.crawler = gp.Crawler()
    
        def is_over_threshold(self):
            '''
            判断代理池是否达到了限制
            :return:
            '''
            if self.redis.count() >= POOL_UPPER_THRESHOLD:
                return True
            else:
                return False
    
        def run(self):
            print('获取器开始执行')
            if not self.is_over_threshold():
                for callback_label in range(self.crawler.__CrawFuncCount__):
                    callback = self.crawler.__CrawFunc__[callback_label]
                    proxies = self.crawler.get_proxies(callback)
                    for proxy in proxies:
                        self.redis.add(proxy)
          
    
  • 检测模块:

在运行这个模块之前,我们已经将各个要爬取的代理网站中的代理都获取下来并存储到数据库中去了,接下来要做的就是检测代理是否可用,可用为100分,不可用则减1分,直到0分为止。
在这里我们为了提高检测效率,我们使用异步请求库aiohttp来进行检测。
request是一个同步请求库,在请求页面时候,有时候页面的加载较慢,这时就会堵塞等待响应,在这段时间内我们完全可以去做其他事情,这便是aiohttp库的用处之一,可以在阻塞时去做其他事情,大大提高效率。
代码的实现如下:
代码中的async关键词是代表这个方法是异步的

VAILD_STATUS_CODES = [200]
TEST_URL = 'https://weixin.sogou.com/weixin?query=nba&type=2'
BATCH_TEST_SIZE = 100

from  代理的使用.代理池的维护 import save_proxy as sp
import aiohttp,asyncio,time

class Tester(object):
    def __init__(self):
        self.redis = sp.RedisClient()           #建立了一个RedisClient对象,供该对象中其他方法使用

    async def test_single_proxy(self,proxy):
        '''
        测试单个代理
        :param proxy:单个代理
        :return: None
        '''
        conn = aiohttp.TCPConnector(verify_ssl=False)
        async with aiohttp.ClientSession(connector=conn) as session :
            try:
                if isinstance(proxy,bytes):
                    proxy = proxy.decode('utf-8')
                    real_proxy = 'http://'+proxy
                    print("正在测试",proxy)
                    async with session.get(TEST_URL,proxy=real_proxy,timeout=15) as response:
                        if response.status in VAILD_STATUS_CODES:
                            self.redis.max(proxy)
                            print('代理可用0',proxy)
                        else:
                            self.redis.decrease(proxy)
                            print('请求响应码不合法',proxy)
            except (TimeoutError,AttributeError,ConnectionError):
                self.redis.decrease(proxy)
                print('代理请求失败',proxy)

    def run(self):
        '''
        主函数
        :return:None
        '''
        print('测试器开始运行')
        try:
            proxies = self.redis.all()
            loop = asyncio.get_event_loop()
            #批量测试
            for i in range(0,len(proxies),BATCH_TEST_SIZE):
                test_proxies = proxies[i:i+BATCH_TEST_SIZE]
                tasks = [self.test_single_proxy(proxy) for proxy in test_proxies]
                loop.run_until_complete(asyncio.wait(tasks))
                time.sleep(6)
        except Exception as e:
            print('测试器发生错误',e.args)

  • 接口模块:

上述的三个模块,我们可以做到了代理的获取,存储和检测。
为了更方便的获取可用代理,并且为了避免以下的问题:

  • 如果代池需要部署在其他远程服务器上,而远程Redis只允许本地连接,那么我们则无法获取代理

  • 如果爬虫运行的主机没有redis或者爬虫并不是python语言编写的,那么我们就无法使用代理
    为了避免以上的问题,我们使用web API的形式来提供有用代理
    在这里我们使用flask来实现这个接口模块
    代码实现如下:

    from flask import Flask,g
    from 代理的使用.代理池的维护 import save_proxy as sp
    
    __all__ = ['WEB_API']
    WEB_API = Flask(__name__)
    
    def get_conn():
        if not hasattr(g,'redis'):
            g.redis = sp.RedisClient()
        return g.redis
    
    #首页
    @WEB_API.route('/')
    def index():
        return '<h2>Welcome to Proxy Pool System</h2>'
    
    #随机代理页
    @WEB_API.route('/random')
    def get_proxy():
        """
        获取随机可用代理
        :return: 随机代理
        """
        conn = get_conn()
        return conn.random()
    
    #获取数量页
    @WEB_API.route('/count')
    def get_counts():
        '''
        获取代理池总量
        :return: 代理池总量
        '''
        conn = get_conn()
        return str(conn.count())
    
    if __name__ == "__main__":
        WEB_API.run()
    
  • 接口模块:

调度模块就是调用以上所定义的三个模块,将这三个模块一多线程的形式运行起来
代码实现如下:
TESTER_ENABLED,GETTER_ENABLED,API_ENABLED三个常量表示三个模块的开关,如果都为True,则代表模块开启

TESTER_CYCLE = 20
GETTER_CYCLE = 20
TESTER_ENABLED = True
GETTER_ENABLED = True
API_ENABLED = True

from multiprocessing import Process
from 代理的使用.代理池的维护.WEB_API import WEB_API
from 代理的使用.代理池的维护.getter import Getter
from 代理的使用.代理池的维护.check import Tester
import time

class Scheduler():
    def schedule_tester(self,cycle=TESTER_CYCLE):
        '''
        定时测试代理
        :param cycle:
        :return:
        '''
        tester = Tester()
        while True:
            print('测试器开始运行')
            tester.run()
            time.sleep(cycle)

    def schedule_getter(self,cycle=GETTER_CYCLE):
        '''
        定时获取代理
        :param cycle:
        :return:
        '''
        getter = Getter()
        while True:
            print('开始抓取代理')
            getter.run()
            time.sleep(cycle)

    def schedule_api(self):
        '''
        开启API
        :return:
        '''
        WEB_API.run('127.0.0.1','8000')

    def run(self):
        print('代理池开始运行')
        if TESTER_ENABLED:
            tester_process = Process(target=self.schedule_tester)
            tester_process.start()

        if GETTER_ENABLED:
            getter_process = Process(target=self.schedule_getter)
            getter_process.start()

        if API_ENABLED:
            api_process = Process(target=self.schedule_api)
            api_process.start()

if __name__ == '__main__':
    scheduler = Scheduler()
    scheduler.run()
4.运行结果:

可用代理显示在页面上,需要的只需要访问页面爬取下来即可
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值