源码网商城,靠谱的源码在线交易网站 我的订单 购物车 帮助

源码网商城

Redis教程之代理ip池设计方法详解

  • 时间:2021-09-27 09:48 编辑: 来源: 阅读:
  • 扫一扫,手机访问
摘要:Redis教程之代理ip池设计方法详解
[b]前言[/b] 众所周知代理 ip 因为配置简单而且廉价,经常用来作为反反爬虫的手段,但是稳定性一直是其诟病。筛选出优质的代理 ip 并不简单,即使付费购买的代理 ip 源,卖家也不敢保证 100% 可用;另外代理 ip 的生命周期也无法预知,可能上一秒能用,下一秒就扑街了。基于这些原因,会给使用代理 ip 的爬虫程序带来很多不稳定的因素。要排除代理 ip 的影响,通常的做法是建一个代理 ip 池,每次请求前来池子取一个 ip,用完之后归还,保证池子里的 ip 都是可用的。本文接下来就探讨一下,如何使用 Redis 构建代理 ip 池,实现自动更新,自动择优。 [b]整体流程[/b] [img]http://img.1sucai.cn/uploads/article/2018010710/20180107100145_0_69024.jpg[/img] 由上图所示,左侧是形成了整个流程的闭环,从爬虫程序以独占的方式拿到一个代理 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 自己的频道池获取。下面就详细说说总池子和频道池子。 [b]总代理 ip 池[/b] 总池子的作用就是共享所有可用的 ip,但是仅作为存储 ip 的池子并不能实现自动择优啊,这里的择优通常是希望延迟低速度快的 ip 更容易被筛选出,所以我们希望池子中的 ip 是根据它们的延时升序排列,借助 Redis 的 [code]Sorted Sets [/code]数据结构即可实现,用延时表示 score,ip 表示 member。 使用 [code]ZADD [/code]添加新 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
使用 [code]ZRANGE [/code]获取 ip,可以指定获取的个数,比如取两个:
> ZRANGE proxy_global_ips 0 1 WITHSCORES
1) "2.2.2.2:80" 
2) "100" 
3) "1.1.1.1:8080" 
4) "200" 
[b]频道 ip 池[/b] 频道 ip 池的作用是为了最大化使用总池子中的 ip,并且隔离其他频道的 ip 池。由于一个 ip 使用次数过多是有很大的概率被目标网站屏蔽掉,所以这里也需要进行择优,应该优先筛选出使用次数少的 ip,同理也是使用 [code]Sorted Sets[/code],使用次数表示 score,ip 表示 member,这里与总池子明显的不同之处是 key 不是固定的,需要把频道名称组合进去,这样保证频道之间的隔离,如频道 abc 的 key:[code]proxy_channel_abc_ips[/code]。 由于频道池子中的 ip 是要以独占的方式取出,我们需要一个 [code]ZPOP [/code]的方法,奈何 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
这里与总池子不同的是多了一个 [code]INCR [/code]选项,这是 Redis 3.0.2 版本后才支持的新特性,即指定在 ZADD 时发生 member 冲突采取的处理方式,[code]INCR [/code]顾名思义是冲突后累加 score 的方式,为什么要用这个选项,看看下面这个流程: [list=1] [*]在频道池子中只有 1.1.1.1,使用次数为 10;总池子也有 1.1.1.1,而且排在第一个[/*] [*]线程 A 取出 1.1.1.1[/*] [*]线程 B 从频道池子取 ip,没取到,从总池子补充 ip 到频道池子:[code]ZADD proxy_channel_abc_ips 0 1.1.1.1[/code];取出 1.1.1.1[/*] [*]线程 A 归还 1.1.1.1:[code]ZADD proxy_channel_abc_ips 11 1.1.1.1[/code][/*] [*]线程 B 归还 1.1.1.1:[code]ZADD proxy_channel_abc_ips 1 1.1.1.1[/code][/*] [/list] 第 5 步结束后,ip 1.1.1.1 的计数被错误地重置为 1,而不是我们预期的 12。使用 [code]INCR [/code]选项就可以避免这个尴尬,其实这也只能保证最终计数正确,中途还是会有些非预期的情况,如: [list=1] [*]在频道池子中有 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 到频道池子:[code]ZADD proxy_channel_abc_ips 0 1.1.1.1[/code];取出 1.1.1.1[/*] [*]线程 C 归还 1.1.1.1:[code]ZADD proxy_channel_abc_ips INCR 1 1.1.1.1[/code][/*] [*]线程 B 归还 2.2.2.2:[code]ZADD proxy_channel_abc_ips INCR 3 2.2.2.2[/code][/*] [*]线程 D 来池子取 ip,按使用次数少的被分配了 1.1.1.1,这就不是我们期望的,1.1.1.1 实际已经用了 12 次,我们更希望 2.2.2.2 被取出[/*] [/list] 如果要避免这个问题,一个简单粗暴的办法就是增加频道池子的容量,让 ip 数永远大于并发的线程数。 [b]更新[/b] 与 ip 有关的两个属性:延时(爬取页面所花的时间)和使用次数。上面只讲到了根据它们自动择优,这里的就来说下它们是如何更新的。延时和使用次数的更新需要爬虫程序的配合,程序中要记录时间和递增使用次数,在归还 ip 时要将最新值带回给总池子和频道池子。上面频道 ip 池的例子也有提及,每次归还 ip 都要将最新的使用次数带上,其次还要将 ip 的延时更新到总池子里面。如果归还 ip 时出现使用失败的情况,就要将该 ip 从总池子里删除掉,保证该 ip 不会再被使用,至于当前的频道池不用归还就行了。其他频道池不作任何处理,因为 ip 在当前频道不可用,一般都是因为被屏蔽,其他频道依然可以使用,即使确实都不能使用,也会在其他频道归还 ip 时被删除。 这两个属性其实也可以都在 Redis 中更新,在获取 ip 时,使用 [code]Hashs [/code]保存 ip 对应的获取时间和使用次数;在归还时从 [code]Hashs [/code]中取出时间计算出延时,取出使用次数并加 1,再分别更新到总池子和频道池子中。而且这还能避免上面提到的获取 ip 不符合预期的问题。 [b]总结[/b] 放在 Redis 中更新的方法也有弊端,延时会包含获取和归还的传输时间,如果爬虫程序获取一个 ip 多次使用,会造成使用次数统计偏少。当然也可以通过在程序中多次调用 Redis 更新 ip 的属性来解决,这样增加了整个流程的复杂性,需要自己权衡。 个人还是倾向在程序中记录,最后更新到 Redis 中。这个方案逻辑确实不够严谨,但是出现问题也不会导致严重后果。程序的健壮性也不是不允许出现 bug,而是出现 bug 有很好的容错性。 以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
  • 全部评论(0)
联系客服
客服电话:
400-000-3129
微信版

扫一扫进微信版
返回顶部