前言
众所周知代理 ip 因为配置简单而且廉价,经常用来作为反反爬虫的手段,但是稳定性一直是其诟病。筛选出优质的代理 ip 并不简单,即使付费购买的代理 ip 源,卖家也不敢保证 100% 可用;另外代理 ip 的生命周期也无法预知,可能上一秒能用,下一秒就扑街了。基于这些原因,会给使用代理 ip 的爬虫程序带来很多不稳定的因素。要排除代理 ip 的影响,通常的做法是建一个代理 ip 池,每次请求前来池子取一个 ip,用完之后归还,保证池子里的 ip 都是可用的。本文接下来就探讨一下,如何使用 Redis 构建代理 ip 池,实现自动更新,自动择优。
整体流程
![](//img.jbzj.com/file_images/article/201701/2017120101927353.png?2017020101956)
由上图所示,左侧是形成了整个流程的闭环,从爬虫程序以独占的方式拿到一个代理 ip 到爬取完成归还 ip。这个流程其实是不太严谨的,如果爬虫程序异常中断,就会导致 ip 无法归还,就会导致这个 ip 无法循环利用。但是由于代理 ip 本身的特点,量多而且循环利用的价值并不大,所以这种情况就let it go。
上面也提到 ip 是以独占的方式获取,如果是去爬两个毫不相关的网站,本来一个 ip 就可以,可现在需要两个。为了资源最大化使用,这里引入了频道 ip 池和总代理 ip 池。两个网站就当做两个频道,各自独占,互不相关;总池子就是保存所有的 ip,每个频道都共享。假设只有一个 ip:1.1.1.1 在总池子,爬 A 网站会把它从总池子取到 A 频道的 ip 池,然后 A 爬虫程序从 A 频道 ip 池取出 1.1.1.1 进行使用,这时 1.1.1.1 依然在总池子里,但 A 频道的 ip 池已经不包含 1.1.1.1 了;爬 B 网站也是一样的流程拿到 1.1.1.1,只是从 B 自己的频道池获取。下面就详细说说总池子和频道池子。
总代理 ip 池
总池子的作用就是共享所有可用的 ip,但是仅作为存储 ip 的池子并不能实现自动择优啊,这里的择优通常是希望延迟低速度快的 ip 更容易被筛选出,所以我们希望池子中的 ip 是根据它们的延时升序排列,借助 Redis 的 Sorted Sets
数据结构即可实现,用延时表示 score,ip 表示 member。
使用 ZADD
添加新 ip 或更新 ip 的延迟:
> ZADD proxy_global_ips 200 1.1.1.1:8080 100 2.2.2.2:80 300 3.3.3.3:8888
(integer) 3
使用 ZRANGE
获取 ip,可以指定获取的个数,比如取两个:
> ZRANGE proxy_global_ips 0 1 WITHSCORES
1) "2.2.2.2:80"
2) "100"
3) "1.1.1.1:8080"
4) "200"
频道 ip 池
频道 ip 池的作用是为了最大化使用总池子中的 ip,并且隔离其他频道的 ip 池。由于一个 ip 使用次数过多是有很大的概率被目标网站屏蔽掉,所以这里也需要进行择优,应该优先筛选出使用次数少的 ip,同理也是使用 Sorted Sets
,使用次数表示 score,ip 表示 member,这里与总池子明显的不同之处是 key 不是固定的,需要把频道名称组合进去,这样保证频道之间的隔离,如频道 abc 的 key:proxy_channel_abc_ips
。
由于频道池子中的 ip 是要以独占的方式取出,我们需要一个 ZPOP
的方法,奈何 Redis 本身没有,还好可以通过 Lua 模拟,在一个原子操作下取出 ip,然后删除:
> eval "local el = redis.call('zrange', KEYS[1], 0, 0, 'WITHSCORES'); redis.call('zrem', KEYS[1], el[1]); return el;" 1 proxy_channel_abc_ips
往频道 ip 池添加 ip:
> ZADD proxy_channel_abc_ips INCR 0 1.1.1.1:8080
这里与总池子不同的是多了一个 INCR
选项,这是 Redis 3.0.2 版本后才支持的新特性,即指定在 ZADD 时发生 member 冲突采取的处理方式,INCR
顾名思义是冲突后累加 score 的方式,为什么要用这个选项,看看下面这个流程:
- 在频道池子中只有 1.1.1.1,使用次数为 10;总池子也有 1.1.1.1,而且排在第一个
- 线程 A 取出 1.1.1.1
- 线程 B 从频道池子取 ip,没取到,从总池子补充 ip 到频道池子:
ZADD proxy_channel_abc_ips 0 1.1.1.1
;取出 1.1.1.1
- 线程 A 归还 1.1.1.1:
ZADD proxy_channel_abc_ips 11 1.1.1.1
- 线程 B 归还 1.1.1.1:
ZADD proxy_channel_abc_ips 1 1.1.1.1
第 5 步结束后,ip 1.1.1.1 的计数被错误地重置为 1,而不是我们预期的 12。使用 INCR
选项就可以避免这个尴尬,其实这也只能保证最终计数正确,中途还是会有些非预期的情况,如:
- 在频道池子中有 1.1.1.1,使用次数为 10,还有 2.2.2.2,使用次数为 2;总池子也有 1.1.1.1,而且排在第一个
- 线程 A 取出 1.1.1.1
- 线程 B 取出 2.2.2.2
- 线程 C 从频道池子取 ip,没取到,从总池子补充 ip 到频道池子:
ZADD proxy_channel_abc_ips 0 1.1.1.1
;取出 1.1.1.1
- 线程 C 归还 1.1.1.1:
ZADD proxy_channel_abc_ips INCR 1 1.1.1.1
- 线程 B 归还 2.2.2.2:
ZADD proxy_channel_abc_ips INCR 3 2.2.2.2
- 线程 D 来池子取 ip,按使用次数少的被分配了 1.1.1.1,这就不是我们期望的,1.1.1.1 实际已经用了 12 次,我们更希望 2.2.2.2 被取出
如果要避免这个问题,一个简单粗暴的办法就是增加频道池子的容量,让 ip 数永远大于并发的线程数。
更新
与 ip 有关的两个属性:延时(爬取页面所花的时间)和使用次数。上面只讲到了根据它们自动择优,这里的就来说下它们是如何更新的。延时和使用次数的更新需要爬虫程序的配合,程序中要记录时间和递增使用次数,在归还 ip 时要将最新值带回给总池子和频道池子。上面频道 ip 池的例子也有提及,每次归还 ip 都要将最新的使用次数带上,其次还要将 ip 的延时更新到总池子里面。如果归还 ip 时出现使用失败的情况,就要将该 ip 从总池子里删除掉,保证该 ip 不会再被使用,至于当前的频道池不用归还就行了。其他频道池不作任何处理,因为 ip 在当前频道不可用,一般都是因为被屏蔽,其他频道依然可以使用,即使确实都不能使用,也会在其他频道归还 ip 时被删除。
这两个属性其实也可以都在 Redis 中更新,在获取 ip 时,使用 Hashs
保存 ip 对应的获取时间和使用次数;在归还时从 Hashs
中取出时间计算出延时,取出使用次数并加 1,再分别更新到总池子和频道池子中。而且这还能避免上面提到的获取 ip 不符合预期的问题。
总结
放在 Redis 中更新的方法也有弊端,延时会包含获取和归还的传输时间,如果爬虫程序获取一个 ip 多次使用,会造成使用次数统计偏少。当然也可以通过在程序中多次调用 Redis 更新 ip 的属性来解决,这样增加了整个流程的复杂性,需要自己权衡。
个人还是倾向在程序中记录,最后更新到 Redis 中。这个方案逻辑确实不够严谨,但是出现问题也不会导致严重后果。程序的健壮性也不是不允许出现 bug,而是出现 bug 有很好的容错性。
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
您可能感兴趣的文章:- 超强、超详细Redis数据库入门教程
- 30个php操作redis常用方法代码例子
- redis 队列操作的例子(php)
- redis常用命令、常见错误、配置技巧等分享
- 64位Windows下安装Redis教程
- Flyway数据库版本控制的教程详解
- 利用Python爬取可用的代理IP
- 利用TaskManager爬取2万条代理IP实现自动投票功能
- php代码检查代理ip的有效性
- 通过Python爬虫代理IP快速增加博客阅读量