首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

用Flask+Aiohttp+Redis维护动态代理池

在网上有大量公开的免费代理,或者我们也可以购买付费的代理IP,但是代理不论是免费的还是付费的,都不能保证都是可用的,因为可能此IP被其他人使用来爬取同样的目标站点而被封禁,或者代理服务器突然发生故障或网络繁忙。一旦我们选用了一个不可用的代理,这势必会影响爬虫的工作效率。

所以,我们需要提前做筛选,将不可用的代理剔除掉,保留可用代理。接下来我们就搭建一个高效易用的代理池。

一、准备工作

首先需要成功安装Redis数据库并启动服务,另外还需要安装aiohttp、requests、RedisPy、pyquery、Flask库。

二、代理池的目标

我们需要做到下面的几个目标,来实现易用高效的代理池。

基本模块分为4块:存储模块、获取模块、检测模块、接口模块。

存储模块负责存储抓取下来的代理。首先要保证代理不重复,要标识代理的可用情况,还要动态实时处理每个代理,所以一种比较高效和方便的存储方式就是使用Redis的Sorted Set,即有序集合。

获取模块需要定时在各大代理网站抓取代理。代理可以是免费公开代理也可以是付费代理,代理的形式都是IP加端口,此模块尽量从不同来源获取,尽量抓取高匿代理,抓取成功之后将可用代理保存到数据库中。

检测模块需要定时检测数据库中的代理。这里需要设置一个检测链接,最好是爬取哪个网站就检测哪个网站,这样更加有针对性,如果要做一个通用型的代理,那可以设置百度等链接来检测。另外,我们需要标识每一个代理的状态,如设置分数标识,100分代表可用,分数越少代表越不可用。检测一次,如果代理可用,我们可以将分数标识立即设置为100满分,也可以在原基础上加1分;如果代理不可用,可以将分数标识减1分,当分数减到一定阈值后,代理就直接从数据库移除。通过这样的标识分数,我们就可以辨别代理的可用情况,选用的时候会更有针对性。

接口模块需要用API来提供对外服务的接口。其实我们可以直接连接数据库来取对应的数据,但是这样就需要知道数据库的连接信息,并且要配置连接,而比较安全和方便的方式就是提供一个Web API接口,我们通过访问接口即可拿到可用代理。另外,由于可用代理可能有多个,那么我们可以设置一个随机返回某个可用代理的接口,这样就能保证每个可用代理都可以取到,实现负载均衡。

以上内容是设计代理的一些基本思路。接下来我们设计整体的架构,然后用代码实现代理池。

三、代理池的架构

根据上文的描述,代理池的架构可以如下图所示。

代理池分为4个模块:存储模块、获取模块、检测模块、接口模块。

存储模块使用Redis的有序集合,用来做代理的去重和状态标识,同时它也是中心模块和基础模块,将其他模块串联起来。

获取模块定时从代理网站获取代理,将获取的代理传递给存储模块,并保存到数据库。

检测模块定时通过存储模块获取所有代理,并对代理进行检测,根据不同的检测结果对代理设置不同的标识。

接口模块通过Web API提供服务接口,接口通过连接数据库并通过Web形式返回可用的代理。

四、代理池的实现

接下来,我们用代码分别实现这4个模块。

1. 存储模块

这里我们使用Redis的有序集合,集合的每一个元素都是不重复的,对于代理池来说,集合的元素就变成了一个个代理,也就是IP加端口的形式,如60.207.237.111:8888,这样的一个代理就是集合的一个元素。另外,有序集合的每一个元素都有一个分数字段,分数是可以重复的,可以是浮点数类型,也可以是整数类型。该集合会根据每一个元素的分数对集合进行排序,数值小的排在前面,数值大的排在后面,这样就可以实现集合元素的排序了。

对于代理池来说,这个分数可以作为判断一个代理是否可用的标志,100为最高分,代表最可用,0为最低分,代表最不可用。如果要获取可用代理,可以从代理池中随机获取分数最高的代理,注意是随机,这样可以保证每个可用代理都会被调用到。

分数是我们判断代理稳定性的重要标准,设置分数规则如下所示。

分数100为可用,检测器会定时循环检测每个代理可用情况,一旦检测到有可用的代理就立即置为100,检测到不可用就将分数减1,分数减至0后代理移除。

新获取的代理的分数为10,如果测试可行,分数立即置为100,不可行则分数减1,分数减至0后代理移除。

这只是一种解决方案,当然可能还有更合理的方案。之所以设置此方案有如下几个原因。

在检测到代理可用时,分数立即置为100,这样可以保证所有可用代理有更大的机会被获取到。你可能会问,为什么不将分数加1而是直接设为最高100呢?设想一下,有的代理是从各大免费公开代理网站获取的,常常一个代理并没有那么稳定,平均五次请求可能有两次成功,三次失败,如果按照这种方式来设置分数,那么这个代理几乎不可能达到一个高的分数,也就是说即便它有时是可用的,但是筛选的分数最高,那这样的代理几乎不可能被取到。如果想追求代理稳定性,可以用上述方法,这种方法可确保分数最高的代理一定是最稳定可用的。所以,这里我们采取“可用即设置100”的方法,确保只要可用的代理都可以被获取到。

在检测到代理不可用时,分数减1,分数减至0后,代理移除。这样一个有效代理如果要被移除需要失败100次,也就是说当一个可用代理如果尝试了100次都失败了,就一直减分直到移除,一旦成功就重新置回100。尝试机会越多,则这个代理拯救回来的机会越多,这样就不容易将曾经的一个可用代理丢弃,因为代理不可用的原因很可能是网络繁忙或者其他人用此代理请求太过频繁,所以在这里将分数为100。

新获取的代理的分数设置为10,代理如果不可用,分数就减1,分数减到0,代理就移除,如果代理可用,分数就置为100。由于很多代理是从免费网站获取的,所以新获取的代理无效的比例非常高,可能不足10%。所以在这里我们将分数设置为10,检测的机会没有可用代理的100次那么多,这也可以适当减少开销。

上述代理分数的设置思路不一定是最优思路,但据个人实测,它的实用性还是比较强的。

现在我们需要定义一个类来操作数据库的有序集合,定义一些方法来实现分数的设置、代理的获取等。代码实现如下所示:

首先我们定义了一些常量,如、、分别代表最大分数、最小分数、初始分数。、、分别代表了Redis的连接信息,即地址、端口、密码。是有序集合的键名,我们可以通过它来获取代理存储所使用的有序集合。

接下来定义了一个类,这个类可以用来操作Redis的有序集合,其中定义了一些方法来对集合中的元素进行处理,它的主要功能如下所示。

方法是初始化的方法,其参数是Redis的连接信息,默认的连接信息已经定义为常量,在方法中初始化了一个的类,建立Redis连接。

方法向数据库添加代理并设置分数,默认的分数是,也就是10,返回结果是添加的结果。

方法是随机获取代理的方法,首先获取100分的代理,然后随机选择一个返回。如果不存在100分的代理,则此方法按照排名来获取,选取前100名,然后随机选择一个返回,否则抛出异常。

方法是在代理检测无效的时候设置分数减1的方法,代理传入后,此方法将代理的分数减1,如果分数达到最低值,那么代理就删除。

方法可判断代理是否存在集合中。

方法将代理的分数设置为,即100,也就是当代理有效时的设置。

方法返回当前集合的元素个数。

方法返回所有的代理列表,以供检测使用。

定义好了这些方法,我们可以在后续的模块中调用此类来连接和操作数据库。如想要获取随机可用的代理,只需要调用方法即可,得到的就是随机的可用代理。

2. 获取模块

获取模块的逻辑相对简单,首先要定义一个Crawler来从各大网站抓取代理,示例如下所示:

方便起见,我们将获取代理的每个方法统一定义为以开头,这样扩展的时候只需要添加开头的方法即可。

在这里实现了几个示例,如抓取代理66、Proxy360、Goubanjia三个免费代理网站,这些方法都定义成了生成器,通过返回一个个代理。程序首先获取网页,然后用解析,解析出IP加端口的形式的代理然后返回。

然后定义了一个方法,将所有以开头的方法调用一遍,获取每个方法返回的代理并组合成列表形式返回。

你可能会想知道,如何获取所有以开头的方法名称呢?其实这里借助了元类来实现。我们定义了一个,类将它设置为元类,元类中实现了方法,这个方法有固定的几个参数,第四个参数中包含了类的一些属性。我们可以遍历这个参数即可获取类的所有方法信息,就像遍历字典一样,键名对应方法的名称。然后判断方法的开头是否,如果是,则将其加入到属性中。这样我们就成功将所有以开头的方法定义成了一个属性,动态获取到所有以开头的方法列表。

所以,如果要做扩展,我们只需要添加一个以开头的方法。例如抓取快代理,我们只需要在类中增加方法,仿照其他几个方法将其定义成生成器,抓取其网站的代理,然后通过返回代理即可。这样,我们可以非常方便地扩展,而不用关心类其他部分的实现逻辑。

代理网站的添加非常灵活,不仅可以添加免费代理,也可以添加付费代理。一些付费代理的提取方式也类似,也是通过Web的形式获取,然后进行解析。解析方式可能更加简单,如解析纯文本或JSON,解析之后以同样的形式返回即可,在此不再代码实现,可以自行扩展。

既然定义了类,接下来再定义一个类,用来动态地调用所有以开头的方法,然后获取抓取到的代理,将其加入到数据库存储起来。

类就是获取器类,它定义了一个变量来表示代理池的最大数量,这个数量可以灵活配置,然后定义了方法来判断代理池是否已经达到了容量阈值。方法调用了方法来获取代理的数量,然后进行判断,如果数量达到阈值,则返回,否则返回。如果不想加这个限制,可以将此方法永久返回。

接下来定义方法。该方法首先判断了代理池是否达到阈值,然后在这里就调用了类的属性,获取到所有开头的方法列表,依次通过方法调用,得到各个方法抓取到的代理,然后再利用的方法加入数据库,这样获取模块的工作就完成了。

3. 检测模块

我们已经成功将各个网站的代理获取下来了,现在就需要一个检测模块来对所有代理进行多轮检测。代理检测可用,分数就设置为100,代理不可用,分数减1,这样就可以实时改变每个代理的可用情况。如要获取有效代理只需要获取分数高的代理即可。

由于代理的数量非常多,为了提高代理的检测效率,我们在这里使用异步请求库aiohttp来进行检测。

requests作为一个同步请求库,我们在发出一个请求之后,程序需要等待网页加载完成之后才能继续执行。也就是这个过程会阻塞等待响应,如果服务器响应非常慢,比如一个请求等待十几秒,那么我们使用requests完成一个请求就会需要十几秒的时间,程序也不会继续往下执行,而在这十几秒的时间里程序其实完全可以去做其他的事情,比如调度其他的请求或者进行网页解析等。

异步请求库就解决了这个问题,它类似JavaScript中的回调,即在请求发出之后,程序可以继续执行去做其他的事情,当响应到达时,程序再去处理这个响应。于是,程序就没有被阻塞,可以充分利用时间和资源,大大提高效率。

对于响应速度比较快的网站来说,requests同步请求和aiohttp异步请求的效果差距没那么大。可对于检测代理来说,检测一个代理一般需要十多秒甚至几十秒的时间,这时候使用aiohttp异步请求库的优势就大大体现出来了,效率可能会提高几十倍不止。

所以,我们的代理检测使用异步请求库aiohttp,实现示例如下所示:

这里定义了一个类,方法中建立了一个对象,供该对象中其他方法使用。接下来定义了一个方法,这个方法用来检测单个代理的可用情况,其参数就是被检测的代理。注意,方法前面加了关键词,这代表这个方法是异步的。方法内部首先创建了aiohttp的对象,此对象类似于requests的对象,可以直接调用该对象的get()方法来访问页面。在这里,代理的设置是通过参数传递给方法,请求方法前面也需要加上关键词来标明其是异步请求,这也是aiohttp使用时的常见写法。

测试的链接在这里定义为常量。如果针对某个网站有抓取需求,建议将设置为目标网站的地址,因为在抓取的过程中,代理本身可能是可用的,但是该代理的IP已经被目标网站封掉了。例如,某些代理可以正常访问百度等页面,但是对知乎来说可能就被封了,所以我们可以将设置为知乎的某个页面的链接,当请求失败、代理被封时,分数自然会减下来,失效的代理就不会被取到了。

如果想做一个通用的代理池,则不需要专门设置,可以将其设置为一个不会封IP的网站,也可以设置为百度这类响应稳定的网站。

我们还定义了变量,这个变量是一个列表形式,包含了正常的状态码,如可以定义成[200]。当然某些目标网站可能会出现其他的状态码,可以自行配置。

程序在获取Response后需要判断响应的状态,如果状态码在列表里,则代表代理可用,可以调用的方法将代理分数设为100,否则调用方法将代理分数减1,如果出现异常也同样将代理分数减1。

另外,我们设置了批量测试的最大值为100,也就是一批测试最多100个,这可以避免代理池过大时一次性测试全部代理导致内存开销过大的问题。

这样,测试模块的逻辑就完成了。

4. 接口模块

通过上述三个模块,我们已经可以做到代理的获取、检测和更新,数据库就会以有序集合的形式存储各个代理及其对应的分数,分数100代表可用,分数越小代表越不可用。

但是我们怎样方便地获取可用代理呢?可以用类直接连接Redis,然后调用方法。这样做没问题,效率很高,但是会有几个弊端。

如果其他人使用这个代理池,他需要知道Redis连接的用户名和密码信息,这样很不安全。

如果代理池需要部署在远程服务器上运行,而远程服务器的Redis只允许本地连接,那么我们就不能远程直连Redis来获取代理。

如果爬虫所在的主机没有连接Redis模块,或者爬虫不是由Python语言编写的,那么我们就无法使用来获取代理。

如果类或者数据库结构有更新,那么爬虫端必须同步这些更新,这样非常麻烦。

综上考虑,为了使代理池可以作为一个独立服务运行,我们最好增加一个接口模块,并以Web API的形式暴露可用代理。

这样一来,获取代理只需要请求接口即可,以上的几个缺点弊端也可以避免。

我们使用一个比较轻量级的库Flask来实现这个接口模块,实现示例如下所示:

在这里,我们声明了一个对象,定义了三个接口,分别是首页、随机代理页、获取数量页。

运行之后,会启动一个Web服务,我们只需要访问对应的接口即可获取到可用代理。

5. 调度模块

调度模块就是调用以上所定义的三个模块,将这三个模块通过多进程的形式运行起来,示例如下所示:

三个常量、、都是布尔类型,表示测试模块、获取模块、接口模块的开关,如果都为,则代表模块开启。

启动入口是方法,这个方法分别判断三个模块的开关。如果开关开启,启动时程序就新建一个Process进程,设置好启动目标,然后调用方法运行,这样三个进程就可以并行执行,互不干扰。

三个调度方法结构也非常清晰。比如,方法用来调度测试模块,首先声明一个对象,然后进入死循环不断循环调用其方法,执行完一轮之后就休眠一段时间,休眠结束之后重新再执行。在这里,休眠时间也定义为一个常量,如20秒,即每隔20秒进行一次代理检测。

最后,只需要调用Scheduler的方法即可启动整个代理池。

以上内容便是整个代理池的架构和相应实现逻辑。

五、运行

接下来,我们将代码整合一下,将代理运行起来,运行之后的输出结果如下图所示。

以上是代理池的控制台输出,可以看到,可用代理设置为100,不可用代理分数减1。

我们再打开浏览器,当前配置了运行在5555端口,所以打开http://127.0.0.1:5555,即可看到其首页,如下图所示。

再访问:http://127.0.0.1:5555/random,即可获取随机可用代理,如下图所示。

我们只需要访问此接口即可获取一个随机可用代理,这非常方便。

获取代理的代码如下所示:

之后便是一个字符串类型的代理,此代理可以按照上一节所示的方法设置,如requests的使用方法如下所示:

有了代理池之后,我们再取出代理即可有效防止IP被封禁的情况。

六、本节代码

本节代码地址为:https://github.com/Python3WebSpider/ProxyPool。

七、结语

本节实现了一个比较高效的代理池,来获取随机可用的代理。接下来,我们会利用代理池来实现数据的抓取。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180330G1BDCO00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券