本文章案例仅供参考
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
一、代理池概述
1.什么是代理池?
代理池就是有代理IP组成的池子, 它可以提供多个稳定可用的代理IP
2.为什么要实现代理池?
- 我们在做爬虫的时候, 最常见一种反爬手段就是
ip反爬
; 也就是当同一个IP访问这个网站次数过多, 频率过高,就会限制这个IP访问. 怎么解决这个问题呢? 就是需要经常换IP; 使用代理IP是其中一个比较常用的方案。 - 免费代理都是非常不稳定的, 有10%是可用就很不错了。
- 一些收费代理稳定性也不好, 便宜一点只有30%~50%左右是可用。 注: 如果代理IP提供商, 提供接口很好, 稳定性也很高, 就无需使用代理池
3.代理池开发环境
平台: Mac,可以运行Window和Linux上
开发语言: Python3
开发工具: PyCharm
使用到的主要技术:
requests: 发送请求, 获取页面数据
lxml: 使用XPATH从页面提取我们想要的数据
pymongo: 把提取到代理IP存储到MongoDB数据库中和从MongoDB数据库中读取代理IP,给爬虫使用.
Flask: 用于提供WEB服务
二、代理池设计
1.代理池五大核心模块
1.1 数据库模块
实现对代理ip的增删改查操作,这里使用MongoDB来存储代理IP
1.2 代理IP的检测模块
- 测试代理速度, 支持的协议以及匿名程度 ,原因: 网站上所标注的协议类型和匿名类型是不准确的
- 这里使用httpbin.org进行检测
1.3 爬虫模块
- 从代理ip网站上采集代理ip
- 进行校验(获取代理响应速度,协议类型,匿名类型)
- 把可用代理ip存储到数据库中
1.4 检验模块
- 从数据库读取所有的代理ip
- 对代理ip进行逐一检测,可用开启多个协程
- 以提高检测速度 依据检测结果对代理ip进行更新
2.代理池的其他模块
程序启动入口: `main.py` 代理池提供一个统一的启动入口
数据模型: `domain.py`: 代理IP的数据模型
工具模块:
1.日志模块: 用于记录日志信息
2.http模块: 用于获取随机User-Agent的请求头
配置文件: `settings.py` 用于配置日志, 启动的爬虫, 启用的代理IP检验类等
3.代理池的项目结构
-- IPProxyPool
-- db
-- __init__.py
-- mongo_pool.py
-- proxy_validate
-- __init__.py
-- httpbin_validator.py
-- proxy_spider
-- __init__.py
-- base_spider.py
-- proxy_spiders.py
-- run_spiders.py
-- utils
-- __init__.py
-- http.py
-- log.py
-- proxy_test.py
-- proxy_api.py
-- main.py
-- domain.py
-- settings.py
4.定义代理池ip的数据模型类
用于封装代理信息
步骤:
- 定义一个类, 继承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 #ip
self.port=port #端口号
self.protocol=protocol #代理IP支持协议类型,HTTP是0,HTTP是1,https和http是支持2
self.nick_type=nick_type #匿名程度:高匿0 匿名1 透明0
self.area=area #所在地区
self.speed=speed #速度 单位s
self.score=score #代理ip评分 在进行代理可用性检查的时候, 每遇到一次请求失败就减1份, 减到0的时候从池中删除. 如果检查代理可用, 就恢复默认分值
self.disable_domains=disable_domains
def __str__(self):
#返回数字字符串
return str(self.__dict__)
5.代理池工具模块
5.1 日志模块
为什么要实现日志模块?
- 能够方便对程序进行调试
- 能够记录程序的运行状态
- 记录错误信息
日志的实现?
步骤:
- 拷贝笔记日志代码到项目中
- 把日志相关配置信息 放到配置文件中
- 修改日志代码, 使用配置文件中的配置信息
代码:
import sys
import 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' # 默认日志文件名称
class Logger(object):
def __init__(self):
#1.获取一个logger对象
self._logger=logging.getLogger()
#2.设置formater对象
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. 获取一个文件日志handler
filehandler=logging.FileHandler(filename=filename,encoding="utf-8")
#2.设置日志格式
filehandler.setFormatter(self.formatter)
#3.返回
return filehandler
def _get_console_handler(self):
'''返回一个输出到终端日志handler'''
# 1. 获取一个输出到终端日志handler
console_handler=logging.StreamHandler(sys.stdout)
#2.设置日志格式
console_handler.setFormatter(self.formatter)
#3.返回handler
return console_handler
@property
def logger(self):
return self._logger
# 初始化并配一个logger对象,达到单例的
# 使用时,直接导入logger就可以使用
logger=Logger().logger
if __name__ == '__main__':
logger.debug("调试信息")
logger.info("状态信息")
logger.warning("警告信息")
logger.error("错误信息")
2022-12-17 19:52:41 日志.py [line:53] INFO: 状态信息
2022-12-17 19:52:41 日志.py [line:54] WARNING: 警告信息
2022-12-17 19:52:41 日志.py [line:55] ERROR: 错误信息
5.2 Http模块
我在从代理IP网站上抓取代理IP
和 检验代理IP
时候, 为了不容易不服务器识别为是一个爬虫, 我们最好提供随机的User-Agent请求头.
获取随机User-Agent的请求头
步骤:
- 1. 准备User-Agent的列表
2. 实现一个方法, 获取随机User-Agent的请求头
代码:
import random
user_agent=[
"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"
]
def get_request_header():
return {
"User-Agent":random.choice(user_agent),
"Connection":"keep-alive",
"Cache-Control":"max-age=0",
"Upgrade-Insecure-Requests":"1",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding":"gzip,deflate",
"Accept-Language":'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7',
}
if __name__ == '__main__':
print(get_request_header())
print(get_request_header())
6.代理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协议
import requests
import time
import json
from utils import http
import settings
from domain import Proxy
from utils.log import logger
def check_proxy(proxy):
'''
检测代理协议类型, 匿名程度
:param
:return:(协议: http和https:2,https:1,http:0, 匿名程度:高匿:0,匿名: 1, 透明:0 , 速度, 单位s )
'''
# 根据proxy对象构造, 请求使用的代理
proxies = {
'http': "http://{}:{}".format(proxy.ip, proxy.port),
'https':"https://{}:{}".format(proxy.ip, proxy.port),
}
http, nick_type, http_speed = _check_http_proxy(proxies)
https, nick_type, https_speed = _check_http_proxy(proxies, False)
if http and https:
# 如果http 和 https 都可以请求成功, 说明支持http也支持https, 协议类型为2
proxy.protocol = 2
proxy.nick_type = nick_type
proxy.speed = http_speed
elif http:
# 如果只有http可以请求成功, 说明支持http协议, 协议类型为 0
proxy.protocol = 0
proxy.nick_type = nick_type
proxy.speed = http_speed
elif https:
# # 如果只有https可以请求成功, 说明支持https协议, 协议类型为 1
proxy.protocol = 1
proxy.nick_type = nick_type
proxy.speed = https_speed
else:
proxy.protocol = -1
proxy.nick_type = -1
proxy.speed = -1
logger.debug(proxy)
return proxy
def _check_http_proxy(proxies, isHttp=True):
nick_type = -1 # 匿名程度
speed = -1 # 响应速度
if isHttp:
test_url = 'http://httpbin.org/get'
else:
test_url = 'https://httpbin.org/get'
try:
start = time.time()
r = requests.get(url=test_url, headers=http.get_request_header(), timeout=settings.TIMEOUT, proxies=proxies)
if r.ok:
# 计算响应速度, 保留两位小数
speed = round(time.time() - start, 2)
# 把响应内容转换为字典
content = json.loads(r.text)
# 获取请求头
headers = content['headers']
# 获取origin, 请求来源的IP地址
ip = content['origin']
# 获取请求头中 `Proxy-Connection` 如果有, 说明匿名代理
proxy_connection = headers.get('Proxy-Connection', None)
if ',' in ip:
# 如果 `origin` 中有','分割的两个IP就是透明代理IP
nick_type = 2 # 透明
elif proxy_connection:
# 如果 `headers` 中包含 `Proxy-Connection` 说明是匿名代理IP
nick_type = 1 # 匿名
else:
# 否则就是高匿代理IP
nick_type = 0 # 高匿
return True, nick_type, speed
else:
return False, nick_type, speed
except Exception as e:
logger.exception(e)
return False, nick_type, speed
if __name__ == '__main__':
proxy = Proxy('118.190.95.35', '9001')
# proxy = Proxy('150.107.143.33', '9797')
rs = check_proxy(proxy)
print(proxy.protocol)
print(rs)
7.代理池数据库模块
用于对proxies
集合进行数据库的相关操作,实现对数据库增删改查相关操作
步骤:
- 1. 定义MongoPool类, 继承object
2. 实现初始化方法, 建立数据连接, 获取要操作的集合
3. 实现插入功能
4. 实现修改该功能
5. 实现保存功能, 如果不存在插入, 如果存在了就更新
6. 实现查询功能: 根据条件进行查询, 可以指定查询数量, 先分数降序, 速度升序排, 保证优质的代理IP在上面.
7. 实现删除代理: 根据代理的IP删除代理
8. 实现根据协议类型 和 要访问网站的域名, 获取代理IP列表
9. 实现根据协议类型 和 要访问完整的域名, 随机获取一个代理IP
import pymongo
import random
from settings import MONGO_URL, DEFAULT_SCORE
from domain import Proxy
from utils.log import logger
class MongoPool(object):
def __init__(self):
"""初始化"""
self.client = pymongo.MongoClient(MONGO_URL)
# 获取要操作的集合
self.proxies = self.client['proxy_pool']['proxies']
def insert(self, proxy=None):
"""保存代理IP到数据库中"""
if proxy:
# 把Proxy对象转换为转换为字典
dic = proxy.__dict__
# 设置_id字段, 为代理IP的IP地址
dic['_id'] = proxy.ip
# 向集合中插入代理IP
self.proxies.insert_one(dic)
# 记录日志
logger.info("插入新代理IP:{}".format(dic))
else:
logger.error("没有传入要插入的proxy")
def update(self, proxy=None):
"""更新代理"""
if proxy:
self.proxies.update_one({'_id':proxy.ip}, {"$set": proxy.__dict__})
logger.info("更新代理: {}".format(proxy))
else:
logger.error("请求传入要更新的代理")
def save(self, proxy):
""" 保存代理信息: 如果代理IP不存在就插入, 存在就更新"""
# 1. 根据IP查询代理IP数量, 如果数量为0 , 说明该代理IP是新的, 否则该代理IP已经存在
count = self.proxies.count_documents({'_id': proxy.ip})
# 如果如果代理IP不存在就插入, 否则, 更新原来的代理IP信息
if count == 0:
self.insert(proxy)
else:
self.update(proxy)
def find(self, conditions=None, count=0):
"""
根据条件查询代理IP
:param conditions: # 字典形式的查询条件
:param count: 查询多少条数据
:return: 返回先按分数降序, 后按响应速度升序排列前count条数据, 如果count==0, 就查询所有的,
"""
# 如果没有conditions, 将conditions设置为{}
if conditions is None:
conditions = {}
# 获取查询的游标地下
cursor = self.proxies.find(conditions, limit=count).sort(
[("score", pymongo.DESCENDING), ("speed", pymongo.ASCENDING)])
# 创建一个list, 用于存储Proxy
results = []
# 变量游标, 获取代理IP
for item in cursor:
# 创建Proxy对象
proxy = Proxy(item['ip'], item['port'],
score=item['score'], protocol=item['protocol'],
nick_type=item['nick_type'], speed=item['speed'],
disable_domains=item['disable_domains'])
# 把Proxy对象添加到结果集
results.append(proxy)
# 返回查询的结果
return results
def delete(self, proxy=None):
"""根据条件删除代理IP"""
if proxy:
self.proxies.delete_one({'_id': proxy.ip})
logger.info('删除代理: {}'.format(proxy))
else:
logger.error("请传入要删除的代理")
def get_proxies(self, protocol=None, domain=None, count=0, nick_type=0):
"""
根据协议类型, 获取代理IP, 默认查询都是高匿的
:param protocol: 协议: http 或 https
:param domain: 要访问网站的域名
:param count: 代理IP的数量, 默认全部
:param nick_type: 匿名程度, 默认为高匿
:return:
"""
conditions = {'nick_type': nick_type}
if domain:
# 如果有域名, 就获取不可用域名中, 没有该域名的代理
conditions['disable_domains'] = {'$nin':[domain]}
if protocol is None:
# 如果没有协议, 就获取及支持http 又支持https的协议
conditions['protocol'] = 2
elif protocol.lower() == 'http':
# 如果是HTTP的请求, 使用支持http 和 支持http/https均可以
conditions['protocol'] = {'$in': [2, 0]}
elif protocol.lower() == 'https':
# 如果是HTTP的请求, 使用支持https 和 支持http/https均可以
conditions["protocol"] = {'$in': [2, 1]}
return self.find(conditions, count=count)
def random(self, protocol=None, domain=None, count=0):
"""
从指定数量代理IP中, 随机获取一个
:param protocol: 协议: http 或 https
:param domain: 要访问网站的域名
:param count: 代理IP的数量
:return: 一个随机获取代理IP
"""
proxy_list = self.get_proxies(protocol, domain=domain, count=count)
return random.choice(proxy_list)
# 创建MongoPool的单例对象
mongo = MongoPool()
if __name__ == '__main__':
# 1. 保存代理IP
# proxy = Proxy('124.89.97.43', '80', protocol=1, nick_type=0, speed=0.36, area='陕西省商洛市商州区')
# proxy = Proxy('124.89.97.43', '80', protocol=2, nick_type=0, speed=0.36, area='陕西省商洛市商州区')
# proxy = Proxy('124.89.97.44', '80', protocol=1, nick_type=0, speed=0.36, area='陕西省商洛市商州区')
# proxy = Proxy('124.89.97.45', '80', protocol=0, nick_type=0, speed=0.36, area='陕西省商洛市商州区')
# proxy = Proxy('124.89.97.46', '80', protocol=0, nick_type=2, speed=0.36, area='陕西省商洛市商州区')
# mongo.save(proxy)
# 3. 测试减少代理分值
# mongo.decrease_score(proxy)
# 4. 恢复
# proxy = Proxy('124.89.97.43', '80', protocol=2, nick_type=0, speed=0.36, area='陕西省商洛市商州区', score=1)
# mongo.resume_score(proxy)
# 5. 获取大理IP列表
# proxies = mongo.get_proxies('http')
# proxies = mongo.get_proxies('https')
# 随机获取一个代理IP
# proxy = mongo.random('http')
# proxy = mongo.random('https')
# print(proxy)
# 删除代理IP
proxy = Proxy('124.89.97.46', '80', protocol=0, nick_type=2, speed=0.36, area='陕西省商洛市商州区')
mongo.delete(proxy)
# 2. 查询高匿代理IP
proxies = mongo.find({'nick_type': 0})
for proxy in proxies:
print(proxy)