zoukankan      html  css  js  c++  java
  • 基于redis实现高并发下的IP代理池可靠更换

    业务需求

    现需对某国外图片网站进行大量爬取,为提高效率使用多进程,对多个子目录下的图片同时爬取。由于网站对单IP的下载量有限额,需要在额度耗尽时自动从代理池里更换新代理。IP的可用额度无法在本地计算或实时获取,只有在耗尽时才能从目标网站得到异常通知。

    业务分析

    虽然是单机并发,但所面对的问题其实属于分布式领域。由于网站并未对访问频率作出限制,所以只需考虑IP的下载总量即可,可让所有进程都走同一个代理IP;又因为代理会随时变更,所以应该在每次下载请求时实时获取,这里使用redis维护代理地址比较合适。爬虫用Python编写,所以用Python的redis客户端。

    业务实现

    假设代理池中有5个地址,192.168.1.1:1080~192.168.1.1:1084,首先我们随机选一个,比如192.168.1.1:1080,作为初始代理。在redis中新建元素proxy和proxylist

    127.0.0.1:6379> set proxy 192.168.1.1:1080
    127.0.0.1:6379> RPUSH proxylist 192.168.1.1:1080 192.168.1.1:1081 192.168.1.1:1082 192.168.1.1:1083 192.168.1.1:1084

    Python代码

    # 版本1
    
    def getproxy():
        proxy = rconn.get('proxy').decode()
        return {'http':proxy, 'https':proxy}
    
    def changeproxy():
        proxies = rconn.lrange('proxylist', 0, 5)
        current_proxy = getproxy().get('http')
        rconn.set(current_proxy, 'cooling', ex=100000) #建立一个key为当前代理IP的键,表示此IP已经进入冷却,并用ex设定冷却时间
        for proxy in proxies:
            if not rconn.get(proxy): #排除在冷却时间内的IP
                res=rconn.set('proxy',proxy)
                return res
        
        return None

    但此法不久后在实践中遇到了一个问题:某次一个干净的代理被设置成冷却中了;后发现,因为任务是并发的,当1号请求返回异常并更改了代理后,使用了前代理的2号请求才返回出异常,于是又触发了一次更换请求,相当于短时间内连续更换了两次代理,造成了资源的浪费。

    # 版本2
    
    def getproxy():
        proxy = rconn.get('proxy').decode()
        return {'http':proxy, 'https':proxy}
    
    def changeproxy():
        proxies = rconn.lrange('proxylist', 0, 5)
        current_proxy = getproxy().get('http')
        '''
        检测是否有保护标志
        '''
        if rconn.get('protect_time'): 
            return True
        rconn.set(current_proxy, 'cooling', ex=100000) #建立一个key为当前代理IP的键,表示此IP已经进入冷却,并用ex设定冷却时间
        for proxy in proxies:
            if not rconn.get(proxy): #排除在冷却时间内的IP
                res=rconn.set('proxy',proxy)
                '''
                在成功更换代理后,放置一个有效期为30s的保护标志,该标志存在期间禁止代理更换。这个有效期理论上最短要设置为一次请求报文的往返时间
                '''
                rconn.set('protect_time', 'yes', ex=30)
                return res
        
        return None

    这种方法能避免有效代理被跳过,但如果代理池里不小心混入了脏代理,且被更换到了,那在这30s的保护时间内,脏代理也会被“保护”,即使时间不长,我们也要想办法避免。笔者想了很久,有没有在客户端不传任何参数的情况下解决这一点,包括错误计数器,进程标识,保护时间削减等等,但最后发现,唯一可靠的还是客户端传当前代理给代理服务器。

    # 版本3(最终)
    
    def getproxy():
        proxy = rconn.get('proxy').decode()
        return {'http':proxy, 'https':proxy}
    
    def changeproxy(local_proxy):
        # local_proxy是客户端发起请求并被返回异常时所用的代理IP
        proxies = rconn.lrange('proxylist', 0, 5)
        current_proxy = getproxy().get('http')
        if local_proxy!=current_proxy:
            return True
        else:
            rconn.set(current_proxy, 'cooling', ex=100000) #建立一个key为当前代理IP的键,表示此IP已经进入冷却,并用ex设定冷却时间
            for proxy in proxies:
                if not rconn.get(proxy): #排除在冷却时间内的IP
                    res=rconn.set('proxy',proxy)
                    return res
            
            return None

    可以看到,最终版本不仅更加简洁,也解决了上述提到的问题。

    ------

    如果要细究,最终版也是有漏洞的,因为整个更换代理的操作并不具备原子性,依旧可能造成代理被跳过(虽然概率极其微小)。而redis又没有真正的事务,所以最好为changeproxy()再加一把锁,或者对最终版做一处微小的修改:

        else:
            flag = rconn.set(current_proxy, 'cooling', ex=100000, nx=True) #nx参数令存在该键时就不建立,并返回false
            if not flag:
                return True
            for proxy in proxies:
                if not rconn.get(proxy): 
                    res=rconn.set('proxy',proxy)
                    return res
  • 相关阅读:
    sql server 镜像操作
    微信测试公众号的创见以及菜单创建
    linux安装redis步骤
    Mysql 查询表字段数量
    linux 链接mysql并覆盖数据
    linux (centos)增删改查用户命令
    CentOS修改用户密码方法
    https原理及其中所包含的对称加密、非对称加密、数字证书、数字签名
    com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. 问题解决方法
    设计模式(三):模板方法模式
  • 原文地址:https://www.cnblogs.com/qjfoidnh/p/12152945.html
Copyright © 2011-2022 走看看