- 1. 代理池概述
- 2. 代理池的设计
- 3. 定义代理IP的数据模型类
- 4. 代理池工具模块
- utils/log.py
- 导入settings中日志配置信息
- 初始化并配一个logger对象,达到单例的
- 使用时,直接导入logger就可以使用
- 5. 代理IP的校验模块
- 6. 代理池数据库模块
- 准备URL列表
- # 分组的XPATH, 用于获取包含代理IP信息的标签列表
- 组内的XPATH, 用于提取 ip, port, area
- 准备URL列表
- # 分组的XPATH, 用于获取包含代理IP信息的标签列表
- 组内的XPATH, 用于提取 ip, port, area
- 当我们两个页面访问时间间隔太短了, 就报错了; 这是一种反爬手段.
- 打猴子补丁
- 导入协程池
- 9. 实现代理池的API模块
- 1. 在proxy_api.py中, 创建ProxyApi类
- 11.代码
1. 代理池概述
1.1 什么是代理池
- 免费代理都是非常不稳定的, 有10%是可用就很不错了.
- 一些收费代理稳定性也不好, 便宜一点只有30%~50%左右是可用.
- 注: 如果代理IP提供商, 提供接口很好, 稳定性也很高, 就无需使用代理池
1.3. 代理池开发环境
- 平台: Window,可以运行Linux上
- 开发语言: Python3
- 开发工具: PyCharm
使用到的主要技术:
代理池工作流程文字描述:
代理池分五大核心模块:
- 爬虫模块: 采集代理IP
- 从代理IP网站上采集代理IP
- 进行校验(获取代理响应速度, 协议类型, 匿名类型),
- 把可用代理IP存储到数据库中
- 代理IP的校验模块: 获取指定代理的响应速度, 支持的协议以及匿名程度
- 原因: 网站上所标注的响应速度,协议类型和匿名类型是不准确的
- 这里使用httpbin.org进行检测
- 数据库模块: 实现对代理IP的增删改查操作
- 这里使用MongoDB来存储代理IP
- 检测模块: 定时的对代理池中代理进行检测, 保证代理池中代理的可用性.
- 从数据库读取所有的代理IP
- 对代理IP进行逐一检测, 可用开启多个协程, 以提高检测速度
- 如果该代理不可用, 就让这个代理分数-1, 当代理的分数到0了, 就删除该代理; 如果检测到代理可用就恢复为满分.
- 代理IP服务接口: 提供高可用的代理IP给爬虫使用
- 根据协议类型和域名获取随机的高质量代理IP
- 根据协议类型和域名获取多个高质量代理IP
- 根据代理IP,不可用域名, 告诉代理池这个代理IP在该域名下不可用, 下次获取这个域名的代理IP时候, 就不会再获取这个代理IP了, 从而保证代理IP高可用性.
- 爬虫模块: 采集代理IP
代理池的其他模块
- 数据模型: domain.py:
- 代理IP的数据模型, 用于封装代理IP相关信息, 比如ip,端口号, 响应速度, 协议类型, 匿名类型,分数等.
- 程序启动入口: main.py
- 代理池提供一个统一的启动入口
- 工具模块:
- 日志模块: 用于记录日志信息
- http模块: 用于获取随机User-Agent的请求头
- 配置文件: settings.py
- 用于默认代理的分数, 配置日志格式, 文件, 启动的爬虫, 检验的间隔时间 等.
2.2. 代理池的项目结构
代理池的项目结构:-- IPProxyPool-- core-- db-- __init__.py-- mongo_pool.py-- proxy_validate-- __init__.py-- httpbin_validator.py-- proxy_spiders-- __init__.py-- base_spider.py-- proxy_spiders.py-- run_spiders.py-- proxy_test.py-- proxy_api.py-- domain.py-- utils-- __init__.py-- http.py-- log.py-- main.py-- settings.py
3. 定义代理IP的数据模型类
- 用于默认代理的分数, 配置日志格式, 文件, 启动的爬虫, 检验的间隔时间 等.
- 数据模型: domain.py:
目标: 用于封装代理信息
步骤:
- 定义一个类, 继承object
- 实现init方法, 负责初始化, 包含如下字段:
- ip: 代理的IP地址
- port: 代理IP的端口号
- protocol: 代理IP支持的协议类型,http是0, https是1, https和http都支持是2
- nick_type: 代理IP的匿名程度, 高匿:0, 匿名: 1, 透明:0
- speed: 代理IP的响应速度, 单位s
- area: 代理IP所在地区
- score: 代理IP的评分, 默认分值可以通过配置文件进行配置. 在进行代理可用性检查的时候, 每遇到一次请求失败就减1份, 减到0的时候从池中删除. 如果检查代理可用, 就恢复默认分值
- disable_domains: 不可用域名列表, 有些代理IP在某些域名下不可用, 但是在其他域名下可用
创建配置文件: settings.py; 定义MAX_SCORE = 50,
# 代理模型类, 用于封装代理相关信息class Proxy(object):def __init__(self, ip, port, protocol=-1, nick_type=-1,speed=-1, area=None, score=50, disable_domains=[]):self.ip = ip # IPself.port = port # 端口号self.protocol = protocol # 代理IP支持协议类型,http是0, https是1, https和http都支持是2self.nick_type = nick_type # 匿名程度:高匿:0,匿名: 1, 透明:0self.speed = speed # 速度, 单位sself.area = area # 所在地区self.score = score # 代理IP的评分, 在进行代理可用性检查的时候, 每遇到一次请求失败就减1份, 减到0的时候从池中删除. 如果检查代理可用, 就恢复默认分值self.disable_domains = disable_domainsdef __str__(self):# 返回数据字符串return str(self.__dict__)
4. 代理池工具模块
4.1 日志模块
4.1.1 为什么要实现日志模块
能够方便的对程序进行调试
- 能够记录程序的运行状态
-
4.1.2. 日志的实现
目标: 实现日志模块, 用于记录日志
- 前提: 日志模块在网上有很多现成的实现, 我们开发的时候, 通常不会再自己写; 而是使用拿来主义. 拿来用就完了.
4.1.3 代码
把日志相关配置信息 放到配置文件中
```python# settings.pyimport logging# 默认的配置LOG_LEVEL = logging.INFO # 默认等级LOG_FMT = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s' # 默认日志格式LOG_DATEFMT = '%Y-%m-%d %H:%M:%S' # 默认时间格式LOG_FILENAME = 'log.log' # 默认日志文件名称
utils/log.py
import sys import logging
导入settings中日志配置信息
from settings import LOG_FMT, LOG_DATEFMT, LOG_FILENAME, LOG_LEVEL
class Logger(object):
def __init__(self):# 1. 获取一个logger对象self._logger = logging.getLogger()# 2. 设置format对象self.formatter = logging.Formatter(fmt=LOG_FMT,datefmt=LOG_DATEFMT)# 3. 设置日志输出# 3.1 设置文件日志模式self._logger.addHandler(self._get_file_handler(LOG_FILENAME))# 3.2 设置终端日志模式self._logger.addHandler(self._get_console_handler())# 4. 设置日志等级self._logger.setLevel(LOG_LEVEL)def _get_file_handler(self, filename):'''返回一个文件日志handler'''# 1. 获取一个文件日志handlerfilehandler = logging.FileHandler(filename=filename,encoding="utf-8")# 2. 设置日志格式filehandler.setFormatter(self.formatter)# 3. 返回return filehandlerdef _get_console_handler(self):'''返回一个输出到终端日志handler'''# 1. 获取一个输出到终端日志handlerconsole_handler = logging.StreamHandler(sys.stdout)# 2. 设置日志格式console_handler.setFormatter(self.formatter)# 3. 返回handlerreturn console_handler@propertydef logger(self):return self._logger
初始化并配一个logger对象,达到单例的
使用时,直接导入logger就可以使用
logger = Logger().logger
if name == ‘main‘: logger.debug(“调试信息”) logger.info(“状态信息”) logger.warning(“警告信息”) logger.error(“错误信息”) logger.critical(“严重错误信息”)
<a name="ABarc"></a>## 4.2 http模块在从代理IP网站上抓取代理IP 和 检验代理IP时候, 为了不容易不服务器识别为是一个爬虫, 我们最好提供随机的User-Agent请求头.- 目标: 获取随机User-Agent的请求头- 步骤:1. 准备User-Agent的列表1. 实现一个方法, 获取随机User-Agent的请求头- 代码:```pythonimport random# 1. 准备User-Agent的列表USER_AGENTS = ["Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)","Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)","Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)","Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)","Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)","Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)","Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6","Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1","Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0","Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20","Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)","Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1","Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5","Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre","Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11","Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"]# 实现一个方法, 获取随机User-Agent的请求头def get_request_headers():headers = {'User-Agent': random.choice(USER_AGENTS),'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Language': 'en-US,en;q=0.5','Connection': 'keep-alive','Accept-Encoding': 'gzip, deflate',}return headersif __name__ == '__main__':print(get_request_headers())print(get_request_headers())print(get_request_headers())
5. 代理IP的校验模块
- 目标: 检查代理IP速度, 支持的协议类型, 以及匿名程度.
- 步骤:
- 检查代理IP速度 和 匿名程度;
- 代理IP速度: 就是发送请求到获取响应的时间间隔
- 匿名程度检查:
- 对 http://httpbin.org/get 或 https://httpbin.org/get 发送请求
- 如果 origin 中有’,’分割的两个IP就是透明代理IP
- 如果 headers 中包含 Proxy-Connection 说明是匿名代理IP
- 否则就是高匿代理IP
- 检查代理IP协议类型
- 如果 http://httpbin.org/get 发送请求可以成功, 说明支持http协议
- 如果 https://httpbin.org/get 发送请求可以成功, 说明支持https协议
- 检查代理IP速度 和 匿名程度;
配置文件配置:
# settings.py
# 测试代理IP的超时时间
TEST_TIMEOUT = 10
import time
import requests
import json
from utils.http import get_request_headers
from settings import TEST_TIMEOUT
from utils.log import logger
from domain import Proxy
def check_proxy(proxy):
"""
用于检查指定 代理IP 响应速度, 匿名程度, 支持协议类型
:param proxy: 代理IP模型对象
:return: 检查后的代理IP模型对象
"""
# 准备代理IP字典
proxies = {
'http':'http://{}:{}'.format(proxy.ip, proxy.port),
'https':'https://{}:{}'.format(proxy.ip, proxy.port),
}
# 测试该代理IP
http, http_nick_type, http_speed = __check_http_proxies(proxies)
https, https_nick_type, https_speed = __check_http_proxies(proxies, False)
# 代理IP支持的协议类型, http是0, https是1, https和http都支持是2
if http and https:
proxy.protocol = 2
proxy.nick_type = http_nick_type
proxy.speed = http_speed
elif http:
proxy.protocol = 0
proxy.nick_type = http_nick_type
proxy.speed = http_speed
elif https:
proxy.protocol = 1
proxy.nick_type = https_nick_type
proxy.speed = https_speed
else:
proxy.protocol = -1
proxy.nick_type = -1
proxy.speed = -1
return proxy
def __check_http_proxies(proxies, is_http=True):
# 匿名类型: 高匿: 0, 匿名: 1, 透明: 2
nick_type = -1
# 响应速度, 单位s
speed = -1
if is_http:
test_url = 'http://httpbin.org/get'
else:
test_url = 'https://httpbin.org/get'
try:
# 获取开始时间
start = time.time()
# 发送请求, 获取响应数据
response = requests.get(test_url, headers=get_request_headers(), proxies=proxies, timeout=TEST_TIMEOUT)
if response.ok:
# 计算响应速度
speed = round(time.time() - start, 2)
# 匿名程度
# 把响应的json字符串, 转换为字典
dic = json.loads(response.text)
# 获取来源IP: origin
origin = dic['origin']
proxy_connection = dic['headers'].get('Proxy-Connection', None)
if ',' in origin:
# 1. 如果 响应的origin 中有','分割的两个IP就是透明代理IP
nick_type = 2
elif proxy_connection:
# 2. 如果 响应的headers 中包含 Proxy-Connection 说明是匿名代理IP
nick_type = 1
else:
# 3. 否则就是高匿代理IP
nick_type = 0
return True, nick_type, speed
return False, nick_type, speed
except Exception as ex:
# logger.exception(ex)
return False, nick_type, speed
if __name__ == '__main__':
proxy = Proxy('61.135.217.7', port='80')
print(check_proxy(proxy))
6. 代理池数据库模块
- 作用: 用于对proxies集合进行数据库的相关操作
- 目标: 实现对数据库增删改查相关操作
- 步骤:
- 在init中, 建立数据连接, 获取要操作的集合, 在 del 方法中关闭数据库连接
2.提供基础的增删改查功能
2.1 实现插入功能
2.2 实现修改该功能
2.3 实现删除代理: 根据代理的IP删除代理
2.4 查询所有代理IP的功能
3. 提供代理API模块使用的功能
3.1 实现查询功能: 根据条件进行查询, 可以指定查询数量, 先分数降序, 速度升序排, 保证优质的代理IP在上面.
3.2 实现根据协议类型 和 要访问网站的域名, 获取代理IP列表
3.3 实现根据协议类型 和 要访问网站的域名, 随机获取一个代理IP
3.4 实现把指定域名添加到指定IP的disable_domain列表中.
```python from pymongo import MongoClient import pymongo import randompython -m pip install -U pymongo
from settings import MONGO_URL from utils.log import logger
from domain import Proxy
class MongoPool(object):
def __init__(self):
# 1.1. 在init中, 建立数据连接
self.client = MongoClient(MONGO_URL)
# 1.2 获取要操作的集合
self.proxies = self.client['proxies_pool']['proxies']
def __del__(self):
# 1.3 关闭数据库连接
self.client.close()
def insert_one(self, proxy):
"""2.1 实现插入功能"""
count = self.proxies.count_documents({'_id': proxy.ip})
if count == 0:
# 我们使用proxy.ip作为, MongoDB中数据的主键: _id
dic = proxy.__dict__
dic['_id'] = proxy.ip
self.proxies.insert_one(dic)
logger.info('插入新的代理:{}'.format(proxy))
else:
logger.warning("已经存在的代理:{}".format(proxy))
def update_one(self, proxy):
"""2.2 实现修改该功能"""
self.proxies.update_one({'_id': proxy.ip}, {'$set':proxy.__dict__})
def delete_one(self, proxy):
"""2.3 实现删除代理: 根据代理的IP删除代理"""
self.proxies.delete_one({'_id': proxy.ip})
logger.info("删除代理IP: {}".format(proxy))
def find_all(self):
"""2.4 查询所有代理IP的功能"""
cursor = self.proxies.find()
for item in cursor:
# 删除_id这个key
item.pop('_id')
proxy = Proxy(**item)
yield proxy
def find(self, conditions={}, count=0):
"""
3.1 实现查询功能: 根据条件进行查询, 可以指定查询数量, 先分数降序, 速度升序排, 保证优质的代理IP在上面.
:param conditions: 查询条件字典
:param count: 限制最多取出多少个代理IP
:return: 返回满足要求代理IP(Proxy对象)列表
"""
cursor = self.proxies.find(conditions, limit=count).sort([
('score', pymongo.DESCENDING),('speed', pymongo.ASCENDING)
])
# 准备列表, 用于存储查询处理代理IP
proxy_list = []
# 遍历 cursor
for item in cursor:
item.pop('_id')
proxy = Proxy(**item)
proxy_list.append(proxy)
# 返回满足要求代理IP(Proxy对象)列表
return proxy_list
def get_proxies(self, protocol=None, domain=None, count=0, nick_type=0):
"""
3.2 实现根据协议类型 和 要访问网站的域名, 获取代理IP列表
:param protocol: 协议: http, https
:param domain: 域名: jd.com
:param count: 用于限制获取多个代理IP, 默认是获取所有的
:param nick_type: 匿名类型, 默认, 获取高匿的代理IP
:return: 满足要求代理IP的列表
"""
# 定义查询条件
conditions = {'nick_type': nick_type}
# 根据协议, 指定查询条件
if protocol is None:
# 如果没有传入协议类型, 返回支持http和https的代理IP
conditions['protocol'] = 2
elif protocol.lower() == 'http':
conditions['protocol'] = {'$in': [0, 2]}
else:
conditions['protocol'] = {'$in': [1, 2]}
if domain:
conditions['disable_domains'] = {'$nin': [domain]}
# 满足要求代理IP的列表
return self.find(conditions, count=count)
def random_proxy(self, protocol=None, domain=None, count=0, nick_type=0):
"""
3.3 实现根据协议类型 和 要访问网站的域名, 随机获取一个代理IP
:param protocol: 协议: http, https
:param domain: 域名: jd.com
:param count: 用于限制获取多个代理IP, 默认是获取所有的
:param nick_type: 匿名类型, 默认, 获取高匿的代理IP
:return: 满足要求的随机的一个代理IP(Proxy对象)
"""
proxy_list = self.get_proxies(protocol=protocol, domain=domain, count=count, nick_type=nick_type)
# 从proxy_list列表中, 随机取出一个代理IP返回
return random.choice(proxy_list)
def disable_domain(self, ip, domain):
"""
3.4 实现把指定域名添加到指定IP的disable_domain列表中.
:param ip: IP地址
:param domain: 域名
:return: 如果返回True, 就表示添加成功了, 返回False添加失败了
"""
# print(self.proxies.count_documents({'_id': ip, 'disable_domains':domain}))
if self.proxies.count_documents({'_id': ip, 'disable_domains':domain}) == 0:
# 如果disable_domains字段中没有这个域名, 才添加
self.proxies.update_one({'_id':ip}, {'$push': {'disable_domains': domain}})
return True
return False
if name == ‘main‘: mongo = MongoPool()
# proxy = Proxy('202.104.113.35', port='53281')
# proxy = Proxy('202.104.113.36', port='53281')
# mongo.insert_one(proxy)
# proxy = Proxy('202.104.113.35', port='8888')
# mongo.update_one(proxy)
# proxy = Proxy('202.104.113.35', port='8888')
# mongo.delete_one(proxy)
# for proxy in mongo.find_all():
# print(proxy)
# dic = { "ip" : "202.104.113.38", "port" : "53281", "protocol" : 0, "nick_type" : 0, "speed" : 8.2, "area" : None, "score" : 50, "disable_domains" : [ "jd.com"] }
# dic = { "ip" : "202.104.113.39", "port" : "53281", "protocol" : 1, "nick_type" : 0, "speed" : 1.2, "area" : None, "score" : 50, "disable_domains" : [ "taobao.com"] }
# dic = { "ip" : "202.104.113.40", "port" : "53281", "protocol" : 2, "nick_type" : 0, "speed" : 4.0, "area" : None, "score" : 50, "disable_domains" : []}
# dic = { "ip" : "202.104.113.41", "port" : "53281", "protocol" : 2, "nick_type" : 0, "speed" : -1, "area" : None, "score" : 49, "disable_domains" : []}
# proxy = Proxy(**dic)
# mongo.insert_one(proxy)
# for proxy in mongo.find():
# for proxy in mongo.find({'protocol':2}, count=1):
# print(proxy)
# for proxy in mongo.get_proxies(protocol='https'):
# for proxy in mongo.get_proxies(protocol='http', domain='taobao.com'):
# print(proxy)
# mongo.disable_domain('202.104.113.38', 'taobao.com')
<a name="MfYVd"></a>
# 7. 实现代理池的爬虫模块
<a name="zl89p"></a>
## 7.1 爬虫模块的需求
- 需求: 抓取各个代理IP网站上的免费代理IP, 进行检测, 如果可用存储到数据库中
- 需要抓取代理IP的页面如下:
- 西刺代理:[https://www.xicidaili.com/nn/1](https://www.xicidaili.com/nn/1)
- ip3366代理:[http://www.ip3366.net/free/?stype=1&page=1](http://www.ip3366.net/free/?stype=1&page=1)
- 快代理:[https://www.kuaidaili.com/free/inha/1/](https://www.kuaidaili.com/free/inha/1/)
- proxylistplus代理:[https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1](https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1)
- 66ip代理: [http://www.66ip.cn/1.html](http://www.66ip.cn/1.html)
<a name="eOPsH"></a>
## 7.2 爬虫模块的设计思路
- 通用爬虫:通过指定URL列表, 分组XPATH和组内XPATH, 来提取不同网站的代理IP
- 原因代理IP网站的页面结构几乎都是Table, 页面结构类似
- 具体爬虫: 用于抓取具体代理IP网站
- 通过继承通用爬虫实现具体网站的抓取, 一般只需要指定爬取的URL列表, 分组的XPATH和组内XPATH就可以了.
- 如果该网站有特殊反爬手段, 可以通过重写某些方法实现反爬
- 爬虫运行模块: 启动爬虫, 抓取代理IP, 进行检测, 如果可用, 就存储到数据库中;
- 通过配置文件来控制启动哪些爬虫, 增加扩展性; 如果将来我们遇到返回json格式的代理网站, 单独写一个爬虫配置下就好了.
<a name="NEEQ7"></a>
## 7.3 实现通用爬虫
- 目标: 实现可以指定不同URL列表, 分组的XPATH和详情的XPATH, 从不同页面上提取代理的IP,端口号和区域的通用爬虫;
- 步骤:
1. 在base_spider.py文件中,定义一个BaseSpider类, 继承object
1. 提供三个类成员变量:
- urls: 代理IP网址的URL的列表
- group_xpath: 分组XPATH, 获取包含代理IP信息标签列表的XPATH
- detail_xpath: 组内XPATH, 获取代理IP详情的信息XPATH, 格式为: {'ip':'xx', 'port':'xx', 'area':'xx'}
3. 提供初始方法, 传入爬虫URL列表, 分组XPATH, 详情(组内)XPATH
3. 对外提供一个获取代理IP的方法
- 遍历URL列表, 获取URL
- 根据发送请求, 获取页面数据
- 解析页面, 提取数据, 封装为Proxy对象
- 返回Proxy对象列表
```python
import requests
from utils.http import get_request_headers
from lxml import etree
from domain import Proxy
# 1. 在base_spider.py文件中,定义一个BaseSpider类, 继承object
class BaseSpider(object):
# 2. 提供三个类成员变量:
# urls: 代理IP网址的URL的列表
urls = []
# group_xpath: 分组XPATH, 获取包含代理IP信息标签列表的XPATH
group_xpath = ''
# detail_xpath: 组内XPATH, 获取代理IP详情的信息XPATH, 格式为: {'ip':'xx', 'port':'xx', 'area':'xx'}
detail_xpath = {}
# 3. 提供初始方法, 传入爬虫URL列表, 分组XPATH, 详情(组内)XPATH
def __init__(self, urls=[], group_xpath='', detail_xpath={}):
if urls:
self.urls = urls
if group_xpath:
self.group_xpath = group_xpath
if detail_xpath:
self.detail_xpath = detail_xpath
def get_page_from_url(self, url):
"""根据URL 发送请求, 获取页面数据"""
response = requests.get(url, headers=get_request_headers())
print(url)
print(response.status_code)
return response.content
def get_first_from_list(self, lis):
# 如果列表中有元素就返回第一个, 否则就返回空串
return lis[0] if len(lis) != 0 else ''
def get_proxies_from_page(self, page):
"""解析页面, 提取数据, 封装为Proxy对象"""
element = etree.HTML(page)
# 获取包含代理IP信息的标签列表
trs = element.xpath(self.group_xpath)
# 遍历trs, 获取代理IP相关信息
for tr in trs:
ip = self.get_first_from_list(tr.xpath(self.detail_xpath['ip']))
port = self.get_first_from_list(tr.xpath(self.detail_xpath['port']))
area = self.get_first_from_list(tr.xpath(self.detail_xpath['area']))
proxy = Proxy(ip, port, area=area)
# print(proxy)
# 使用yield返回提取到的数据
yield proxy
def get_proxies(self):
# 4. 对外提供一个获取代理IP的方法
# 4.1 遍历URL列表, 获取URL
for url in self.urls:
# print(url)
# 4.2 根据发送请求, 获取页面数据
page = self.get_page_from_url(url)
# 4.3 解析页面, 提取数据, 封装为Proxy对象
proxies = self.get_proxies_from_page(page)
# 4.4 返回Proxy对象列表
yield from proxies
if __name__ == '__main__':
config = {
'urls': ['http://www.ip3366.net/free/?stype=1&page={}'.format(i) for i in range(1, 4)],
'group_xpath': '//*[@id="list"]/table/tbody/tr',
'detail_xpath': {
'ip':'./td[1]/text()',
'port':'./td[2]/text()',
'area':'./td[5]/text()'
}
}
spider = BaseSpider(**config)
for proxy in spider.get_proxies():
print(proxy)
7.4 实现具体的爬虫类
- 目标: 通过继承通用爬虫, 实现多个具体爬虫, 分别从各个免费代理IP网站上抓取代理IP
- 步骤:
- 实现西刺代理爬虫: http://www.xicidaili.com/nn/1
- 定义一个类,继承通用爬虫类(BasicSpider)
- 提供urls, group_xpath 和 detail_xpath
- 实现ip3366代理爬虫: http://www.ip3366.net/free/?stype=1&page=1
- 定义一个类,继承通用爬虫类(BasicSpider)
- 提供urls, group_xpath 和 detail_xpath
- 实现快代理爬虫: https://www.kuaidaili.com/free/inha/1/
- 定义一个类,继承通用爬虫类(BasicSpider)
- 提供urls, group_xpath 和 detail_xpath
- 实现proxylistplus代理爬虫: https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1
- 定义一个类,继承通用爬虫类(BasicSpider)
- 提供urls, group_xpath 和 detail_xpath
- 实现66ip爬虫: http://www.66ip.cn/1.html
- 定义一个类,继承通用爬虫类(BasicSpider)
- 提供urls, group_xpath 和 detail_xpath
- 由于66ip网页进行js + cookie反爬, 需要重写父类的get_page_from_url方法 ```python import time import random import requests import re import js2py
- 实现西刺代理爬虫: http://www.xicidaili.com/nn/1
from core.proxy_spider.base_spider import BaseSpider from utils.http import get_request_headers
“””
- 实现西刺代理爬虫: http://www.xicidaili.com/nn/1 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “””
class XiciSpider(BaseSpider):
# 准备URL列表
urls = ['https://www.xicidaili.com/nn/{}'.format(i) for i in range(1, 11)]
# 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="ip_list"]/tr[position()>1]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
'ip':'./td[2]/text()',
'port':'./td[3]/text()',
'area':'./td[4]/a/text()'
}
“””
- 实现ip3366代理爬虫: http://www.ip3366.net/free/?stype=1&page=1
定义一个类,继承通用爬虫类(BasicSpider)
提供urls, group_xpath 和 detail_xpath
“””
class Ip3366Spider(BaseSpider):
准备URL列表
urls = [‘http://www.ip3366.net/free/?stype={}&page={}'.format(i, j) for i in range(1, 4, 2) for j in range(1, 8)]# 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = ‘//*[@id=”list”]/table/tbody/tr’组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
}'ip':'./td[1]/text()', 'port':'./td[2]/text()', 'area':'./td[5]/text()'
“””
实现快代理爬虫: https://www.kuaidaili.com/free/inha/1/ 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “”” class KaiSpider(BaseSpider):
准备URL列表
urls = [‘https://www.kuaidaili.com/free/inha/{}/'.format(i) for i in range(1, 6)]
# 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = ‘//*[@id=”list”]/table/tbody/tr’
组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
'ip':'./td[1]/text()', 'port':'./td[2]/text()', 'area':'./td[5]/text()'}
当我们两个页面访问时间间隔太短了, 就报错了; 这是一种反爬手段.
def get_page_from_url(self, url):
# 随机等待1,3s time.sleep(random.uniform(1, 3)) # 调用父类的方法, 发送请求, 获取响应数据 return super().get_page_from_url(url)
“””
- 实现proxylistplus代理爬虫: https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “””
class ProxylistplusSpider(BaseSpider):
# 准备URL列表
urls = ['https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-{}'.format(i) for i in range(1, 7)]
# # 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="page"]/table[2]/tr[position()>2]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
'ip':'./td[2]/text()',
'port':'./td[3]/text()',
'area':'./td[5]/text()'
}
“””
- 实现66ip爬虫: http://www.66ip.cn/1.html 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath 由于66ip网页进行js + cookie反爬, 需要重写父类的get_page_from_url方法 “””
class Ip66Spider(BaseSpider):
# 准备URL列表
urls = ['http://www.66ip.cn/{}.html'.format(i) for i in range(1, 11)]
# # 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="main"]/div/div[1]/table/tr[position()>1]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
'ip':'./td[1]/text()',
'port':'./td[2]/text()',
'area':'./td[3]/text()'
}
# 重写方法, 解决反爬问题
def get_page_from_url(self, url):
headers = get_request_headers()
response = requests.get(url, headers=headers)
if response.status_code == 521:
# 生成cookie信息, 再携带cookie发送请求
# 生成 `_ydclearance` cookie信息
# 1. 确定 _ydclearance 是从哪里来的;
# 观察发现: 这个cookie信息不使用通过服务器响应设置过来的; 那么他就是通过js生成.
# 2. 第一次发送请求的页面中, 有一个生成这个cookie的js; 执行这段js, 生成我们需要的cookie
# 这段js是经过加密处理后的js, 真正js在 "po" 中.
# 提取 `jp(107)` 调用函数的方法, 以及函数
result = re.findall('window.onload=setTimeout\("(.+?)", 200\);\s*(.+?)\s*</script> ', response.content.decode('GBK'))
# print(result)
# 我希望执行js时候, 返回真正要执行的js
# 把 `eval("qo=eval;qo(po);")` 替换为 return po
func_str = result[0][1]
func_str = func_str.replace('eval("qo=eval;qo(po);")', 'return po')
# print(func_str)
# 获取执行js的环境
context = js2py.EvalJs()
# 加载(执行) func_str
context.execute(func_str)
# 执行这个方法, 生成我们需要的js
# code = gv(50)
context.execute('code = {};'.format(result[0][0]))
# 打印最终生成的代码
# print(context.code)
cookie_str = re.findall("document.cookie='(.+?); ", context.code)[0]
# print(cookie_str)
headers['Cookie'] = cookie_str
response = requests.get(url, headers=headers)
return response.content.decode('GBK')
else:
return response.content.decode('GBK')
if name == ‘main‘:
# spider = XiciSpider()
# spider = Ip3366Spider()
# spider = KaiSpider()
# spider = ProxylistplusSpider()
spider = Ip66Spider()
for proxy in spider.get_proxies():
print(proxy)
# print(Ip3366Spider.urls)
# # 测试: http://www.66ip.cn/1.html
# url = 'http://www.66ip.cn/1.html'
# headers = {
# 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
# # 'Cookie': '_ydclearance=35fd4248c8889feb58597e27-a31e-4f84-9edc-f1a22c16a949-1546164684;'
# }
# response = requests.get(url, headers=headers)
# print(response.status_code)
# text = response.content.decode('GBK')
#
# # 生成 `_ydclearance` cookie信息
# # 1. 确定 _ydclearance 是从哪里来的;
# # 观察发现: 这个cookie信息不使用通过服务器响应设置过来的; 那么他就是通过js生成.
# # 2. 第一次发送请求的页面中, 有一个生成这个cookie的js; 执行这段js, 生成我们需要的cookie
# # 这段js是经过加密处理后的js, 真正js在 "po" 中.
# # 提取 `jp(107)` 调用函数的方法, 以及函数
# result = re.findall('window.onload=setTimeout\("(.+?)", 200\);\s*(.+?)\s*</script> ' ,text)
# # print(result)
# # 我希望执行js时候, 返回真正要执行的js
# # 把 `eval("qo=eval;qo(po);")` 替换为 return po
# func_str = result[0][1]
# func_str = func_str.replace('eval("qo=eval;qo(po);")', 'return po')
# # print(func_str)
# # 获取执行js的环境
# context = js2py.EvalJs()
# # 加载(执行) func_str
# context.execute(func_str)
# # 执行这个方法, 生成我们需要的js
# # code = gv(50)
# context.execute('code = {};'.format(result[0][0]))
# # 打印最终生成的代码
# # print(context.code)
# cookie_str = re.findall("document.cookie='(.+?); ", context.code)[0]
# # print(cookie_str)
# headers['Cookie'] = cookie_str
# response = requests.get(url, headers=headers)
# print(response.content.decode('GBK'))
<a name="q0xUO"></a>
## 7.5 实现运行爬虫模块
- 目标: 根据配置文件信息, 加载爬虫, 抓取代理IP, 进行校验, 如果可用, 写入到数据库中
- 思路:
- 在run_spider.py中, 创建RunSpider类
- 提供一个运行爬虫的run方法, 作为运行爬虫的入口, 实现核心的处理逻辑
- 根据配置文件信息, 获取爬虫对象列表.
- 获取爬虫对象, 遍历爬虫对象的get_proxies方法, 获取代理IP
- 检测代理IP(代理IP检测模块)
- 如果可用,写入数据库(数据库模块)
- 处理异常, 防止一个爬虫内部出错了, 影响其他的爬虫.
- 使用异步来执行每一个爬虫任务, 以提高抓取代理IP效率
- 在init方法中创建协程池对象
- 把处理一个代理爬虫的代码抽到一个方法
- 使用异步执行这个方法
- 调用**协程的join方法**, 让当前线程等待 协程 任务的完成.
- 使用schedule模块, 实现每隔一定的时间, 执行一次爬取任务
- 定义一个start的类方法
- 创建当前类的对象, 调用run方法
- 使用schedule模块, 每隔一定的时间, 执行当前对象的run方法
- 步骤:
- 在run_spider.py中, 创建RunSpider类
- 修改 setting.py 增加 代理IP爬虫的配置信息
```python
PROXIES_SPIDERS = [
# 爬虫的全类名,路径: 模块.类名
'core.proxy_spider.proxy_spiders.Ip66Spider',
'core.proxy_spider.proxy_spiders.Ip3366Spider',
'core.proxy_spider.proxy_spiders.KaiSpider',
'core.proxy_spider.proxy_spiders.ProxylistplusSpider',
'core.proxy_spider.proxy_spiders.XiciSpider',
]
实现根据配置文件, 加载爬虫, 把爬虫对象放到列表中的方法
- 定义一个列表, 用于存储爬虫对象
- 遍历爬虫配置信息, 获取每一个爬虫路径
- 根据爬虫路径获取模块名和类名
- 使用importlib根据模块名, 导入该模块
- 根据类名, 从模块中获取类
使用类创建对象, 添加到对象列表中
def _auto_import_instances(self): """根据配置信息, 自动导入爬虫""" instances = [] # 遍历配置的爬虫, 获取爬虫路径 for path in settings.PROXIES_SPIDERS: # 根据路径, 获取模块名 和 类名 module_name, cls_name = path.rsplit('.', maxsplit=1) # 根据模块名导入模块 module = importlib.import_module(module_name) # 根据类名, 从模块中, 获取爬虫类 cls = getattr(module, cls_name) # 创建爬虫对象, 添加到列表中 instances.append(cls()) # 返回爬虫对象列表 return instances实现run方法, 用于运行整个爬虫
获取代理爬虫列表
- 遍历代理爬虫列表
- 遍历爬虫的get_proxies()方法, 获取代理IP
- 如果代理IP为None, 继续一次循环
- 检查代理, 获取代理协议类型, 匿名程度, 和速度
- 如果代理速度不为-1, 就是说明该代理可用, 保存到数据库中
处理下异常, 防止一个爬虫内部错误, 其他爬虫都运行不了
class RunSpider(object): def __init__(self): self.proxy_pool = MongoPool() def run(self): """启动爬虫""" # 获取代理爬虫 spiders = self._auto_import_instances() # 执行爬虫获取代理 for spider in spiders: try: for proxy in spider.get_proxies(): if proxy is None: # 如果是None继续一个 continue # 检查代理, 获取代理协议类型, 匿名程度, 和速度 proxy = check_proxy(proxy) # 如果代理速度不为-1, 就是说明该代理可用 if proxy.speed != -1: # 保存该代理到数据库中 self.proxy_pool.save(proxy) except Exception as e: logger.exception(e) logger.exception("爬虫{} 出现错误".format(spider))使用协程池异步来运行每一个爬虫, 以提高爬取的速度
实现init方法, 创建协程池
- 把执行处理每一个爬虫的代码抽取一个方法
- 使用异步调用这个方法
调用协程的join方法, 让当前线程等待队列任务的完成.
class RunSpider(object): def __init__(self): self.proxy_pool = MongoPool() self.pool = Pool() ... def run(self): """启动爬虫""" # 获取代理爬虫 spiders = self._auto_import_instances() # 执行爬虫获取代理 for spider in spiders: # 使用协程异步调用该方法,提高爬取的效率 self.pool.apply_async(self.__run_one_spider, args=(spider, )) # 等待所有爬虫任务执行完毕 self.pool.join() def __run_one_spider(self, spider): try: for proxy in spider.get_proxies(): if proxy is None: # 如果是None继续一个 continue # 检查代理, 获取代理协议类型, 匿名程度, 和速度 proxy = check_proxy(proxy) # 如果代理速度不为-1, 就是说明该代理可用 if proxy.speed != -1: # 保存该代理到数据库中 self.proxy_pool.save(proxy) except Exception as e: logger.exception(e) logger.exception("爬虫{} 出现错误".format(spider))每隔一定的时间, 执行一次爬取任务
修改 setting.py 文件, 爬虫间隔时间的配置
# 抓取IP的时间间隔, 单位小时 SPIDER_INTERVAL = 2安装 schedule: pip install schedule
在 RunSpider 中提供start的类方法, 用于启动爬虫的运行, 每间隔指定时间, 重新运行一次.
@classmethod def start(cls): # 创建本类对象 run_spider = RunSpider() run_spider.run() # 每隔 SPIDER_INTERVAL 小时检查下代理是否可用 schedule.every(settings.SPIDER_INTERVAL).hours.do(run_spider.run) while True: schedule.run_pending() time.sleep(1)爬虫运行模块完整代码 ```python
打猴子补丁
from gevent import monkey monkey.patch_all()
导入协程池
from gevent.pool import Pool import importlib import schedule import time
from settings import PROXIES_SPIDERS from core.proxy_validate.httpbin_validator import check_proxy from core.db.mongo_pool import MongoPool from utils.log import logger from settings import RUN_SPIDERS_INTERVAL
class RunSpider(object):
def __init__(self):
# 创建MongoPool对象
self.mongo_pool = MongoPool()
# 3.1 在init方法中创建协程池对象
self.coroutine_pool = Pool()
def get_spider_from_settings(self):
"""根据配置文件信息, 获取爬虫对象列表."""
# 遍历配置文件中爬虫信息, 获取每个爬虫全类名
for full_class_name in PROXIES_SPIDERS:
# core.proxy_spider.proxy_spiders.Ip66Spider
# 获取模块名 和 类名
module_name, class_name = full_class_name.rsplit('.', maxsplit=1)
# 根据模块名, 导入模块
module = importlib.import_module(module_name)
# 根据类名, 从模块中, 获取类
cls = getattr(module, class_name)
# 创建爬虫对象
spider = cls()
# print(spider)
yield spider
def run(self):
# 2.1 根据配置文件信息, 获取爬虫对象列表.
spiders = self.get_spider_from_settings()
# 2.2 遍历爬虫对象列表, 获取爬虫对象, 遍历爬虫对象的get_proxies方法, 获取代理IP
for spider in spiders:
# 2.5 处理异常, 防止一个爬虫内部出错了, 影响其他的爬虫.
# 3.3 使用异步执行这个方法
# self.__execute_one_spider_task(spider)
self.coroutine_pool.apply_async(self.__execute_one_spider_task,args=(spider, ))
# 3.4 调用协程的join方法, 让当前线程等待 协程 任务的完成.
self.coroutine_pool.join()
def __execute_one_spider_task(self, spider):
# 3.2 把处理一个代理爬虫的代码抽到一个方法
# 用于处理一个爬虫任务的.
try:
# 遍历爬虫对象的get_proxies方法, 获取代理I
for proxy in spider.get_proxies():
# print(proxy)
# 2.3 检测代理IP(代理IP检测模块)
proxy = check_proxy(proxy)
# 2.4 如果可用,写入数据库(数据库模块)
# 如果speed不为-1, 就说明可用
if proxy.speed != -1:
# 写入数据库(数据库模块)
self.mongo_pool.insert_one(proxy)
except Exception as ex:
logger.exception(ex)
@classmethod
def start(cls):
# 4. 使用schedule模块, 实现每隔一定的时间, 执行一次爬取任务
# 4.1 定义一个start的类方法
# 4.2 创建当前类的对象, 调用run方法
rs = RunSpider()
rs.run()
# 4.3 使用schedule模块, 每隔一定的时间, 执行当前对象的run方法
# 4.3.1 修改配置文件, 增加爬虫运行时间间隔的配置, 单位为小时
schedule.every(RUN_SPIDERS_INTERVAL).hours.do(rs.run)
while True:
schedule.run_pending()
time.sleep(1)
if name == ‘main‘:
# rs = RunSpider()
# rs.run()
RunSpider.start()
# 测试schedule
# def task():
# print('呵呵')
#
# schedule.every(10).seconds.do(task)
# while True:
# schedule.run_pending()
# time.sleep(1)
<a name="WQN1K"></a>
# 8. 实现代理池的检测模块
- 目的: 检查代理IP可用性, 保证代理池中代理IP基本可用
- 思路
1. 在proxy_test.py中, 创建ProxyTester类
1. 提供一个 run 方法, 用于处理检测代理IP核心逻辑
1. 从数据库中获取所有代理IP
1. 遍历代理IP列表
1. 检查代理可用性
- 如果代理不可用, 让代理分数-1, 如果代理分数等于0就从数据库中删除该代理, 否则更新该代理IP
- 如果代理可用, 就恢复该代理的分数, 更新到数据库中
3. 为了提高检查的速度, 使用异步来执行检测任务
1. 把要检测的代理IP, 放到队列中
1. 把检查一个代理可用性的代码, 抽取到一个方法中; 从队列中获取代理IP, 进行检查; 检查完毕, 调度队列的task_done方法
1. 通过异步回调, 使用死循环不断执行这个方法,
1. 开启多个一个异步任务, 来处理代理IP的检测; 可以通过配置文件指定异步数量
4. 使用schedule模块, 每隔一定的时间, 执行一次检测任务
1. 定义类方法 start, 用于启动检测模块
1. 在start方法中
1. 创建本类对象
1. 调用run方法
1. 每间隔一定时间, 执行一下, run方法
- 步骤
- 在proxy_test.py中, 创建ProxyTester类
- 提供一个 run 方法, 用于检查代理IP的可用性
```python
import time
import requests
import json
from utils.http import get_request_headers
from settings import TEST_TIMEOUT
from utils.log import logger
from domain import Proxy
def check_proxy(proxy):
"""
用于检查指定 代理IP 响应速度, 匿名程度, 支持协议类型
:param proxy: 代理IP模型对象
:return: 检查后的代理IP模型对象
"""
# 准备代理IP字典
proxies = {
'http':'http://{}:{}'.format(proxy.ip, proxy.port),
'https':'https://{}:{}'.format(proxy.ip, proxy.port),
}
# 测试该代理IP
http, http_nick_type, http_speed = __check_http_proxies(proxies)
https, https_nick_type, https_speed = __check_http_proxies(proxies, False)
# 代理IP支持的协议类型, http是0, https是1, https和http都支持是2
if http and https:
proxy.protocol = 2
proxy.nick_type = http_nick_type
proxy.speed = http_speed
elif http:
proxy.protocol = 0
proxy.nick_type = http_nick_type
proxy.speed = http_speed
elif https:
proxy.protocol = 1
proxy.nick_type = https_nick_type
proxy.speed = https_speed
else:
proxy.protocol = -1
proxy.nick_type = -1
proxy.speed = -1
return proxy
def __check_http_proxies(proxies, is_http=True):
# 匿名类型: 高匿: 0, 匿名: 1, 透明: 2
nick_type = -1
# 响应速度, 单位s
speed = -1
if is_http:
test_url = 'http://httpbin.org/get'
else:
test_url = 'https://httpbin.org/get'
try:
# 获取开始时间
start = time.time()
# 发送请求, 获取响应数据
response = requests.get(test_url, headers=get_request_headers(), proxies=proxies, timeout=TEST_TIMEOUT)
if response.ok:
# 计算响应速度
speed = round(time.time() - start, 2)
# 匿名程度
# 把响应的json字符串, 转换为字典
dic = json.loads(response.text)
# 获取来源IP: origin
origin = dic['origin']
proxy_connection = dic['headers'].get('Proxy-Connection', None)
if ',' in origin:
# 1. 如果 响应的origin 中有','分割的两个IP就是透明代理IP
nick_type = 2
elif proxy_connection:
# 2. 如果 响应的headers 中包含 Proxy-Connection 说明是匿名代理IP
nick_type = 1
else:
# 3. 否则就是高匿代理IP
nick_type = 0
return True, nick_type, speed
return False, nick_type, speed
except Exception as ex:
# logger.exception(ex)
return False, nick_type, speed
if __name__ == '__main__':
# proxy = Proxy('202.104.113.35', port='53281')
proxy = Proxy('61.135.217.7', port='80')
print(check_proxy(proxy))
9. 实现代理池的API模块
- 目标:
- 为爬虫提供高可用代理IP的服务接口
- 步骤:
- 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
- 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
- 实现给指定的IP上追加不可用域名的服务
- 实现:
- 在proxy_api.py中, 创建ProxyApi类
- 实现初始方法
- 初始一个Flask的Web服务
- 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
- 可用通过 protocol 和 domain 参数对IP进行过滤
- protocol: 当前请求的协议类型
- domain: 当前请求域名
- 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
- 可用通过protocol 和 domain 参数对IP进行过滤
- 实现给指定的IP上追加不可用域名的服务
- 如果在获取IP的时候, 有指定域名参数, 将不在获取该IP, 从而进一步提高代理IP的可用性.
- 实现run方法, 用于启动Flask的WEB服务
- 实现start的类方法, 用于通过类名, 启动服务 ```python from flask import Flask from flask import request import json
from core.db.mongo_pool import MongoPool from settings import PROXIES_MAX_COUNT
1. 在proxy_api.py中, 创建ProxyApi类
class ProxyApi(object):
def __init__(self):
# 2. 实现初始方法
# 2.1 初始一个Flask的Web服务
self.app = Flask(__name__)
# 创建MongoPool对象, 用于操作数据库
self.mongo_pool = MongoPool()
@self.app.route('/random')
def random():
"""
2.2 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
可用通过 protocol 和 domain 参数对IP进行过滤
protocol: 当前请求的协议类型
domain: 当前请求域名
"""
protocol = request.args.get('protocol')
domain = request.args.get('domain')
proxy = self.mongo_pool.random_proxy(protocol, domain, count=PROXIES_MAX_COUNT)
if protocol:
return '{}://{}:{}'.format(protocol, proxy.ip, proxy.port)
else:
return '{}:{}'.format(proxy.ip, proxy.port)
@self.app.route('/proxies')
def proxies():
"""
2.3 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
可用通过protocol 和 domain 参数对IP进行过滤
实现给指定的IP上追加不可用域名的服务
"""
# 获取协议: http/https
protocol = request.args.get('protocol')
# 域名: 如:jd.com
domain = request.args.get('domain')
proxies = self.mongo_pool.get_proxies(protocol, domain, count=PROXIES_MAX_COUNT)
# proxies 是一个 Proxy对象的列表, 但是Proxy对象不能进行json序列化, 需要转换为字典列表
# 转换为字典列表
proxies = [proxy.__dict__ for proxy in proxies]
# 返回json格式值串
return json.dumps(proxies)
@self.app.route('/disable_domain')
def disable_domain():
"""
2.4 如果在获取IP的时候, 有指定域名参数, 将不在获取该IP, 从而进一步提高代理IP的可用性.
"""
ip = request.args.get('ip')
domain = request.args.get('domain')
if ip is None:
return '请提供ip参数'
if domain is None:
return '请提供域名domain参数'
self.mongo_pool.disable_domain(ip, domain)
return "{} 禁用域名 {} 成功".format(ip, domain)
def run(self):
"""3. 实现run方法, 用于启动Flask的WEB服务"""
self.app.run('0.0.0.0', port=16888)
@classmethod
def start(cls):
# 4. 实现start的类方法, 用于通过类名, 启动服务
proxy_api = cls()
proxy_api.run()
if name == ‘main‘:
# proxy_api = ProxyApi()
# proxy_api.run()
ProxyApi.start()
<a name="M4y1L"></a>
# 10. 实现代理池的启动入口
- 目标: 把启动爬虫, 启动检测代理IP, 启动WEB服务 统一到一起
- 思路:
- 开启三个进程, 分别用于启动爬虫, 检测代理IP, WEB服务
- 步骤:
- 定义一个run方法用于启动动代理池
- 定义一个列表, 用于存储要启动的进程
- 创建 启动爬虫 的进程, 添加到列表中
- 创建 启动检测 的进程, 添加到列表中
- 创建 启动提供API服务 的进程, 添加到列表中
- 遍历进程列表, 启动所有进程
- 遍历进程列表, 让主进程等待子进程的完成
- 在 if __name__ == '__main__': 中调用run方法
```python
from multiprocessing import Process
from core.proxy_spider.run_spiders import RunSpider
from core.proxy_test import ProxyTester
from core.proxy_api import ProxyApi
def run():
# 1. 定义一个列表, 用于存储要启动的进程
process_list = []
# 2. 创建 启动爬虫 的进程, 添加到列表中
process_list.append(Process(target=RunSpider.start))
# 3. 创建 启动检测 的进程, 添加到列表中
process_list.append(Process(target=ProxyTester.start))
# 4. 创建 启动提供API服务 的进程, 添加到列表中
process_list.append(Process(target=ProxyApi.start))
# 5. 遍历进程列表, 启动所有进程
for process in process_list:
# 设置守护进程
process.daemon = True
process.start()
# 6. 遍历进程列表, 让主进程等待子进程的完成
for process in process_list:
process.join()
if __name__ == '__main__':
run()

