文章目录
项目背景
在爬取一些大网站的时候,总会出现被反爬技术阻碍的情况,限制IP就是其中一种,那么使用代理就是很好的解决方案.
- 爬虫经常会用到代理IP, 高效使用这些IP是一个比较麻烦的事情。
- 代理池是爬虫、采集、爆破、刷单等必不可少的配备。
项目需求
- 需要监控ip是否过期,如果已经过期就从池中删除
- 监控访问目标网址的成功率,将成功率低的自动剔除
- 让ip池长期保持设定的ip数量,以便随时取用
项目技术细节
- 程序需要在服务端24小时运行
- 实时监控,默认2秒频率 apscheduler模块
- redis的有序集合积分板功能作为ip储存,所以程序最好是放于redis同服务器或者同内网上保障实时读写效率
- 提取IP的时候,有效存活时间过短的自动放弃(不入库)自动筛选(自行完成)
- 监控内容:
- 扫描每个IP过期时间,到期删除(自行完成)
- 总个数小于预设值就申请新的IP且值初始ip质量分=1
- ‘ip质量分’ + ‘到期时间戳’ 例如: 101556616966 后面10位是时间戳,前面是分数10(自行完成)。
注意: 分数加减只是对前面2位进行加减,后面10位是时间戳用来比对时间
代码实现
源码链接:https://github.com/MrDonkeykk/ProxyPoolManage
工具模块(utils.py)
模块介绍:
- 封装了getPage(获网页信息, 代理池为空时不使用代理),testProxyVaild(测试代理IP是否可用)的函数
# encoding=utf-8
"""
Date:2019-08-14 16:00
User:LiYu
Email:liyu_5498@163.com
FD:页面信息获取 IP测试工具
"""
import telnetlib
import requests
# colorama是一个python专门用来在控制台、命令行输出彩色文字的模块,可以跨平台使用。
from colorama import Fore
from fake_useragent import UserAgent
from db import RedisClient
ua = UserAgent()
def getPage(url):
print(Fore.GREEN + '[+] 正在抓取', url)
try:
headers = {
'User-Agent': ua.random
}
# 代理池为空时,第一次爬取可不使用代理IP,或者手动设置代理IP
redis = RedisClient()
if redis.count() == 0:
response = requests.get(url, headers=headers)
else:
proxyFromRedis = redis.random()
proxy = {
'https': proxyFromRedis,
'http': proxyFromRedis
}
response = requests.get(url, headers=headers, proxies=proxy)
response.raise_for_status()
response.encoding = response.apparent_encoding
except Exception as e:
print(Fore.RED + '[-] 抓取失败', url)
return ''
else:
print(Fore.GREEN + '[+] 抓取成功', url, response.status_code)
# print(response.text)
return response.text
def testProxyVaild(proxy):
"""
测试代理IP是否可用
:param proxy: ip:port
:return:
"""
ip, port = proxy.split(":")
try:
tn = telnetlib.Telnet(ip, int(port))
except Exception as e:
# print(Fore.RED + '[-] IP不可用', e)
return False
else:
return True
配置文件(config.py)
模块介绍:
程序中使用的变量设置
# encoding=utf-8
"""
Date:2019-08-14 16:44
User:LiYu
Email:liyu_5498@163.com
FD:配置信息
"""
# 爬取时测试代理IP的线程数
PROXY_THREAD_COUNT = 200
# 筛选代理IP的线程数
FILTER_THREAD_COUNT = 100
# 最大爬取页数
PAGES = 5
# Redis数据库地址
REDIS_HOST = '127.0.0.1'
# Redis端口
REDIS_PORT = 6379
# Redis密码,如无填None
REDIS_PASSWORD = None
REDIS_KEY = 'proxies_2'
# 代理分数
MAX_SCORE = 3
MIN_SCORE = 0
INITIAL_SCORE = 1
VALID_STATUS_CODES = [200, 302]
# 代理池数量界限
POOL_UPPER_THRESHOLD = 200
# 检查周期
TESTER_CYCLE = 20
# 获取周期
GETTER_CYCLE = 300
# 测试API,建议抓哪个网站测哪个
TEST_URL = 'http://www.baidu.com'
# API配置
API_HOST = '0.0.0.0'
API_PORT = 7777
# 开关
TESTER_ENABLED = True
GETTER_ENABLED = True
API_ENABLED = True
# 最大批测试量
BATCH_TEST_SIZE = 10
错误信息模块(errors.py)
模块介绍:
- 封装了PoolEmptyError(代理池枯竭)的类
# encoding=utf-8
"""
Date:2019-08-14 16:32
User:LiYu
Email:liyu_5498@163.com
FD:代理不足ERROR
"""
class PoolEmptyError(Exception):
def __init__(self):
Exception.__init__(self)
def __str__(self):
return repr('代理池已经枯竭')
数据库模块(db.py)
模块介绍:
- 使用redis有序列表存储代理IP,为IP设置分数值
- 封装了add(添加IP),random(随机弹出IP),decrease(代理值减一分,小于最小值则删除),drop(删除IP),max(将代理设置为MAX_SCORE),count(获取代理池中代理IP数量)等方法
# encoding=utf-8
"""
Date:2019-08-14 16:17
User:LiYu
Email:liyu_5498@163.com
FD:redis数据库方法封装
"""
import random
import re
import redis
from colorama import Fore
from config import *
from errors import PoolEmptyError
"""
db.py模块处在整个系统的中心位置,与其它的任意模块都有着紧密的联系。
该模块仅定义了一个RedisClient()类,该类定义了对Redis队列进行操作的几个通用方法(add,decrease等 )
"""
class RedisClient(object):
"""
存储数据到Redis数据库中
清空整个 Redis 服务器的数据:flushall
清空当前库中的所有 key:flushdb
"""
def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
"""
初始化
:param host: Redis 地址
:param port: Redis 端口
:param password: Redis密码
"""
# decode_responses=True:这样写存的数据是字符串格式
self.db = redis.StrictRedis(host=host,
port=port,
password=password,
decode_responses=True)
def add(self, proxy, score=INITIAL_SCORE):
"""
添加代理,设置分数为最高
:param proxy: 代理
:param score: 分数
:return: 添加结果
"""
if not re.match(r'\d+\.\d+\.\d+\.\d+:\d+', proxy):
print(Fore.RED + '代理不符合规范', proxy, '丢弃')
return
# zscore: 返回有序集 key 中,成员 member 的 score 值。
if not self.db.zscore(REDIS_KEY, proxy):
# Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。
# ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN
return self.db.zadd(REDIS_KEY, {proxy: score})
def random(self):
"""
随机获取有效代理,首先尝试获取最高分数代理,如果不存在,按照排名获取,否则异常
:return: 随机代理
"""
# Zrangebyscore 返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)依次排序排列。
result = self.db.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
if len(result):
return random.choice(result)
else:
# ZRANGE: 递增排列
# ZREVRANGE: 递减排列
result = self.db.zrevrange(REDIS_KEY, 0, 100)
if len(result):
return random.choice(result)
else:
raise PoolEmptyError
def decrease(self, proxy):
"""
代理值减一分,小于最小值则删除
:param proxy: 代理
:return: 修改后的代理分数
"""
# Redis Zscore 命令返回有序集中,成员的分数值。
score = self.db.zscore(REDIS_KEY, proxy)
if score and score > MIN_SCORE:
print(Fore.YELLOW + '代理', proxy, '当前分数', score, '减1')
# zincrby 对有序集合中指定成员的分数加上增量 increment, 正数是增加, 负数是减少;
return self.db.zincrby(REDIS_KEY, proxy, -1)
else:
print(Fore.RED + '代理', proxy, '当前分数', score, '移除')
# zrem: 用于移除有序集中的一个或多个成员,不存在的成员将被忽略。
return self.db.zrem(REDIS_KEY, proxy)
def exists(self, proxy):
"""
判断是否存在
:param proxy: 代理
:return: 是否存在
"""
return not self.db.zscore(REDIS_KEY, proxy) == None
def max(self, proxy):
"""
将代理设置为MAX_SCORE
:param proxy: 代理
:return: 设置结果
"""
print(Fore.GREEN + '代理', proxy, '可用,设置为', MAX_SCORE)
return self.db.zadd(REDIS_KEY, MAX_SCORE, proxy)
def count(self):
"""
获取数量
:return: 数量
"""
# zard 命令用于计算集合中元素的数量。
return self.db.zcard(REDIS_KEY)
def all(self):
"""
获取全部代理
:return: 全部代理列表
"""
return self.db.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
def batch(self, count=5, start=0, stop=None):
"""
批量获取
:param start: 开始索引
:param stop: 结束索引
:return: 代理列表
"""
if not stop: stop = start + count
return self.db.zrevrange(REDIS_KEY, start, stop)
def drop(self, proxy):
self.db.zrem(REDIS_KEY, proxy)
爬虫模块(spider.py)
模块介绍:
- 自定义元类创建爬虫类,使用new魔术方法添加两个属性,一个保存所有的爬取网站的方法名(目前只有西刺代理网站),一个保存爬取网站方法的数量,爬取时运行getProxies方法即可依次自动运行所有的爬取方法,返回生成器。
- 获取代理储存到代理池,需要实例化redisClient对象,和Crawler对象,先调用crawler的getProxies方法获取IP,在检测IP可用后,使用redis的add方法添加到数据库,其中检测IP是否可用使用线程池完成
# encoding=utf-8
"""
Date:2019-08-14 14:37
User:LiYu
Email:liyu_5498@163.com
FD:爬虫模块,包含 爬取,存储到代理池
"""
import re
import sys
from config import POOL_UPPER_THRESHOLD, PAGES, PROXY_THREAD_COUNT
from concurrent.futures import ThreadPoolExecutor
from utils import *
class ProxyMetaClass(type):
"""
自定义元类,获取目标类中所有爬取IP的函数信息
"""
def __new__(cls, name, bases, attrs):
"""
:param name: Crawler, 被装饰的类名
:param bases: (<class 'object'>,), 被装饰的类的父类
:param attrs:{'属性名': '属性值', '方法名': '方法对象'}, 被装饰的类的详细信息
:return:
"""
count = 0
# 给被装饰的类新增两个属性
attrs['__CrawlFunc__'] = []
for k, v in attrs.items():
if 'crawl' in k:
attrs['__CrawlFunc__'].append(k)
count += 1
attrs['__CrawlFuncCount__'] = count
return type.__new__(cls, name, bases, attrs)
class Crawler(object, metaclass=ProxyMetaClass):
def getProxies(self, callback):
# print(callback)
for proxy in eval("self.{}()".format(callback)):
yield proxy
def crawlXicidaili(self):
for page in range(1, PAGES + 1):
startUrl = 'https://www.xicidaili.com/nt/%s' % page
html = getPage(startUrl)
if html:
findtrs = re.compile(r'<tr class=.*?>(.*?)</tr>', re.S)
trs = findtrs.findall(html)
findip = re.compile(r'<td>(\d+\.\d+\.\d+\.\d+)</td>')
findport = re.compile(r'<td>(\d+)</td>')
for tr in trs:
# print(tr)
ip = findip.findall(tr)[0]
port = findport.findall(tr)[0]
addressPort = ip + ':' + port
yield addressPort
else:
yield ''
class PoolGetter(object):
def __init__(self):
self.redis = RedisClient()
self.crawler = Crawler()
def isOverThreshold(self):
"""
判断是否达到了代理池限制
"""
if self.redis.count() >= POOL_UPPER_THRESHOLD:
return True
else:
return False
def testProxyAdd(self, proxy):
"""检测是否可用, 可用添加到redis中"""
if testProxyVaild(proxy):
print(Fore.GREEN + '成功获取到代理', proxy)
self.redis.add(proxy)
def run(self):
print(Fore.GREEN + "[-] 代理池获取器开始执行......")
if not self.isOverThreshold():
for callbackLabel in range(self.crawler.__CrawlFuncCount__):
callback = self.crawler.__CrawlFunc__[callbackLabel]
# 获取代理
proxies = self.crawler.getProxies(callback)
# 刷新输出
sys.stdout.flush()
with ThreadPoolExecutor(PROXY_THREAD_COUNT) as pool:
pool.map(self.testProxyAdd, proxies)
def isPoolGetterOK():
pool = PoolGetter()
pool.run()
print(pool.redis.count())
def isCrawlerOK():
"""
测试爬虫代码
:return:
"""
crawler = Crawler()
# print(crawler.__CrawlFuncCount__)
for callbackLabel in range(crawler.__CrawlFuncCount__):
# print(callbackLabel)
callback = crawler.__CrawlFunc__[callbackLabel]
proxies = crawler.getProxies(callback)
# print(proxies)
for i in proxies:
print(i)
if __name__ == '__main__':
# isCrawlerOK()
isPoolGetterOK()
代理池过滤模块(ProxyPoolFilter.py)
模块介绍:
实例化一个RedisClient对象,封装了testSingleProxy方法(测试单个代理,可用则代理值设置为最大值,不可用则从redis数据库删除),任务使用线程池完成。
# encoding=utf-8
"""
Date:2019-08-15 09:19
User:LiYu
Email:liyu_5498@163.com
FD:检测代理池内代理IP状态
"""
from colorama import Fore
from db import RedisClient, FILTER_THREAD_COUNT
from utils import testProxyVaild
from concurrent.futures import ThreadPoolExecutor
class PoolTester(object):
def __init__(self):
self.redis = RedisClient()
def testSingleProxy(self, proxy):
"""
测试单个代理
:param proxy:
:return:
"""
if testProxyVaild(proxy):
self.redis.max(proxy)
print(Fore.GREEN + "[+] 代理可用", proxy)
else:
self.redis.drop(proxy)
print(Fore.RED + "[-] 代理不可用", proxy)
def run(self):
"""
测试的主函数
:return:
"""
print(Fore.GREEN + "测试器开始运行.......")
try:
count = self.redis.count()
print(Fore.GREEN + "当前剩余%d个代理" % count)
# 使用线程池, 快速检测proxy是否可用
with ThreadPoolExecutor(FILTER_THREAD_COUNT) as pool:
pool.map(self.testSingleProxy, self.redis.all())
except Exception as e:
print(Fore.RED + "测试器发生错误", e)
if __name__ == '__main__':
tester = PoolTester()
tester.run()
API模块(api.py)
模块介绍:
- 部署API,让用户可以通过API接口获取代理IP,查看代理池中代理数量
# encoding=utf-8
"""
Date:2019-08-15 11:08
User:LiYu
Email:liyu_5498@163.com
FD:API部署
"""
from flask import Flask
from config import API_HOST, API_PORT
from db import RedisClient
app = Flask(__name__)
@app.route('/')
def index():
html = """
<h1 style='color: green'>欢迎来到代理池监控维护器</h1>
<hr/>
<ul>
<li><a href= "/getProxy">获取代理IP</a></li>
<li><a href= "/count">代理IP个数</a></li>
</ul>
"""
return html
@app.route('/getProxy')
def getProxy():
return RedisClient().random()
@app.route('/count/')
def count():
return str(RedisClient().count())
if __name__ == '__main__':
app.run(host=API_HOST, port=API_PORT)
定时任务模块(scheduler.py)
模块介绍:
- 封装了scheduleTester方法(实例化PoolTester对象,每隔TESTER_CYCLE时间过滤一次代理池,保证池中代理随时可用)
- 封装了scheduleGetter方法(实例化PoolGetter对象,每隔GETTER_CYCLE时间获取一次代理补充代理池)
- 封装了scheduleApi方法(开启API)
# encoding=utf-8
"""
Date:2019-08-15 10:08
User:LiYu
Email:liyu_5498@163.com
FD:定时任务 爬取 测试 API
"""
import time
from multiprocessing import Process
from ProxyPoolFilter import PoolTester
from api import app
from config import TESTER_CYCLE, GETTER_CYCLE, API_HOST, API_PORT, API_ENABLED, TESTER_ENABLED, GETTER_ENABLED
from spider import PoolGetter
class Scheduler():
def scheduleTester(self, cycle=TESTER_CYCLE):
"""定时检测代理IP是否可用?"""
tester = PoolTester()
while True:
tester.run()
# 每隔指定时间进行测试
time.sleep(cycle)
def scheduleGetter(self, cycle=GETTER_CYCLE):
"""
定期获取代理
:param cycle:
:return:
"""
getter = PoolGetter()
while True:
print("开始抓取代理")
getter.run()
time.sleep(cycle)
def scheduleApi(self):
"""
开启API
"""
app.run(API_HOST, API_PORT)
def run(self):
print("代理池开始运行......")
if API_ENABLED:
print("正在启动API........")
api_process = Process(target=self.scheduleApi())
api_process.start()
if TESTER_ENABLED:
print("正在启动TESTER.......")
test_process = Process(target=self.scheduleTester)
test_process.start()
if GETTER_ENABLED:
print("正在启动GETTER......")
getter_process = Process(target=self.scheduleGetter)
getter_process.start()
if __name__ == '__main__':
scheduler = Scheduler()
scheduler.run()
程序入口(run.py)
模块介绍:
- 整个程序的入口,实例化Scheduler对象,执行定时任务
# encoding=utf-8
"""
Date:2019-08-15 11:28
User:LiYu
Email:liyu_5498@163.com
FD:程序入口
"""
from scheduler import Scheduler
if __name__ == '__main__':
scheduler = Scheduler()
scheduler.run()