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

 更新时间:2017年01月20日 10:21:00   投稿:daisy  
这篇文章主要介绍了Redis实现代理ip池的设计方法,文中给出了详细的介绍与示例代码,相信对大家的理解和学习具有一定的参考借鉴价值,有需要的朋友们下面来一起看看吧。

前言

众所周知代理 ip 因为配置简单而且廉价,经常用来作为反反爬虫的手段,但是稳定性一直是其诟病。筛选出优质的代理 ip 并不简单,即使付费购买的代理 ip 源,卖家也不敢保证 100% 可用;另外代理 ip 的生命周期也无法预知,可能上一秒能用,下一秒就扑街了。基于这些原因,会给使用代理 ip 的爬虫程序带来很多不稳定的因素。要排除代理 ip 的影响,通常的做法是建一个代理 ip 池,每次请求前来池子取一个 ip,用完之后归还,保证池子里的 ip 都是可用的。本文接下来就探讨一下,如何使用 Redis 构建代理 ip 池,实现自动更新,自动择优。

整体流程

由上图所示,左侧是形成了整个流程的闭环,从爬虫程序以独占的方式拿到一个代理 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.1,使用次数为 10;总池子也有 1.1.1.1,而且排在第一个
  2. 线程 A 取出 1.1.1.1
  3. 线程 B 从频道池子取 ip,没取到,从总池子补充 ip 到频道池子:ZADD proxy_channel_abc_ips 0 1.1.1.1;取出 1.1.1.1
  4. 线程 A 归还 1.1.1.1:ZADD proxy_channel_abc_ips 11 1.1.1.1
  5. 线程 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.1,使用次数为 10,还有 2.2.2.2,使用次数为 2;总池子也有 1.1.1.1,而且排在第一个
  2. 线程 A 取出 1.1.1.1
  3. 线程 B 取出 2.2.2.2
  4. 线程 C 从频道池子取 ip,没取到,从总池子补充 ip 到频道池子:ZADD proxy_channel_abc_ips 0 1.1.1.1;取出 1.1.1.1
  5. 线程 C 归还 1.1.1.1:ZADD proxy_channel_abc_ips INCR 1 1.1.1.1
  6. 线程 B 归还 2.2.2.2:ZADD proxy_channel_abc_ips INCR 3 2.2.2.2
  7. 线程 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启动,停止,及端口占用处理方法

    redis启动,停止,及端口占用处理方法

    今天小编就为大家分享一篇redis启动,停止,及端口占用处理方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • Python交互Redis的实现

    Python交互Redis的实现

    本文主要介绍了Python交互Redis的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Redis RDB技术底层原理详解

    Redis RDB技术底层原理详解

    为了使Redis在重启之后仍能保证数据不丢失,需要将数据从内存中以某种形式同步到硬盘中,这一过程就是持久化,本文重点给大家介绍Redis RDB技术底层原理实现方法,一起看看吧
    2021-09-09
  • Redis集群的三种部署方式及三种应用问题的处理

    Redis集群的三种部署方式及三种应用问题的处理

    这篇文章主要介绍了Redis集群的三种部署方式及三种应用问题的处理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04
  • Redis在项目中常见的12种使用场景示例和说明

    Redis在项目中常见的12种使用场景示例和说明

    Redis是一个开源的高性能键值对数据库,它以其内存中数据存储、键过期策略、持久化、事务、丰富的数据类型支持以及原子操作等特性,在许多项目中扮演着关键角色,以下是整理的12个Redis在项目中常见的使用场景举例说明和解释
    2024-06-06
  • 基于redis实现token验证用户是否登陆

    基于redis实现token验证用户是否登陆

    这篇文章主要为大家详细介绍了基于redis实现token验证用户是否登陆,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-08-08
  • Redis为什么选择单线程?Redis为什么这么快?

    Redis为什么选择单线程?Redis为什么这么快?

    这篇文章主要介绍了Redis为什么选择单线程?Redis为什么这么快?的相关资料,需要的朋友可以参考下
    2023-03-03
  • Redis使用ZSET实现消息队列的项目实践

    Redis使用ZSET实现消息队列的项目实践

    本文主要介绍了Redis使用ZSET实现消息队列的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Windows下安装Redis的流程详解

    Windows下安装Redis的流程详解

    Redis作为常用开源的非关系型数据库,是开发中常用的数据库之一,很多朋友不清楚Windows下安装Redis的过程,今天小编通过分享本文给大家介绍详细过程,一起看看吧
    2021-08-08
  • Redis和Nginx实现限制接口请求频率的示例

    Redis和Nginx实现限制接口请求频率的示例

    限流就是限制API访问频率,当访问频率超过某个阈值时进行拒绝访问等操作,本文主要介绍了Redis和Nginx实现限制接口请求频率的示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02

最新评论