您的位置  > 互联网

Redis实现代理ip池的设计方法与借鉴价值介绍

本文主要介绍Redis实现代理IP池的设计方法。 文章给出了详细的介绍和示例代码。 相信对于大家的理解和学习有一定的参考价值。 有需要的朋友可以看看下面。 酒吧。

Redis是一个用ANSI C语言编写的开源日志型Key-Value数据库,支持网络,可以基于内存且持久化,提供多种语言的API。

前言

众所周知,代理IP因其配置简单、成本低廉而常被用作反反爬虫的方法。 但其稳定性却一直饱受诟病。 选择高质量的代理IP并不容易。 即使代理IP源是付费购买的,卖家也不能保证100%可用。 另外,代理IP的生命周期是不可预测的。 可能这一秒还可用,但下一秒就会被使用。 。 由于这些原因,会给使用代理IP的爬虫程序带来很多不稳定的因素。 为了消除代理IP的影响,通常的做法是建立一个代理IP池,每次请求都到池中获取一个IP,使用后返回,以保证池中所有IP可用。 接下来,本文将讨论如何使用Redis构建代理IP池,实现自动更新和自动选择。

整体流程

如上图所示,左侧从爬虫程序独占获取代理IP到爬取完成返回IP,形成了整个流程的闭环。 其实这个过程并不是很严谨。 如果爬虫程序异常中断,则无法返回IP,也无法回收IP。 但由于代理IP本身的特点,数量较大,回收价值不大,既然如此,就放了吧。

上面也提到了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

  1. Sorted Sets

数据结构可以实现,用delay来表示score,用ip来表示。

使用

  1. ZADD

添加新 IP 或更新 IP 延迟:

  1.  
  2. > ZADD proxy_global_ips 200 1.1.1.1:8080 100 2.2.2.2:80 300 3.3.3.3:8888
  3. (integer) 3

使用

  1. ZRANGE

要获取IP地址,可以指定获取的数量,例如2:

  1.  
  2. > ZRANGE proxy_global_ips 0 1 WITHSCORES
  3. 1) "2.2.2.2:80"
  4. 2) "100"
  5. 3) "1.1.1.1:8080"
  6. 4) "200"

频道IP池

频道IP池的作用是最大限度地利用总池中的IP,并隔离其他频道IP池。由于使用次数过多的IP很有可能被目标网站屏蔽,因此也是有必要的。在这里选择最好的。 应优先考虑使用频率较低的 IP。 使用时也是如此

  1. Sorted Sets

,使用次数代表score,ip代表。 这里与总池的明显区别是密钥不固定。 需要将通道名称组合起来,保证通道之间的隔离性,比如通道abc的key:

  1. proxy_channel_abc_ips

由于通道池中的IP是要独占取出来的,所以我们需要一个

  1. ZPOP

然而Redis本身并没有这个方法。 幸运的是,可以通过Lua来模拟,原子操作中可以取出ip,然后删除:

  1.  
  2. > 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池:

  1.  
  2. > ZADD proxy_channel_abc_ips INCR 0 1.1.1.1:8080

这个地方和主池不同的是,多了一个

  1. INCR

,这是Redis 3.0.2版本之后才支持的新特性,指定了ZADD发生时冲突的处理方式。

  1. INCR

顾名思义,这是冲突后积累分数的一种方式。 为什么使用这个选项? 看看下面的流程:

通道池中只有1.1.1.1,使用次数为10; 总池中还有1.1.1.1,排在第一位。 线程 A 取出 1.1.1.1。 线程B从通道池获取IP。 如果没有得到,它会从总池中添加 IP。 到通道池:

  1. ZADD proxy_channel_abc_ips 0 1.1.1.1

;取出1.1.1.1线程A,返回1.1.1.1:

  1. ZADD proxy_channel_abc_ips 11 1.1.1.1

线程 B 返回 1.1.1.1:

  1. ZADD proxy_channel_abc_ips 1 1.1.1.1

在第 5 步之后,ip 1.1.1.1 的计数错误地重置为 1,而不是我们预期的 12。use

  1. 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 添加到通道池中:

  1. ZADD proxy_channel_abc_ips 0 1.1.1.1

;取出1.1.1.1线程C并返回1.1.1.1:

  1. ZADD proxy_channel_abc_ips INCR 1 1.1.1.1

线程 B 返回 2.2.2.2:

  1. 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时,使用

  1. Hashs

保存IP对应的获取时间和使用次数; 退货时,从

  1. Hashs

通过取出时间、取出使用次数加1来计算延迟,然后分别更新到总池和通道池中。 而这也可以避免上述获取IP不符合预期的问题。

总结

Redis 中的更新方法也有缺点。 延迟将包括获取和返回的传输时间。 如果爬虫程序获取某个IP并多次使用,则使用统计次数会偏低。 当然,也可以通过在程序中多次调用Redis更新IP属性来解决。 这增加了整个过程的复杂性,需要自己权衡。

就我个人而言,我还是更喜欢记录在程序中,最后更新到Redis中。 这个方案的逻辑确实不够严谨,但出现问题不会导致严重后果。 程序的健壮性并不是指不允许出现Bug,而是指在出现Bug时具有良好的容错能力。