基于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