有時候你的爬蟲剛開始的時候能夠正常運行,可以正常的爬取數據,可是過了一會,卻出現了一個「403 Forbidden",或者是」您的IP訪問頻率過高「這樣的提示,這就意味着你的IP被ban了,好一點的狀況是過一段時間你就能繼續爬取了,壞一點的狀況就是你的IP已經進入別人的黑名單了,而後你的爬蟲就GG了。怎麼辦呢?咱們能夠經過設置代理來解決,付費代理的效果天然沒必要多說,可是對於學習階段的人來講,我以爲爬取網上的免費代理來用是一個更好的選擇,而這一篇博客就將教你怎麼利用免費代理搭建屬於你本身的代理池。html
要搭建一個代理池,須要三個模塊:存儲模塊、爬取模塊和測試模塊。git
存儲模塊:負責存儲咱們爬取下來的代理,首先咱們須要保證這些代理不能有重複的,而後咱們還要對代理是否可用進行標記,這裏可使用Redis數據庫的SortedSet(有序集合)進行存儲。github
爬取模塊:負責對一些提供免費代理的網站進行爬取,代理的形式是IP+端口,爬取下來以後保存到數據庫裏。redis
測試模塊:負責對代理池中的代理的可用性進行測試,因爲測試出錯不必定就代表代理不可用,多是由於網絡問題或者請求超時等等,因此咱們能夠設置一個分數標識,100分標識可用,分數越低標識可用性越低,當分數低於一個閾值以後,就從代理池中移除。數據庫
這裏使用的是Redis的有序集合。Redis 有序集合和集合同樣也是string類型元素的集合,且不容許重複的成員,不一樣的是每一個元素都會關聯一個double類型的分數,Redis正是經過分數來爲集合中的成員進行從小到大的排序。這樣咱們保存到數據庫中的元素就是一個代理和一個分數,好比112.17.65.133:8060和100,這就表示112.17.65.133:8060這個代理是可用的。網絡
對於新添加到數據庫中的代理,設置的初始分數爲10,添加以後會進行一次測試,若是可用就把分數改成100代表可用,若是測試的結果是不可用就把分數減1,當分數減到0後就從代理池中移除。這麼作的意義在於一次測試不可用並不能表明這個代理徹底不可用,有可能在以後的某次測試中是可用的,這樣咱們就減少了將一個本來可用的代理從代理池中移除出去的機率。併發
當咱們想要從代理池中獲取一個代理的時候,優先從分數爲100的代理中隨機獲取,若是一個100分的代理都沒有,則對全部代理進行排序,而後隨機獲取一個代理。因爲咱們使用的是隨機獲取,這樣就能保證代理池中的全部代理都有被獲取的可能性。app
如今咱們須要定義一個類來實現這個有序集合,還須要設置一些方法來實現添加代理、修改分數、獲取代理等功能。具體代碼以下:dom
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)
爬取模塊比較簡單,就是定義一個Crawler類來對一些提供免費代理的網站進行爬取。具體代碼以下:ide
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")
咱們已經將代理成功爬取下來並保存到代理池中了,可是咱們還須要對代理的可用性進行測試。測試的方法就是使用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!