Python 自建 IP 代理池
文章有点儿长,谨慎食用~
python 爬虫在爬取网页内容时,遭遇的最常见的反爬措施就是 ip 限制/封禁,对此最常见的解决方式就是设置 IP 代理池,每次请求时随机使用一个代理 IP 去访问资源。
网上有成熟的代理服务,但是小伙汁的爬虫需求多是非定期的自定义项目,使用付费代理并不划算,遂有了爬取免费代理并测试是否可用,进而构建一个可用代理 IP 池的想法。本项目亦可作为后续网络相关服务的子模块。
版本1:先通过 request 或者 selenium 进行爬取;
版本2(大概率是鸽了~):学习并使用 scrapy 进行爬取,填上坑了,重构了在这儿 https://www.cnblogs.com/zishu/p/17516900.html ;
0 项目逻辑架构
经过在编码过程中不断的修修改改,重重构构,整体逻辑终于是有了一个相对解耦的模式,由于项目相比于大型项目来说还是 just like a toy,所以核心模块就是一个通用的 Spider 父类模块,定义了整体的爬虫逻辑,其余针对特定网页的实例都继承自该 Spider。先看图吧:
Spider
是基础爬虫类,定义了一些静态的属性和功能方法spider x
是实例爬虫,每个实例爬虫需要根据自己网页的结构需要,重写pre_parse()
、parse()
、get_all_proxies()
方法Proxy Manager
是运行时的代理管理类(目前仅简单提供可用代理的临时存储功能)- 项目运行前,将多个对象爬虫实例化后配置在配置文件中,组成爬虫链。这样主函数执行时会串行加载每个爬虫实例并运行其爬取逻辑(之所以不用并行是想着后面的爬虫能利用前面爬虫验证过的代理,所以将那些没有反爬的爬虫实例尽量配在爬虫链的前面)
下面会详细的介绍每个模块的具体实现细节,再介绍之前,让我再 bb 几句吧。这个项目主要是出于个人兴趣,作为一个初入 python 爬虫领域的菜鸟,利用业余时间在拖拖拉拉中写完了这个项目,写的过程中也逐渐学习了一些 python 的高级语法,例如装饰器、自定义异常、多进程异步操作等,收获还是蛮多的。此外,由于免费代理资源本身并不是很稳定,指望通过免费代理资源来构建一个鲁棒的代理池还是有点困难的,所以这个项目更多的还是当学习使用。
食用提醒:
- 前置知识
- 最好还是要对 python requests 库有点了解,包括 请求、jsonpath 解析网页资源 等
- python 类的继承、多态等
- 可能的收获
- python requests 的使用
- python 类的使用
- python 多进程、进程池
- python 装饰器
- python 自定义异常类并在程序中手动抛出并处理
- python 进度条、输出格式个性化定制等
- 一点点软件工程设计的思想
1 Spider 模块
这个模块一开始写得时候比较简单,但是在后续加入各种各样的实例爬虫后,为了解耦和鲁棒性,功能也在不断的完善,为了阅读方便,先上简单版本代码:
# _*_ coding : utf-8 _*_
"""
定义基础 Spider 类
"""
import sys
import time
import requests
from tqdm import tqdm
from tools import check_proxy_icanhazip, check_proxy_900cha, get_free_proxy
from concurrent.futures import ProcessPoolExecutor, as_completed
import json
from config import *
class Spider:
def __init__(self, *args, **kwargs):
self.url = kwargs.get('url')
self.headers = kwargs.get('headers')
self.req_type = kwargs.get('req_type') # get or post
self.data = kwargs.get('data') # for post
self.proxies = kwargs.get('proxies')
self.verify = kwargs.get('verify')
self.day = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())).split(' ')[0].strip() # time tag
self.timeout = 3
self.proxy_try_num = 0 # 设置使用代理时的全局失败尝试次数
self.response = None
self.parse_urls = [] # 代理资源页入口
self.all_proxies = [] # 爬取到的所有代理
self.all_proxies_filter = [] # 验证后可用的所有代理
def pre_parse(self):
"""
识别当天代理信息资源页面
"""
pass
def parse(self):
"""
解析代理
"""
pass
def get_all_proxies(self):
"""
获取所有 proxies
"""
pass
def update_attrs(self, *args, **kwargs):
"""
update the spider object's attrs
:param args:
:param kwargs:
:return:
"""
self.url = self.url if kwargs.get('url') is None else kwargs.get('url')
self.headers = self.headers if kwargs.get('headers') is None else kwargs.get('headers')
self.req_type = self.req_type if kwargs.get('req_type') is None else kwargs.get('req_type')
self.data = self.data if kwargs.get('data') is None else kwargs.get('data')
self.proxies = self.proxies if kwargs.get('proxies') is None else kwargs.get('proxies')
self.verify = self.verify if kwargs.get('verify') is None else kwargs.get('verify')
self.timeout = self.timeout if kwargs.get('timeout') is None else kwargs.get('timeout')
def update_response(self, *args, **kwargs):
"""
recontruct a url request, and update the spider's response attribute
:param args:
:param kwargs:
:return:
"""
if self.data is None:
self.response = requests.get(self.url, headers=self.headers, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
else:
self.response = requests.post(self.url, headers=self.headers, data=self.data, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
return self.response
def filter_all_proxies_mp(self):
"""
测试代理 ip 可用性
多进程处理
"""
self.all_proxies_filter = dict()
# 进程池
pool = ProcessPoolExecutor(max_workers=50)
all_task = [pool.submit(check_proxy_900cha, proxy) for proxy in self.all_proxies]
for future in tqdm(as_completed(all_task), total=len(all_task), file=sys.stdout, desc='[{}] checking proxies...'.format(self.__class__.__name__)):
res, proxy = future.result()
if res:
self.all_proxies_filter['{}:{}'.format(proxy['ip'], proxy['port'])] = proxy
self.all_proxies_filter = self.all_proxies_filter.values()
return self.all_proxies_filter
def save_to_txt(self, file_name, all_proxies, add_day_tag=True):
"""
存文件
"""
if not os.path.isdir(os.path.dirname(file_name)):
os.makedirs(os.path.dirname(file_name))
if add_day_tag:
file_name = file_name.split('.')[0] + '_{}.'.format(self.day.replace('-', '_')) + file_name.split('.')[-1]
with open(file_name, 'a+', encoding='utf-8') as f:
for proxy in all_proxies:
f.write(json.dumps(proxy, ensure_ascii=False) + '\n')
def run(self):
"""
General spider running logic:
init -> face page url request -> (resource page collect) -> crawl all proxies -> check proxies' useability -> save
:return:
"""
# 1 爬取所有 proxies
self.all_proxies = self.get_all_proxies()
print('[{}] 爬取代理数:{}'.format(self.__class__.__name__, len(self.all_proxies)))
# 2 过滤可用代理
self.all_proxies_filter = self.filter_all_proxies_mp()
print('[{}] 可用代理数:{}'.format(self.__class__.__name__, len(self.all_proxies_filter)))
# 3 存储可用代理
# 默认存储路径配置在 config 中,如果想要另存,在子爬虫中重构 run() 方法即可
self.save_to_txt(os.path.join(useful_ip_file_path, useful_ip_file_name), self.all_proxies_filter)
print('[{}] run successed.'.format(self.__class__.__name__))
return list(self.all_proxies_filter)
if __name__ == '__main__':
help(Spider)
上面这段代码即对应着项目中所有实例爬虫的通用运行逻辑,步骤如下:
- 初始化相关参数
- run() 实例化爬虫运行入口
- pre_parse() 请求代理资源页
- get_all_proxies() 爬取逻辑的入口函数,开始对当前实例爬虫进行代理资源爬取
- parse() 对代理详情页进行解析
- filter_all_proxies_mp() 验证爬取到的代理的可用性
- save_to_txt() 将可用代理存到文件中
为了便于理解代理资源页、代理详情页,下面以站大爷的网页结构为例进行说明:
代理资源页:可包含多个具体的代理资源集合
代理详情页:即前面每一条资源的详情页面
实际使用中,假设我们已有了一个实例爬虫 SpiderX
,只需通过以下方式来启动:
spider_obj = SpiderX()
spider_obj.run()
插播一下:
上面我这里直接上了进程池的版本,本来一开始写得是串行验证,但是速度太慢了,所以果断换多进程并发。如果对 python 多进程不太熟悉,可以先停一会儿去这里看下相关知识,啪的一下很快的:https://www.cnblogs.com/zishu/p/17300868.html
1.1 Config
存储路径等参数放在 config.py
中:
# _*_ coding : utf-8 _*_
import os
RETRY_LIMIT = 4 # 爬取失败时的重试次数
# 存储文件地址
useful_ip_file_path = os.path.join('D:/FreeIPProxyGettingPro', 'proxies_spider_results')
# 存储文件名称
useful_ip_file_name = 'useful_proxies_spiding.txt'
1.2 验证代理可用性
当爬取到代理时,需要验证其可用性,对可用代理才将其保存。一般来说,较为简单的验证逻辑就是使用该代理对百度首页进行访问,然后根据返回结果验证代理可用性。但是在实际使用过程中,会出现各种问题,比如代理访问并没有隐藏掉源 ip、百度返回验证页面、无法访问但返回一个正常的说明网页(非百度首页)等。
所以这里采用的是使用代理对 IP 查询网站(https://ip.900cha.com/)进行访问,解析网页结果,判断网页显示的 ip 是否与所使用的代理 ip 一致:
代码逻辑 tools.py
:
# _*_ coding : utf-8 _*_
import requests
from lxml import etree
import time
def check_proxy_900cha(proxy, timeout=3, realtimeout=False):
"""
验证代理可用性
:param proxy:
:param timeout:
:param realtimeout:
:return:
"""
time.sleep(1) # 防止频繁访问给服务器带来过大压力
url = 'https://ip.900cha.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
proxies = {
'http': '{}:{}'.format(proxy['ip'], proxy['port']),
'https': '{}:{}'.format(proxy['ip'], proxy['port'])
}
try:
response = requests.get(url=url, headers=headers, proxies=proxies, timeout=timeout)
except Exception as e:
return False, None
else:
tree = etree.HTML(response.text)
ret_ip = tree.xpath('//div[@class="col-md-8"]/h3/text()')[0].strip()
if ret_ip == proxies['http'].split(':')[0]:
if realtimeout:
print(f'代理 {proxy["ip"]}:{proxy["port"]} 有效!')
return True, proxy
else:
return False, None
2 实例爬虫
搜集网上的一些免费代理资源,限于篇幅,这里以 3 个结构典型案例来展示。
2.1 seo 代理
可以说是最简单的一个代理页面了,入口页直接就放了 proxy 列表:
实例代码:
# _*_ coding : utf-8 _*_
from tools import *
from ProxiesSpider.spider import Spider # 前面的 Spider 类
class SpiderSeo(Spider):
def __init__(self, *args, **kwargs):
url = 'https://proxy.seofangfa.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(url=url, headers=headers)
def pre_parse(self):
self.parse_urls = [
'https://proxy.seofangfa.com/'
]
def parse(self):
"""
解析代理
"""
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//table[@class="table"]/tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[1]/text()')[0].strip(),
'port': proxy_obj.xpath('./td[2]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[4]/text()')[0].strip(),
'day': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
}
proxies.append(dic_)
return proxies
def get_all_proxies(self):
"""
获取所有 proxies
"""
self.pre_parse()
for parse_url in self.parse_urls:
self.update_attrs(url=parse_url)
self.update_response()
proxies = self.parse()
self.all_proxies += proxies
return self.all_proxies
if __name__ == '__main__':
spider_seo = SpiderSeo()
spider_seo.run()
pre_parse() 用来获取代理资源页,但是 seo 没有,为了结构一致性,在其中直接填充详情页。
2.2 快代理
快代理的网页结构介于 seo 和站大爷之间,其也没有代理资源页,但是其有两份代理资源(普通 & 高匿),所以同 seo 一样,直接在 pre_parse() 函数中填充即可。
# _*_ coding : utf-8 _*_
from ProxiesSpider.spider import Spider
from tools import *
import time
import sys
class SpiderKuai(Spider):
def __init__(self, *args, **kwargs):
kwargs['url'] = 'https://www.kuaidaili.com/free/inha/1/'
kwargs['headers'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(**kwargs)
def pre_parse(self):
self.parse_urls = [
'https://www.kuaidaili.com/free/intr/', # 国内普通代理
'https://www.kuaidaili.com/free/inha/' # 国内高匿代理
]
return self.parse_urls
def parse(self):
"""
解析代理
"""
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//div[@id="list"]//tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[@data-title="IP"]/text()')[0].strip(),
'port': proxy_obj.xpath('./td[@data-title="PORT"]/text()')[0].strip(),
'type': proxy_obj.xpath('./td[@data-title="类型"]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[@data-title="位置"]/text()')[0].strip(),
'day': proxy_obj.xpath('./td[@data-title="最后验证时间"]/text()')[0].strip().split(' ')[0]
}
if dic_['day'] != self.day:
break
proxies.append(dic_)
return proxies
def get_all_proxies(self):
"""
获取所有 proxies
"""
# 1 先获取所有待采集的 proxy list 页
self.pre_parse()
# 2 对每个 proxy 信息页的资源进行解析
for parse_url in self.parse_urls:
time.sleep(3)
self.update_attrs(url=parse_url)
self.update_response()
# 3 获取资源页所有的 proxy
count = 1
pbar = tqdm(file=sys.stdout, desc='[{}] crawling all pages...'.format(self.__class__.__name__))
while True:
proxies = self.parse()
if len(proxies) == 0:
break
self.all_proxies += proxies
next_page = '{}{}/'.format(parse_url, count+1)
count += 1
time.sleep(3)
self.update_attrs(url=next_page)
self.update_response()
pbar.update(1)
pbar.close()
return self.all_proxies
if __name__ == '__main__':
spider_kuai = SpiderKuai()
spider_kuai.run()
基本结构和 seo 的逻辑一致,只是这里在代理详情页解析资源时要复杂些,因为是多页结构。
观察各页资源可以发现,快代理是将所有累计的免费代理都放在一起,并没有按天分区,所以这里要面对的问题有两个:
- 翻页爬取
- 翻页过程中对资源更新日期进行检测,一但资源声明周期超过当天,就停止继续爬取
所以我们这里直接采用一个 True 循环,在 parse() 中一旦遇到生成周期超过当天的资源后,就及时返回。这样在继续翻页并且下一页没有当天资源时,就会返回空列表,此时结束循环。
在测试过程中,我发现快代理的还是有着简单的反爬限制的:
- 当连续访问多页内容时,会返回 -10,获取不到具体数据;
- 一天内多次访问时,会封 IP;
其中针对第一种,只需要在多个连续请求之间 sleep() 一下即可。而对第二种限制,由于我们的爬虫正式逻辑是一天访问一次,所以正式运行时逻辑上不会存在封禁 ip 的情况,所以可以不做处理(当然后面也可以利用已获取的代理对快代理网站进行访问)。
插播一下:
这里还有个知识点,就是 python 进度条组件 tqdm 的使用,由于 tqdm 默认的输出模式和 print 是不一样的,会导致 tqdm 输出和 print 输出排版交混的问题,所以这里需要在 tqdm 中指定
file=sys.stdout
,这样就不会出现上述问题。
2.3 站大爷
相对来说结构最为完善的网页,包含代理资源页、代理详情页。所以 pre_parse()
函数中需要先对代理资源页进行解析,获取当天的代理详情页链接,再进二级页面进行爬取。
# _*_ coding : utf-8 _*_
from lxml import etree
from ProxiesSpider.spider import Spider
import time
import sys
class SpiderZdaye(Spider):
"""
zdaye 自有证书,需要设置 verify=False
"""
def __init__(self, *args, **kwargs):
url = 'https://www.zdaye.com/dayProxy.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(url=url, headers=headers, verify=False)
def pre_parse(self):
"""
代理资源页解析
:return:
"""
self.update_response()
# 该网页需对 request 结果指定 utf-8 编码
self.response.encoding = 'utf-8'
content = self.response.text
tree = etree.HTML(content)
proxy_page_info_obj = tree.xpath('//div[@class="thread_content"]/h3/a')
for ppio in proxy_page_info_obj:
title = ppio.xpath('./text()')[0].strip().split(' ')[0]
parse_day = title.split('日')[0].replace('年', '-').replace('月', '-')
if [int(x) for x in parse_day.split('-')] == [int(x) for x in self.day.split('-')]:
self.parse_urls.append(ppio.xpath('./@href')[0])
else:
break
return self.parse_urls
def parse(self):
"""
解析代理
"""
self.response.encoding = 'utf-8'
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//table[@id="ipc"]/tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[1]/text()')[0].strip().replace('"', '').strip(),
'port': proxy_obj.xpath('./td[2]/text()')[0].strip().replace('"', '').strip(),
'type': proxy_obj.xpath('./td[3]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
'isp': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[1] if len(proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')) > 1 else None,
'day': self.day
}
proxies.append(dic_)
return proxies
def get_all_proxies(self):
"""
获取所有 proxies
"""
# 1 先获取所有代理详情页的 url
self.pre_parse()
# 2 对每个 proxy 信息页的资源进行解析
for parse_url in self.parse_urls:
parse_url = 'https://www.zdaye.com' + parse_url
self.update_attrs(url=parse_url)
self.update_response()
# 3 获取详情页所有的 proxy
while True:
time.sleep(3) # 间隔爬取
proxies = self.parse()
if len(proxies) == 0:
break
self.all_proxies += proxies
next_tag = etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')
if len(next_tag) == 0:
break
else:
next_page = 'https://www.zdaye.com' + etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')[0]
self.update_attrs(url=next_page)
self.update_response()
return self.all_proxies
if __name__ == '__main__':
spider_zdy = SpiderZdaye()
spider_zdy.run()
站大爷的网站需要注意的主要有两点:
- 设置
verify=False
来关闭 SSL 验证; - 该网站的反爬措施还蛮严厉的,上述代码中尽管对请求进行了简单的
sleep()
,但是只要多调试几次,还是会被封;
关于反爬,后面会讲解如何使用已爬取的代理来请求,并且站大爷这个网站还挺难搞的,后面会说到的。
3 爬虫链 & 使用代理绕过反爬
其实写到这里,如果要求不高的话,上面的功能已经可以做基本使用了,只需要挨个运行或者直接写一个主函数依次实例化并运行即可。但是上面已经说到了有些网站会有反爬限制,如果不使用代理,这个实例爬虫基本上就废掉了,所以下面我将多个实例爬虫排排队,串行执行,并在此过程中更新已有可用代理,并编写代理请求接口函数随机的获取一个可用代理,用来应对被限制的情况。
3.1 加载已有代理资源
看过 Spider 类代码的应该有印象,该项目将爬取并验证后的代理以字典的形式存到 txt 文件中,形式如下:
{"ip": "222.190.208.49", "port": "8089", "position": "江苏省泰州市", "isp": "电信", "day": "2023-04-13"}
{"ip": "36.137.106.110", "port": "7890", "position": "北京市", "isp": "移动", "day": "2023-04-13"}
{"ip": "182.241.132.30", "port": "80", "position": "云南省红河州", "isp": "电信", "day": "2023-04-13"}
并且多个实例爬虫是采用追加的形式向同一份文件追加写入的,那么我们自然可以简单的通过加载文件的方式来获取已有资源:
# 项目文件结构
-- proxies_spider_results:
-- useful_proxies_2023-04-11.txt
-- useful_proxies_2023-04-12.txt
-- ProxiesSpider:
-- seo_spider.py
-- kuai_spider.py
-- zdaye_spider.py
-- main.py
-- tools.py
import sys
import json
import random
import os
from config import useful_ip_file_path
here = os.path.dirname(__file__)
def get_latest_proxy_file(file_path):
"""
获取当前路径下的最新文件内容
:param file_path:
:return:
"""
file_latest = sorted(os.listdir(file_path))[-1]
with open(os.path.join(file_path, file_latest), 'r', encoding='utf=8') as f:
all_free_proxies = [json.loads(s.strip()) for s in f.readlines()]
return all_free_proxies
def get_free_proxy():
all_free_proxies = get_latest_proxy_file(useful_ip_file_path)
for i in tqdm(range(len(all_free_proxies)), file=sys.stdout, desc='choosing a useful proxy...'):
index = random.randint(0, len(all_free_proxies)-1)
proxy = all_free_proxies[index]
useful, proxy = check_proxy_900cha(proxy)
if useful:
return proxy
print('无可用 proxy ~')
return None
if __name__ == '__main__':
print(get_free_proxy())
这样,通过 get_free_proxy()
函数,就可以很容易的从已有代理中随机挑选一个可用的代理。
3.2 使用代理
有了上面的代理获取函数,在遇到反爬时,我们只需要调用一下,如果能返回一个可用的代理,那么就可以拿着这个代理去重新请求网页资源。
这里我以 kuai spider 为例,我们可以设置超时时间为 0.01 s,来模拟访问失败的情况,然后 catch 这个 Exception 并重新使用代理访问正确的网页:
# _*_ coding : utf-8 _*_
import sys
import os
sys.path.append(os.path.dirname(__name__))
from ProxiesSpider.spider import Spider
from lxml import etree
import time
import sys
from wrappers import req_respose_none_wrapper
from tqdm import tqdm
from tools import get_free_proxy
class SpiderKuai(Spider):
def __init__(self, *args, **kwargs):
kwargs['url'] = 'https://www.kuaidaili.com/free/inha/1/'
kwargs['headers'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(**kwargs)
def pre_parse(self):
self.parse_urls = [
'https://www.kuaidaili.com/free/intr/', # 国内普通代理
'https://www.kuaidaili.com/free/inha/' # 国内高匿代理
]
return self.parse_urls
def parse(self):
"""
解析代理
"""
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//div[@id="list"]//tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[@data-title="IP"]/text()')[0].strip(),
'port': proxy_obj.xpath('./td[@data-title="PORT"]/text()')[0].strip(),
'type': proxy_obj.xpath('./td[@data-title="类型"]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[@data-title="位置"]/text()')[0].strip(),
'day': proxy_obj.xpath('./td[@data-title="最后验证时间"]/text()')[0].strip().split(' ')[0]
}
if dic_['day'] != self.day:
break
proxies.append(dic_)
return proxies
def get_all_proxies(self):
"""
获取所有 proxies
"""
# 1 先获取所有待采集的 proxy list 页
self.pre_parse()
# 2 对每个 proxy 信息页的资源进行解析
for parse_url in self.parse_urls:
time.sleep(3)
self.update_attrs(url=parse_url)
self.update_response()
# 3 获取资源页所有的 proxy
count = 1
pbar = tqdm(file=sys.stdout, desc='[{}] crawling all pages...'.format(self.__class__.__name__))
while True:
proxies = self.parse()
if len(proxies) == 0:
break
self.all_proxies += proxies
next_page = '{}{}/'.format(parse_url, count+1)
count += 1
time.sleep(3)
self.update_attrs(url=next_page)
self.update_response()
pbar.update(1)
pbar.close()
return self.all_proxies
if __name__ == '__main__':
spider_kuai = SpiderKuai()
spider_kuai.timeout = 0.01
try:
spider_kuai.run()
except Exception as e:
print(e)
# 使用代理
proxy = get_free_proxy()
print('使用代理:', proxy)
if proxy is None:
proxies = None
else:
proxies = {
'http': '{}:{}'.format(proxy['ip'], proxy['port']),
'https': '{}:{}'.format(proxy['ip'], proxy['port']),
}
spider_kuai.timeout = 3
spider_kuai.run()
执行结果:
~\python_spider\FreeIPProxyGettingPro_TMP> kuai_proxy_spider.py
HTTPSConnectionPool(host='www.kuaidaili.com', port=443): Max retries exceeded with url: /free/intr/ (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x00000183962DFB50>, 'Connection to www.kuaidaili.com timed out. (connect timeout=0.01)'))
choosing a useful proxy...: 6%|█████▊ | 1/17 [00:06<01:38, 6.13s/it]
使用代理: {'ip': '182.241.132.30', 'port': '80', 'position': '云南省红河州', 'isp': '电信', 'day': '2023-04-13'}
[SpiderKuai] crawling all pages...: 2it [00:07, 3.72s/it]
[SpiderKuai] crawling all pages...: 2it [00:07, 3.69s/it]
[SpiderKuai] 爬取代理数:42
[SpiderKuai] checking proxies...: 100%|██████████████████████████████████████████████████████████| 42/42 [00:07<00:00, 5.48it/s]
[SpiderKuai] 可用代理数:0
[SpiderKuai] run successed.
可以看到,基本逻辑没问题。
4 鲁棒性
上一节介绍了如何使用爬取到的代理访问资源页,但是处理方式还是不够优雅。
我们可以想象一下,真实的爬虫运行出错场景是怎样的,首先肯定一开始是正常请求,结果报错了,此时我们应该暂停一下再次访问(确保不是网络本身的问题),如果还是不行就通过 get_free_proxy()
获取一个代理来进行请求,当然也不是无限次请求,我们暂且设置这种尝试次数不超过三次,之后如果还是报错,则停止这个爬虫。
4.1 request 请求出错处理逻辑
好了,有了这个基础的逻辑,我们就可以愉快的写代码了。
首先,为了解耦,我们肯定是不会在每个实例爬虫中像上一节中那样进行 try catch 的,别忘了 Spider 这个类,对 url 进行 requset 请求操作被封装在了 update_response()
这个函数中,所以我们可以直接对该函数进行 try catch。
在写之前梳理下逻辑:
-
try catch 住失败的 request,第一次失败,停一下,后面再尝试三次,为此我们需要一个计数参数,这个 Spider 中之前已经出现过了,就是
self.proxy_try_num = 0
,初始时赋 0; -
尝试三次之后该网页就不再尝试爬取了,换下个网页,但是下个网页如果失败还是要走上面的逻辑,所以需要将
self.proxy_try_num
参数再次置零; -
第三次尝试失败时,由于后续的代理页还是需要继续爬取的,但由于此时
response = requests.get()
写法中,response 拿到的是None
,这会导致后续parse()
过程报错(当然了,因为根本就没有爬取成功,自然没东西来解析嘛)。为此,还需要对
parse()
函数进行执行时异常捕获,这里我自定义了一个TryWithSelfProxyLimitException
异常用来标识重试失败这种情况。
好了,话不多说看代码,关键节点都在注释中提示了:
# _*_ coding : utf-8 _*_
"""
基础 Spider 类
"""
import sys
import time
import requests
from tqdm import tqdm
from tools import check_proxy_900cha, get_free_proxy
from concurrent.futures import ProcessPoolExecutor, as_completed
import json
from config import *
class Spider:
def __init__(self, *args, **kwargs):
...
self.proxy_try_num = 0 # 设置使用代理时的全局失败尝试次数
...
def update_response(self, *args, **kwargs):
try:
if self.proxy_try_num >= RETRY_LIMIT:
# 重置尝试次数
self.proxy_try_num = 0
# 抛出自定义异常:超出尝试次数
raise TryWithSelfProxyLimitException
if self.data is None:
self.response = requests.get(self.url, headers=self.headers, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
else:
self.response = requests.post(self.url, headers=self.headers, data=self.data, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
except TryWithSelfProxyLimitException as e:
# 打印异常信息
print('-' * 90)
print('\033[1;31m{}\033[0m'.format(
'[Trying times beyond the limit] [{}] | page: {} | e >>> {}'.format(self.__class__.__name__, self.url, e)
))
print('-' * 90)
# 直接返回 None
return None
except Exception as e:
print('\033[1;31m{}\033[0m'.format(
'[{}][{}] | page: {} | e >>> {}'.format(self.proxy_try_num, self.__class__.__name__, self.url, e)
))
if self.proxy_try_num == 0:
# 先简单停一下
print('\033[1;31m{}\033[0m'.format(
'停一下停一下~'
))
time.sleep(10)
else:
# 使用新代理重新请求
proxy = get_free_proxy()
if proxy is None:
proxies = None
else:
proxies = {
'http': '{}:{}'.format(proxy['ip'], proxy['port']),
'https': '{}:{}'.format(proxy['ip'], proxy['port']),
}
print('[{}] use new proxy: {}'.format(self.__class__.__name__, proxies))
self.update_attrs(proxies=proxies)
# 尝试次数 +1
self.proxy_try_num += 1
# 更换代理后再次请求
return self.update_response()
return self.response
...
if __name__ == '__main__':
help(Spider)
TryWithSelfProxyLimitException 定义在 custom_exceptions.py
中:
# _*_ coding : utf-8 _*_
"""
自定义异常类
"""
class TryWithSelfProxyLimitException(Exception):
"""
Use yourself proxy to re-request a url, if exceed the up limit times, throws this exception
"""
def __init__(self, obj=None):
pass
def __str__(self):
return 'Attempt to use self proxy excedding limit of times!'
这样再次运行 seo_spider.py
时,就会先停一下,然后尝试 3 次,之后在 parse()
中报 NoneType
异常(因为返回的 response 为 None
)。
至此,update_response()
这一层级的异常处理算是完成了,我们下面再在 parse()
上层对 NoneType
异常进行处理。
通常来说,我们要在 parse()
上层捕获 NoneType
异常,需要使用 try catch 包裹它,类似于下面这样:
try:
self.parse()
except Exception as e:
# 处理逻辑
但是我们的实例爬虫有多个,当然可以在每个实例爬虫代码中都写一遍,但这显然会使得代码后期维护起来变得更加困难,这里我们可以借助 python 装饰器,将处理逻辑封装到装饰器函数中。
关于 python 装饰器,简单来说就是一个自定义的外层函数,在被装饰函数执行前、后执行其他的逻辑,更详细的可以参考:
休息一下吧~
为了更直观的展示这里 parse()
中的 NoneType
异常确实是 3 次重试之后出现的,我自定义了一个 ResponseTextNoneException 异常(custom_exceptions.py
):
# _*_ coding : utf-8 _*_
"""
自定义异常类
"""
class ResponseTextNoneException(Exception):
"""
标识 self.response = None 的场景
"""
def __init__(self):
pass
def __str__(self):
return 'Self.response is None!'
异常已准备,开始写装饰器函数,捕获这个异常,我将所有装饰器函数都写在 wrappers.py
中:
# _*_ coding : utf-8 _*_
from functools import wraps
from custom_exceptions import ResponseTextNoneException
def req_respose_none_wrapper(func):
"""
装饰 self.response = None 的场景
"""
def inner(*args, **kwargs):
obj = args[0]
try:
if obj.response is None:
raise ResponseTextNoneException
else:
res = func(*args, **kwargs)
return res
except ResponseTextNoneException as e:
# 打印异常信息
print('-' * 90)
print('\033[1;31m{}\033[0m'.format(
'[Request failed, response is None] [{}] | page: {} | e >>> {}'.format(obj.__class__.__name__, obj.url, e)
))
print('-' * 90)
# 返回空列表,表示未爬取到代理资源
return []
return inner
定义好装饰器函数后,在实例爬虫中的相关函数之前通过 @
方式配上:
# _*_ coding : utf-8 _*_
import sys
import os
sys.path.append(os.path.dirname(__name__))
from tools import *
from ProxiesSpider.spider import Spider
from wrappers import req_respose_none_wrapper
class SpiderSeo(Spider):
def __init__(self, *args, **kwargs):
url = 'https://proxy.seofangfa.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(url=url, headers=headers)
def pre_parse(self):
...
# 配上装饰器
@req_respose_none_wrapper
def parse(self):
"""
解析代理
"""
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//table[@class="table"]/tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[1]/text()')[0].strip(),
'port': proxy_obj.xpath('./td[2]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[4]/text()')[0].strip(),
'day': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
}
proxies.append(dic_)
return proxies
def get_all_proxies(self):
...
再次运行 seo spider,输出结果如下:
[0][SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> HTTPSConnectionPool(host='proxy.seofangfa.com', port=443): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x0000019101C217C0>, 'Connection to proxy.seofangfa.com timed out. (connect timeout=0.01)'))
停一下停一下~
[1][SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> HTTPSConnectionPool(host='proxy.seofangfa.com', port=443): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x0000019101C3D130>, 'Connection to proxy.seofangfa.com timed out. (connect timeout=0.01)'))
choosing a useful proxy...: 0%| | 0/28 [00:02<?, ?it/s]
[SpiderSeo] use new proxy: {'http': '36.137.158.200:7890', 'https': '36.137.158.200:7890'}
[2][SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> HTTPSConnectionPool(host='proxy.seofangfa.com', port=443): Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.', timeout('timed out')))
choosing a useful proxy...: 0%| | 0/28 [00:01<?, ?it/s]
[SpiderSeo] use new proxy: {'http': '58.32.1.58:8090', 'https': '58.32.1.58:8090'}
[3][SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> HTTPSConnectionPool(host='proxy.seofangfa.com', port=443): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x0000019101C61760>, 'Connection to 58.32.1.58 timed out. (connect timeout=0.01)'))
choosing a useful proxy...: 0%| | 0/28 [00:01<?, ?it/s]
[SpiderSeo] use new proxy: {'http': '180.184.91.187:443', 'https': '180.184.91.187:443'}
------------------------------------------------------------------------------------------
[Trying times beyond the limit] [SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> Attempt to use self proxy excedding limit of times!
------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------
[Request failed, response is None] [SpiderSeo] | page: https://proxy.seofangfa.com/ | e >>> The self.response is None!
------------------------------------------------------------------------------------------
[SpiderSeo] 爬取代理数:0
[SpiderSeo] checking proxies...: 0it [00:00, ?it/s]
[SpiderSeo] 可用代理数:0
[SpiderSeo] run successed.
可以看到,首先是触发了重试次数超限的异常,再触发了 self.response = None
的异常,足后返回 []
,由于 seo 只有一个 url 资源页,所以爬虫运行紧随结束,爬取数为 0。
4.2 其他异常
到这里,常见异常的处理基本结束了。但是,在爬取站大爷资源的过程中,我发现当被限制爬取资源时,站大爷并不是直接拒绝请求从而导致 response 为 None,而是会返回一个 500 的页面,长下面这样,这样,爬取过程并不会报错,但是确实爬不到想要的资源,自然也不会触发上面所说的 3 次重试逻辑。
为了防止这种情况导致爬虫逻辑直接一次被略过,可以自定义异常并抛出,和其他异常一起触发上面的重试逻辑,我定义了一个 Request500Exception
异常(custom_exceptions.py
):
# _*_ coding : utf-8 _*_
"""
自定义异常类
"""
class Request500Exception(Exception):
"""
If we receive status code of 500 durning requesting, we'll get a special page(500-page).
In this case, the spider runs successfully, but we do not get any data.
"""
def __init__(self, obj=None):
self.obj = obj if obj is not None else 'unknown'
def __str__(self):
if self.obj == 'unknow':
return 'The request response statu code is 500!'
else:
return '{} | The request response statu code is 500!'.format(self.obj.__class__.__name__)
之后,针对这种情况,在 Spider 类中抛出:
# _*_ coding : utf-8 _*_
"""
定义基础 Spider 类
"""
import sys
import time
import requests
from wrappers import old_version_fun_wrapper, req_exceed_limit_wrapper, req_respose_none_wrapper
from tqdm import tqdm
from tools import check_proxy_icanhazip, check_proxy_900cha, get_free_proxy
from concurrent.futures import ProcessPoolExecutor, as_completed
import json
from config import *
from custom_exceptions import Request500Exception, TryWithSelfProxyLimitException, ResponseTextNoneException
class Spider:
def __init__(self, *args, **kwargs):
...
...
def update_response(self, *args, **kwargs):
"""
recontruct a url request, and update the spider's response attribute
:param args:
:param kwargs:
:return:
"""
try:
if self.proxy_try_num >= RETRY_LIMIT:
# 重置尝试次数
self.proxy_try_num = 0
raise TryWithSelfProxyLimitException
if self.data is None:
self.response = requests.get(self.url, headers=self.headers, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
else:
self.response = requests.post(self.url, headers=self.headers, data=self.data, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
# 自定义一些特定异常(在 catch Exceptino 中会触发重试逻辑)
if self.response.status_code == 500:
raise Request500Exception(self)
except TryWithSelfProxyLimitException as e:
print('-' * 90)
print('\033[1;31m{}\033[0m'.format(
'[Trying times beyond the limit] [{}] | page: {} | e >>> {}'.format(self.__class__.__name__, self.url, e)
))
print('-' * 90)
# 直接返回 None
return None
except Exception as e:
# 这里也会捕获 Request500Exception 异常
print('\033[1;31m{}\033[0m'.format(
'[{}][{}] | page: {} | e >>> {}'.format(self.proxy_try_num, self.__class__.__name__, self.url, e)
))
if self.proxy_try_num == 0:
# 先简单停一下
print('\033[1;31m{}\033[0m'.format(
'停一下停一下~'
))
time.sleep(5)
else:
# 使用新代理重新请求
proxy = get_free_proxy()
if proxy is None:
proxies = None
else:
proxies = {
'http': '{}:{}'.format(proxy['ip'], proxy['port']),
'https': '{}:{}'.format(proxy['ip'], proxy['port']),
}
print('[{}] use new proxy: {}'.format(self.__class__.__name__, proxies))
self.update_attrs(proxies=proxies)
self.proxy_try_num += 1
return self.update_response()
return self.response
...
站大爷有多个资源页和详情页,为了证明我们的重试逻辑是正常有效的,可以在代码中记录下爬取成功和失败的页面,最后输出查看,zday spider 代码如下:
# _*_ coding : utf-8 _*_
import sys
import os
sys.path.append(os.path.dirname(__name__))
from lxml import etree
from ProxiesSpider.spider import Spider
import time
import sys
from wrappers import req_respose_none_wrapper
class SpiderZdaye(Spider):
"""
zdaye 自有证书,需要设置 verify=False
"""
def __init__(self, *args, **kwargs):
url = 'https://www.zdaye.com/dayProxy.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
super().__init__(url=url, headers=headers, verify=False)
def pre_parse(self):
"""
代理资源页解析
:return:
"""
self.update_response()
self.response.encoding = 'utf-8'
content = self.response.text
tree = etree.HTML(content)
proxy_page_info_obj = tree.xpath('//div[@class="thread_content"]/h3/a')
for ppio in proxy_page_info_obj:
title = ppio.xpath('./text()')[0].strip().split(' ')[0]
parse_day = title.split('日')[0].replace('年', '-').replace('月', '-')
if [int(x) for x in parse_day.split('-')] == [int(x) for x in self.day.split('-')]:
self.parse_urls.append(ppio.xpath('./@href')[0])
else:
break
return self.parse_urls
@req_respose_none_wrapper
def parse(self):
"""
解析代理
"""
self.response.encoding = 'utf-8' # 注意该页面的编码
content = self.response.text
tree = etree.HTML(content)
proxies_obj = tree.xpath('//table[@id="ipc"]/tbody/tr')
proxies = []
for proxy_obj in proxies_obj:
dic_ = {
'ip': proxy_obj.xpath('./td[1]/text()')[0].strip().replace('"', '').strip(),
'port': proxy_obj.xpath('./td[2]/text()')[0].strip().replace('"', '').strip(),
'type': proxy_obj.xpath('./td[3]/text()')[0].strip(),
'position': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
'isp': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[1] if len(proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')) > 1 else None,
'day': self.day
}
proxies.append(dic_)
return proxies
def get_all_proxies(self):
"""
获取所有 proxies
"""
# 1 先获取所有代理详情页的 url
self.pre_parse()
print(self.parse_urls)
# for debug
req_successed_urls = [] # 请求成功的页面
req_failed_urls = [] # 请求失败的页面
# 2 对每个 proxy 信息页的资源进行解析
for parse_url in self.parse_urls:
parse_url = 'https://www.zdaye.com' + parse_url
self.update_attrs(url=parse_url)
self.update_response()
# for debug
print('-' * 90)
print(self.response.status_code)
if self.response.status_code == 200:
# 请求成功
req_successed_urls.append(parse_url)
else:
# 请求失败
req_failed_urls.append(parse_url)
# 将页面存下看看
self.response.encoding = 'utf-8'
with open('zday.html', 'w', encoding='utf-8') as f:
f.write(self.response.text)
print('save the page screen shot')
print('-' * 90)
# 3 获取资源页所有的 proxy
while True:
time.sleep(3) # 间隔爬取
proxies = self.parse()
if len(proxies) == 0:
break
self.all_proxies += proxies
next_tag = etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')
if len(next_tag) == 0:
break
else:
next_page = 'https://www.zdaye.com' + etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')[0]
self.update_attrs(url=next_page)
self.update_response()
# for debug
if self.response.status_code == 200:
req_successed_urls.append(next_page)
else:
req_failed_urls.append(next_page)
# for debug
if len(req_failed_urls) > 0:
print('+' * 90)
print('successed urls:', req_successed_urls)
print('failed urls:', req_failed_urls)
print('+' * 90)
return self.all_proxies
if __name__ == '__main__':
spider_zdy = SpiderZdaye()
spider_zdy.run()
运行上面代码,输出如下:
~
['/dayProxy/ip/334680.html', '/dayProxy/ip/334679.html', '/dayProxy/ip/334678.html']
parse_url: https://www.zdaye.com/dayProxy/ip/334680.html
------------------------------------------------------------------------------------------
200
save the page screen shot
------------------------------------------------------------------------------------------
parse_url: https://www.zdaye.com/dayProxy/ip/334679.html
[0][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334679.html | e >>> SpiderZdaye | The request response statu code is 500!
停一下停一下~
[1][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334679.html | e >>> SpiderZdaye | The request response statu code is 500!
choosing a useful proxy...: 4%|▎ | 1/28 [00:05<02:19, 5.18s/it]
[SpiderZdaye] use new proxy: {'http': '183.221.242.107:8443', 'https': '183.221.242.107:8443'}
[2][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334679.html | e >>> SpiderZdaye | The request response statu code is 500!
choosing a useful proxy...: 14%|█▍ | 4/28 [00:10<01:01, 2.56s/it]
[SpiderZdaye] use new proxy: {'http': '183.221.242.103:9443', 'https': '183.221.242.103:9443'}
[3][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334679.html | e >>> SpiderZdaye | The request response statu code is 500!
choosing a useful proxy...: 0%| | 0/28 [00:03<?, ?it/s]
[SpiderZdaye] use new proxy: {'http': '36.137.158.200:7890', 'https': '36.137.158.200:7890'}
------------------------------------------------------------------------------------------
[Trying times beyond the limit] [SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334679.html | e >>> Attempt to use self proxy excedding limit of times!
------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------
500
save the page screen shot
------------------------------------------------------------------------------------------
parse_url: https://www.zdaye.com/dayProxy/ip/334678.html
[0][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334678.html | e >>> HTTPSConnectionPool(host='www.zdaye.com', port=443): Max retries exceeded with url: /dayProxy/ip/334678.html (Caused by ProxyError('Cannot connect to proxy.', timeout('_ssl.c:1112: The handshake operation timed out')))
停一下停一下~
[1][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334678.html | e >>> HTTPSConnectionPool(host='www.zdaye.com', port=443): Max retries exceeded with url: /dayProxy/ip/334678.html (Caused by ProxyError('Cannot connect to proxy.', timeout('_ssl.c:1112: The handshake operation timed out')))
choosing a useful proxy...: 4%|▎ | 1/28 [00:06<02:58, 6.60s/it]
[SpiderZdaye] use new proxy: {'http': '111.225.152.224:8089', 'https': '111.225.152.224:8089'}
[2][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334678.html | e >>> HTTPSConnectionPool(host='www.zdaye.com', port=443): Max retries exceeded with url: /dayProxy/ip/334678.html (Caused by ProxyError('Cannot connect to proxy.', timeout('timed out')))
choosing a useful proxy...: 32%|███▏ | 9/28 [00:34<01:13, 3.87s/it]
[SpiderZdaye] use new proxy: {'http': '182.241.132.30:80', 'https': '182.241.132.30:80'}
[3][SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334678.html | e >>> SpiderZdaye | The request response statu code is 500!
choosing a useful proxy...: 7%|▋ | 2/28 [00:07<01:39, 3.84s/it]
[SpiderZdaye] use new proxy: {'http': '180.184.91.187:443', 'https': '180.184.91.187:443'}
------------------------------------------------------------------------------------------
[Trying times beyond the limit] [SpiderZdaye] | page: https://www.zdaye.com/dayProxy/ip/334678.html | e >>> Attempt to use self proxy excedding limit of times!
------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------
500
save the page screen shot
------------------------------------------------------------------------------------------
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
successed urls: ['https://www.zdaye.com/dayProxy/ip/334680.html', 'https://www.zdaye.com/dayProxy/ip/334680/2.html', 'https://www.zdaye.com/dayProxy/ip/334680/3.html', 'https://www.zdaye.com/dayProxy/ip/334680/4.html', 'https://www.zdaye.com/dayProxy/ip/334680/5.html']
failed urls: ['https://www.zdaye.com/dayProxy/ip/334679.html', 'https://www.zdaye.com/dayProxy/ip/334678.html']
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[SpiderZdaye] 爬取代理数:100
[SpiderZdaye] checking proxies...: 100%|██████████| 100/100 [00:40<00:00, 2.49it/s]
[SpiderZdaye] 可用代理数:3
[SpiderZdaye] run successed.
可以看到,有些页成功,有些页失败,整体逻辑没问题。
5 主函数
写到这里,上面的基础模块基本介绍完了,并且单个爬虫实例也测试完了。接下来要做的就是将整个项目的结构做个整合,通过入口函数根据爬虫链逐步执行。
5.1 参数配置文件
配置运行时的各种参数(config.py
):
# _*_ coding : utf-8 _*_
"""
配置文件
"""
import os
RETRY_LIMIT = 4 # 爬取失败时的重试次数
# 可用 ip 存储文件地址
here_cfg = os.path.dirname(__file__)
useful_ip_file_path = os.path.join(here_cfg, 'proxies_spider_results')
# 可用 ip 存储文件名称
useful_ip_file_name = 'useful_proxies_spiding.txt'
5.2 存量数据验证
假设爬虫项目已经运行了多天,那么在开始启动实例爬虫保存当天的数据之前,其实昨天的爬取结果从逻辑上来说也是一份免费资源,并且当我们爬虫链中的第一个爬虫就需要代理时,我们正好可以从昨天的代理资源中抽取一个可用代理。
因此,在运行爬虫链之前,比较好的做法应该是先验证前一天爬取过的资源,本项目中,我将对前一天的资源验证操作也封装成一个爬虫实例,放在爬虫链的首位。
不过由于无需请求网络,该实例爬虫的逻辑和其他真实爬虫并不一样,所以无需继承 Spider 类,但为了爬虫链的统一运行逻辑,代码结构也需与其他实例爬虫保持一定的一致性(stock_proxy_spider.py
):
# _*_ coding : utf-8 _*_
"""
前一天爬取的存量代理的验证(为保持结构一致性,也封装成 Spider 类形式)
"""
from tqdm import tqdm
from config import *
import json
from tools import get_latest_proxy_file
from concurrent.futures import ProcessPoolExecutor, as_completed
from tools import check_proxy_900cha
import time
import sys
class SpiderStock:
def __init__(self):
self.parse_urls = []
self.all_proxies = []
self.all_proxies_filter = []
def pre_parse(self):
"""
识别当天代理页面
:return:
"""
pass
def parse(self):
"""
解析代理
"""
pass
def get_all_proxies(self):
"""
获取所有 proxies
"""
self.all_proxies = get_latest_proxy_file(useful_ip_file_path)
return self.all_proxies
def filter_all_proxies_mp(self):
"""
测试代理 ip 可用性
多进程处理
"""
self.all_proxies_filter = dict()
# process pool
pool = ProcessPoolExecutor(max_workers=50)
all_task = [pool.submit(check_proxy_900cha, proxy) for proxy in self.all_proxies]
for future in tqdm(as_completed(all_task), file=sys.stdout, total=len(all_task), desc='[{}] checking proxies...'.format(self.__class__.__name__)):
res, proxy = future.result()
if res:
self.all_proxies_filter['{}:{}'.format(proxy['ip'], proxy['port'])] = proxy
self.all_proxies_filter = self.all_proxies_filter.values()
return self.all_proxies_filter
def save_to_txt(self, file_name, all_proxies, add_day_tag=True):
"""
存文件
"""
day = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())).split(' ')[0].strip()
if not os.path.isdir(os.path.dirname(file_name)):
os.makedirs(os.path.dirname(file_name))
if add_day_tag:
file_name = file_name.split('.')[0] + '_{}.'.format(day.replace('-', '_')) + file_name.split('.')[-1]
with open(file_name, 'w', encoding='utf-8') as f:
for proxy in all_proxies:
f.write(json.dumps(proxy, ensure_ascii=False) + '\n')
# print(f'写入成功:{file_name}')
def run(self):
"""
General spider running logic:
init -> face page url request -> (resource page collect) -> crawl all proxies -> check proxies' usability -> save
:return:
"""
self.get_all_proxies()
self.filter_all_proxies_mp()
self.save_to_txt(os.path.join(useful_ip_file_path, useful_ip_file_name), self.all_proxies_filter)
if __name__ == '__main__':
stock_spider = SpiderStock()
stock_spider.run()
用到的工具函数(tools.py
):
# _*_ coding : utf-8 _*_
import sys
import requests
from lxml import etree
import json
import random
from tqdm import tqdm
import os
from config import useful_ip_file_path
here = os.path.dirname(__file__)
def get_latest_proxy_file(file_path):
"""
获取当前路径下的最新文件内容
:param file_path:
:return:
"""
file_latest = sorted(os.listdir(file_path))[-1]
with open(os.path.join(file_path, file_latest), 'r', encoding='utf=8') as f:
all_free_proxies = [json.loads(s.strip()) for s in f.readlines()]
return all_free_proxies
def get_free_proxy():
all_free_proxies = get_latest_proxy_file(useful_ip_file_path)
for i in tqdm(range(len(all_free_proxies)), file=sys.stdout, desc='choosing a useful proxy...'):
index = random.randint(0, len(all_free_proxies)-1)
proxy = all_free_proxies[index]
useful, proxy = check_proxy_900cha(proxy)
if useful:
return proxy
print('无可用 proxy ~')
return None
5.3 爬虫链配置文件
将多个实例爬虫串行组织(chain_of_spiders.py
),存量数据验证 spider 放首位:
# _*_ coding : utf-8 _*_
# 自定义爬虫链
from ProxiesSpider import (
kuai_proxy_spider,
seo_proxy_spider,
zdaye_proxy_spider,
stock_proxy_spider
)
proxy_spiders = [
stock_proxy_spider.SpiderStock,
kuai_proxy_spider.SpiderKuai,
seo_proxy_spider.SpiderSeo,
zdaye_proxy_spider.SpiderZdaye
]
5.4 代理管理类(proxy manager)
为了更好的管理爬虫链中多个实例爬虫,我们可以定义一个逻辑上的总管,这个管理类可以管理多个实例爬虫,并为其提供一些服务。我们可以在管理类中添加任何你想用的小功能:
# _*_ coding : utf-8 _*_
"""
代理管理器
"""
import random
from tqdm import tqdm
from tools import check_proxy_900cha
class ProxyManager:
def __init__(self):
self.proxies = []
self.proxies_set = set()
def get_proxy(self):
# 随机返回一个代理
idx = random.randint(0, len(self.proxies)-1)
return self.proxies[idx]
def get_proxy_num(self):
# 返回当前已爬取并验证过的代理数
return len(self.proxies)
def add_proxy(self, proxy):
# 向管理类中添加一个代理
proxy_str = "{}:{}".format(proxy['ip'], proxy['port'])
if proxy_str not in self.proxies_set:
self.proxies_set.add(proxy_str)
self.proxies.append(proxy)
def add_proxies(self, proxies):
# 向管理类中添加一组代理
for proxy in tqdm(proxies):
proxy_str = "{}:{}".format(proxy['ip'], proxy['port'])
if proxy_str not in self.proxies_set:
self.proxies_set.add(proxy_str)
self.proxies.append(proxy)
def get_useful_proxy(self):
# 获取一个可用的代理
for i in tqdm(range(len(self.proxies))):
proxy = self.get_proxy()
if check_proxy_900cha(proxy['ip'], proxy['port']):
return proxy
print('无可用 proxy ~')
return None
5.4 主函数
整个项目的入口(main.py
):
# _*_ coding : utf-8 _*_
"""
项目入口
逻辑:
1. 先开启全局代理持有器
2. 进行存量数据验证
3. 启动配置过的爬虫
4. 保存
"""
from proxy_manager import ProxyManager
from ProxiesSpider.stock_proxy_spider import SpiderStock
from chain_of_spiders_cfg import *
if __name__ == '__main__':
# global proxy manager
proxy_manager = ProxyManager()
print('ini useful proxy num:', proxy_manager.get_proxy_num())
# 按序执行爬虫链
spiders = [spider() for spider in proxy_spiders]
for i, spider in enumerate(spiders):
print('=' * 160)
spider.run()
proxy_manager.add_proxies(spider.all_proxies_filter)
if i == 0:
print('after stock proxies loading, the useful proxy num:', proxy_manager.get_proxy_num())
print("proxy_manager's proxy num:", proxy_manager.get_proxy_num())
执行 main 文件,会自动的串行执行整个爬虫链。
6 差不多该结束了
写到这里,基本的结构都介绍完了,代码其实也不复杂,学习之旅告一段落,学习代码已上传至 git:https://github.com/sinat-jiang/FreeIPProxyGettingPro 。
本文来自博客园,作者:sinatJ,转载请注明原文链接:https://www.cnblogs.com/zishu/p/17316593.html