基于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
posted @ 2020-01-05 17:09  qjfoidnh  阅读(1034)  评论(0编辑  收藏  举报