【Python3爬虫】教你怎么利用免费代理搭建代理池
一、写在前面
有时候你的爬虫刚开始的时候可以正常运行,能够正常的爬取数据,但是过了一会,却出现了一个“403 Forbidden",或者是”您的IP访问频率太高“这样的提示,这就意味着你的IP被ban了,好一点的情况是过一段时间你就能继续爬取了,坏一点的情况就是你的IP已经进入别人的黑名单了,然后你的爬虫就GG了。怎么办呢?我们可以通过设置代理来解决,付费代理的效果自然不必多说,但是对于学习阶段的人来说,我觉得爬取网上的免费代理来用是一个更好的选择,而这一篇博客就将教你怎么利用免费代理搭建属于你自己的代理池。
二、目标分析
要搭建一个代理池,需要三个模块:存储模块、爬取模块和测试模块。
存储模块:负责存储我们爬取下来的代理,首先我们需要保证这些代理不能有重复的,然后我们还要对代理是否可用进行标记,这里可以使用Redis数据库的SortedSet(有序集合)进行存储。
爬取模块:负责对一些提供免费代理的网站进行爬取,代理的形式是IP+端口,爬取下来之后保存到数据库里。
测试模块:负责对代理池中的代理的可用性进行测试,由于测试出错不一定就表明代理不可用,可能是因为网络问题或者请求超时等等,所以我们可以设置一个分数标识,100分标识可用,分数越低标识可用性越低,当分数低于一个阈值之后,就从代理池中移除。
三、具体实现
1、存储模块
这里使用的是Redis的有序集合。Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的成员进行从小到大的排序。这样我们保存到数据库中的元素就是一个代理和一个分数,比如112.17.65.133:8060和100,这就表示112.17.65.133:8060这个代理是可用的。
对于新添加到数据库中的代理,设置的初始分数为10,添加之后会进行一次测试,如果可用就把分数改为100表明可用,如果测试的结果是不可用就把分数减1,当分数减到0后就从代理池中移除。这么做的意义在于一次测试不可用并不能代表这个代理完全不可用,有可能在之后的某次测试中是可用的,这样我们就减小了将一个原本可用的代理从代理池中移除出去的概率。
当我们想要从代理池中获取一个代理的时候,优先从分数为100的代理中随机获取,如果一个100分的代理都没有,则对所有代理进行排序,然后随机获取一个代理。由于我们使用的是随机获取,这样就能保证代理池中的所有代理都有被获取的可能性。
现在我们需要定义一个类来实现这个有序集合,还需要设置一些方法来实现添加代理、修改分数、获取代理等功能。具体代码如下:
1 """ 2 Version: Python3.5 3 Author: OniOn 4 Site: http://www.cnblogs.com/TM0831/ 5 Time: 2019/2/12 14:54 6 """ 7 import redis 8 import random 9 10 MAX_SCORE = 100 # 最高分 11 MIN_SCORE = 0 # 最低分 12 INITIAL_SCORE = 10 # 初始分数 13 REDIS_HOST = "localhost" 14 REDIS_PORT = 6379 15 16 17 class RedisClient: 18 def __init__(self): 19 self.db = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) 20 self.key = "proxies" 21 22 def add(self, proxy, score=INITIAL_SCORE): 23 """ 24 将代理添加到代理池中 25 :param proxy: 代理 26 :param score: 分数 27 :return: 28 """ 29 if not self.is_exist(proxy): 30 self.db.zadd(self.key, proxy, score) 31 32 def is_exist(self, proxy): 33 """ 34 判断代理池中是否存在该代理 35 :param proxy: 代理 36 :return: True or False 37 """ 38 if self.db.zscore(self.key, proxy): 39 return True 40 else: 41 return False 42 43 def random(self): 44 """ 45 获取有效代理,先获取最高分代理,如果不存在,则按分数排名然后随机获取 46 :return: 代理 47 """ 48 result = self.db.zrangebyscore(self.key, MAX_SCORE, MAX_SCORE) 49 if len(result): 50 return random.choice(result) 51 else: 52 result = self.db.zrangebyscore(self.key, MIN_SCORE, MAX_SCORE) 53 if len(result): 54 return random.choice(result) 55 else: 56 print("代理池已空!") 57 58 def decrease(self, proxy): 59 """ 60 代理分数减1分,若小于最低分,则从代理池中移除 61 :param proxy: 62 :return: 63 """ 64 if self.is_exist(proxy): 65 score = self.db.zscore(self.key, proxy) 66 if score > MIN_SCORE: 67 score -= 1 68 self.db.zadd(self.key, proxy, score) 69 else: 70 self.delete(proxy) 71 72 def max(self, proxy): 73 """ 74 将代理分数设置为最高分 75 :param proxy: 代理 76 :return: 77 """ 78 if self.is_exist(proxy): 79 self.db.zadd(self.key, proxy, MAX_SCORE) 80 81 def delete(self, proxy): 82 """ 83 从代理池中移除该代理 84 :param proxy: 代理 85 :return: 86 """ 87 if self.is_exist(proxy): 88 self.db.zrem(self.key, proxy) 89 90 def all(self): 91 """ 92 获取代理池中的所有代理 93 :return: 94 """ 95 if self.count(): 96 return self.db.zrange(self.key, MIN_SCORE, MAX_SCORE) 97 98 def count(self): 99 """ 100 获取代理池中代理数量 101 :return: 102 """ 103 return self.db.zcard(self.key)
2、爬取模块
爬取模块比较简单,就是定义一个Crawler类来对一些提供免费代理的网站进行爬取。具体代码如下:
1 """ 2 Version: Python3.5 3 Author: OniOn 4 Site: http://www.cnblogs.com/TM0831/ 5 Time: 2019/2/12 15:07 6 """ 7 import requests 8 from lxml import etree 9 from fake_useragent import UserAgent 10 11 12 # 设置元类 13 class CrawlMetaClass(type): 14 def __new__(cls, name, bases, attrs): 15 attrs['__CrawlFuncCount__'] = 0 16 attrs['__CrawlFunc__'] = [] 17 for k, v in attrs.items(): 18 if 'crawl_' in k: 19 attrs['__CrawlFunc__'].append(k) 20 attrs['__CrawlFuncCount__'] += 1 21 # attrs['__CrawlFuncCount__'] = count 22 return type.__new__(cls, name, bases, attrs) 23 24 25 class Crawler(object, metaclass=CrawlMetaClass): 26 def __init__(self): 27 self.proxies = [] # 代理列表 28 ua = UserAgent() # 使用随机UA 29 self.headers = { 30 "UserAgent": ua.random 31 } 32 33 def get_proxies(self, callback): 34 """ 35 运行各个代理爬虫 36 :param callback: crawl函数名称 37 :return: 38 """ 39 for proxy in eval("self.{}()".format(callback)): 40 print("成功获取代理:", proxy) 41 self.proxies.append(proxy) 42 return self.proxies 43 44 def crawl_kdd(self): 45 """ 46 快代理爬虫 47 :return: 48 """ 49 urls = ["https://www.kuaidaili.com/free/inha/{}/".format(i) for i in range(1, 4)] 50 for url in urls: 51 res = requests.get(url, headers=self.headers) 52 try: 53 et = etree.HTML(res.text) 54 ip_list = et.xpath('//*[@data-title="IP"]/text()') 55 port_list = et.xpath('//*[@data-title="PORT"]/text()') 56 for ip, port in zip(ip_list, port_list): 57 yield ip + ":" + port 58 except Exception as e: 59 print(e) 60 61 def crawl_89ip(self): 62 """ 63 89IP爬虫 64 :return: 65 """ 66 urls = ["http://www.89ip.cn/index_{}.html".format(i) for i in range(1, 4)] 67 for url in urls: 68 res = requests.get(url, headers=self.headers) 69 try: 70 et = etree.HTML(res.text) 71 ip_list = et.xpath('//*[@class="layui-table"]/tbody/tr/td[1]/text()') 72 port_list = et.xpath('//*[@class="layui-table"]/tbody/tr/td[2]/text()') 73 ip_list = [i.strip() for i in ip_list] 74 port_list = [i.strip() for i in port_list] 75 for ip, port in zip(ip_list, port_list): 76 yield ip + ":" + port 77 except Exception as e: 78 print(e) 79 80 def crawl_xc(self): 81 """ 82 西刺代理爬虫 83 :return: 84 """ 85 url = "https://www.xicidaili.com/?t=253" 86 res = requests.get(url, headers=self.headers) 87 try: 88 et = etree.HTML(res.text) 89 ip_list = et.xpath('//*[@id="ip_list"]/tr[3]/td[2]/text()') 90 port_list = et.xpath('//*[@id="ip_list"]/tr[3]/td[3]/text()') 91 for ip, port in zip(ip_list, port_list): 92 yield ip + ":" + port 93 except Exception as e: 94 print(e)
可以看到几个爬虫的方法名称都是以crawl开头的,主要是为了方便,如果要添加新的爬虫就只用添加crawl开头的方法即可。
这里我写了爬取快代理、89IP代理和西刺代理的爬虫,都是用xpath进行解析,也都定义了一个生成器,然后用yield返回爬取到的代理。然后定义了一个get_proxies()方法,将所有以crawl开头的方法都调用一遍,获取每个方法返回的结果并生成一个代理列表,最后返回这个代理列表。那么如何获取crawl开头的方法呢?这里借用了元类来实现。首先定义一个类CrawlMetaClass,然后实现了__new__()方法,这个方法的第四个参数attrs里包含了类的一些属性,所以我们可以遍历attrs中包含的信息,就像遍历一个字典一样,如果方法名以crawl开头,就将其添加到__CrawlFunc__中,这样我们就能获取crawl开头的方法了。
我们已经定义好了爬取的方法了,但是还需要定义一个类来执行这些方法,这里可以定义一个GetProxy类来实现爬取代理并保存到代理池中,具体代码如下:
1 """ 2 Version: Python3.5 3 Author: OniOn 4 Site: http://www.cnblogs.com/TM0831/ 5 Time: 2019/2/14 12:19 6 """ 7 from ProxyPool.crawl import Crawler 8 from ProxyPool.pool import RedisClient 9 10 11 class GetProxy: 12 def __init__(self): 13 self.crawler = Crawler() 14 self.redis = RedisClient() 15 16 def get_proxy(self): 17 """ 18 运行爬虫爬取代理 19 :return: 20 """ 21 print("[INFO]Crawl Start...") 22 count = 0 23 for callback_label in range(self.crawler.__CrawlFuncCount__): 24 callback = self.crawler.__CrawlFunc__[callback_label] 25 # 获取代理 26 proxies = self.crawler.get_proxies(callback) 27 for proxy in proxies: 28 self.redis.add(proxy) 29 count += len(proxies) 30 print("此次爬取的代理数量为:{}".format(count)) 31 print("[INFO]Crawl End...\n\n")
3、测试模块
我们已经将代理成功爬取下来并保存到代理池中了,但是我们还需要对代理的可用性进行测试。测试的方法就是使用requests库设置代理并发送请求,如果请求成功并且返回的状态码是200的话,就表明这个代理是可用的,然后我们就要将该代理的分数设置为100,反之如果出现请求失败、请求超时或者返回的状态码不是200的话, 就要将该代理的分数减1,如果分数等于0了,就要从代理池中移除。
这里可以定义一个TestProxy类来实现,具体代码如下:
1 """ 2 Version: Python3.5 3 Author: OniOn 4 Site: http://www.cnblogs.com/TM0831/ 5 Time: 2019/2/14 14:24 6 """ 7 import time 8 import random 9 import requests 10 from fake_useragent import UserAgent 11 from ProxyPool.crawl import Crawler 12 from ProxyPool.pool import RedisClient 13 14 15 class TestProxy: 16 def __init__(self): 17 self.crawler = Crawler() 18 self.redis = RedisClient() 19 ua = UserAgent() # 使用随机UA 20 self.headers = { 21 "UserAgent": ua.random 22 } 23 24 def test(self): 25 """ 26 测试函数,测试代理池中的代理 27 :return: 28 """ 29 proxy_list = self.redis.all() 30 proxy_list = [i.decode('utf-8') for i in proxy_list] # 字节型转字符串型 31 32 print("[INFO]Test Start...") 33 for proxy in proxy_list: 34 self.request(proxy) 35 print("[INFO]Test End...\n\n") 36 37 def request(self, proxy): 38 """ 39 测试请求函数 40 :param proxy: 41 :return: 42 """ 43 print("当前测试代理:{} 该代理分数为:{}".format(proxy, self.redis.db.zscore(self.redis.key, proxy))) 44 time.sleep(random.randint(1, 4)) 45 try: 46 url = "https://www.baidu.com/" 47 proxies = { 48 "https": "https://" + proxy 49 } 50 res = requests.get(url, headers=self.headers, proxies=proxies, timeout=5) 51 52 if res.status_code == 200: 53 print("代理可用,分数设置为100") 54 self.redis.max(proxy) 55 else: 56 print("错误的请求状态码,分数减1") 57 self.redis.decrease(proxy) 58 except: 59 print("代理请求失败,分数减1") 60 self.redis.decrease(proxy)
四、运行程序
这里我定义了一个Main类来实现,过程为先爬取代理,然后对代理池中的代理进行测试,最后从代理池中获取一个可用代理。具体代码如下:
1 """ 2 Version: Python3.5 3 Author: OniOn 4 Site: http://www.cnblogs.com/TM0831/ 5 Time: 2019/2/14 14:26 6 """ 7 from ProxyPool.pool import RedisClient 8 from ProxyPool.get import GetProxy 9 from ProxyPool.test import TestProxy 10 11 12 class Main: 13 def __init__(self): 14 self.gp = GetProxy() 15 self.tp = TestProxy() 16 self.db = RedisClient() 17 18 def run(self): 19 """ 20 运行的主函数,先爬取代理,然后测试,最后获取一个有效代理 21 :return: 22 """ 23 self.gp.get_proxy() 24 self.tp.test() 25 proxy = self.db.random() 26 proxy = proxy.decode('utf-8') 27 print("从代理池中取出的代理为:{}".format(proxy)) 28 29 30 if __name__ == '__main__': 31 m = Main() 32 m.run()
运行结果截图如下:
完整代码已上传到GitHub!