项目背景
在爬取一些大网站的时候,总会出现被反爬技术阻碍的情况,限制IP就是其中一种,那么使用代理就是很好的解决方案.
- 爬虫经常会用到代理IP, 高效使用这些IP是一个比较麻烦的事情。
- 代理池是爬虫、采集、爆破、刷单等必不可少的配备。
需求分析:
我们在做上个拉钩网招聘信息项目的时候,如果爬取的信息过多,就会被禁止访问。为了防止这种情况的发生。我们再次爬取一些能够使用的代理IP。做一个能够长远使用的代理IP池。用的时候从中获取就可以了。
项目需求
- 需要监控ip是否过期,如果已经过期就从池中删除
- 监控访问目标网址的成功率,将成功率低的自动剔除
- 让ip池长期保持设定的ip数量,以便随时取用
项目技术细节
- 程序需要在服务端24小时运行
- 实时监控,默认2秒频率 apscheduler模块
- redis的有序集合积分板功能作为ip储存,所以程序最好是放于redis同服务器或者同内网上保障实时读写效率
- 提取IP的时候,有效存活时间过短的自动放弃(不入库)自动筛选(自行完成)
- 监控内容:
- 扫描每个IP过期时间,到期删除(自行完成)
- 总个数小于预设值就申请新的IP且值初始ip质量分=1
- ‘ip质量分’ + ‘到期时间戳’ 例如: 101556616966 后面10位是时间戳,前面是分数10(自行完成)。
注意: 分数加减只是对前面2位进行加减,后面10位是时间戳用来比对时间
核心代码
首先我们要获取很多个可用的IP地址。去爬取https://www.xicidaili.com/网页的代理IP。
爬取的时候需要很多配置信息,所以我们创建一个config.py文件专门用来存贮我们所需要的信息。用的时候读取就可以了。
config核心代码:
"""
Date: 2019--14 16:37
User: yz
Email: 1147570523@qq.com
Desc:
"""
# Redis数据库地址
REDIS_HOST = '127.0.0.1'
# Redis端口
REDIS_PORT = 6379
# Redis密码,如无填None
REDIS_PASSWORD = None
REDIS_KEY = 'proxies'
# 代理分数
MAX_SCORE = 3
MIN_SCORE = 0
INITIAL_SCORE = 1
VALID_STATUS_CODES = [200, 302]
# 代理池数量界限
POOL_UPPER_THRESHOLD = 200
#线程
ThreadCount=100
FilterThreadCount = 100
# 检查周期
TESTER_CYCLE = 10
# 获取周期
GETTER_CYCLE = 20
# # 测试API,建议抓哪个网站测哪个
# TEST_URL = 'http://www.baidu.com'
# # API配置
API_HOST = '172.25.254.39'
API_PORT = 9999
#
# # 开关
TESTER_ENABLED = True
GETTER_ENABLED = True
API_ENABLED = True
接着就是爬虫主体的核心代码:
"""
批量获取待测ip的"挖掘机"
- ProxyMetaClass作为ProxyMetaClass的元类, 是为了能够动态地生成两个和爬取免费ip的方法有关;
- Crawler类主要定义了一组以crawl_打头的爬取方法,并通过调用主方法get_proxies, 遍历上述爬取方法,
批量爬取一系列proxies,生成的未经有效性检测的ip代理。
"""
import re
import sys
from config import POOL_UPPER_THRESHOLD
from db import RedisClient
from utils import get_page,test_proxy_vaild
from colorama import Fore
from concurrent.futures import ThreadPoolExecutor
class PoolEmptyError(Exception):
def __init__(self):
Exception.__init__(self)
def __str__(self):
return repr('代理池已经枯竭')
# 自定义元类
class ProxyMetaClass(type):
def __new__(cls, name, bases, attrs):
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 get_proxies(self, callback):
for proxy in eval("self.{}()".format(callback)):
yield proxy
def crawl_xicidaili(self):
"""爬取西刺网站的用户代理"""
for page in range(1, 10): # 获取前5页的信息
start_url = 'https://www.xicidaili.com/nt/{}'.format(page) # 构建网址
html = get_page(start_url) # 获取网页内容
# print(html)
if html: # 如果获取成功, 则提取ip和端口
find_trs = re.compile('<tr class.*?>(.*?)</tr>', re.S)
trs = find_trs.findall(html)
for tr in trs:
# 提取ip的正则表达式
find_ip = re.compile('<td>(\d+\.\d+\.\d+\.\d+)</td>')
re_ip_address = find_ip.findall(tr)
# 提取端口的正则表达式
find_port = re.compile('<td>(\d+)</td>')
re_port = find_port.findall(tr)
for address, port in zip(re_ip_address, re_port):
# 用冒号拼接IP和端口# str
address_port = address + ':' + port
# 返回生成器
yield address_port.replace(' ', '')
class PoolGetter(object):
def __init__(self):
self.redis = RedisClient()
self.crawler = Crawler()
def is_over_threshold(self):
"""
判断是否达到了代理池限制
"""
if self.redis.count() >= POOL_UPPER_THRESHOLD:
return True
else:
return False
def test_proxy_add(self, proxy):
"""检测是否可用, 可用添加到redis中"""
if test_proxy_vaild(proxy):
# print('[+]' + proxy + "可用")
print(Fore.GREEN + '成功获取到代理', proxy)
self.redis.add(proxy)
def run(self):
print("[-] 代理池获取器开始执行......")
if not self.is_over_threshold():
for callback_label in range(self.crawler.__CrawlFuncCount__):
callback = self.crawler.__CrawlFunc__[callback_label]
# 获取代理
proxies = self.crawler.get_proxies(callback)
# 刷新输出
sys.stdout.flush()
with ThreadPoolExecutor(200) as pool:
pool.map(self.test_proxy_add, proxies)
def is_Crawler_ok():
crawler = Crawler()
for callback_lable in range(crawler.__CrawlFuncCount__):
callback = crawler.__CrawlFunc__[callback_lable]
proxies = crawler.get_proxies(callback)
for item in proxies:
print(item)
def is_pool_ok():
proxyPool=PoolGetter()
proxyPool.run()
print("proxy count:",proxyPool.redis.count())
if __name__ == '__main__':
is_Crawler_ok()
#is_pool_ok()
我们爬虫所需要的请求信息放在一个utils.py文件中,用的时候读取。
核心代码:
import requests
from requests.exceptions import ConnectionError
# colorama是一个python专门用来在控制台、命令行输出彩色文字的模块,可以跨平台使用。
from colorama import Fore, Back, Style
import telnetlib
from db import RedisClient
"""
Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Style: DIM, NORMAL, BRIGHT, RESET_ALL
"""
base_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36',
}
def get_page(url, options=None):
"""
获取的网页源代码: 抓取代理
:param url:
:param options:
:return:
"""
r = RedisClient()
proxyIP = r.random()
proxies = {
'http': str(proxyIP),
'https': str(proxyIP)
}
if not options:
options = {}
headers = dict(base_headers, **options)
print(Fore.GREEN + '[+] 正在抓取', url)
try:
response = requests.get(url, headers=headers, proxies=proxies)
print(Fore.GREEN + '[+] 抓取成功', url, response.status_code)
if response.status_code == 200:
return response.text
except ConnectionError:
print(Fore.RED + '[-] 抓取失败', url)
return None
def test_proxy_vaild(proxy):
ip, port = proxy.split(":")
try:
tn = telnetlib.Telnet(ip, int(port), timeout=20)
except Exception as e:
print(e)
return False
else:
return True
if __name__ == '__main__':
url = 'http://www.baidu.com'
request=get_page(url)
print(request.headers)
爬取的时候难免会出现一些错误信息。我们将错误信息反馈回来。在我们主体代码里直接调用。
核心代码:
"""
class PoolEmptyError(Exception):
def __init__(self):
Exception.__init__(self)
def __str__(self):
return repr('代理池已经枯竭')
获取到代理IP后我们要把它存入数据库中,以便以后方便使用。
建立db.py文件。
核心代码:
"""
Date: 2019--14 16:37
User: yz
Email: 1147570523@qq.com
Desc:
"""
"""
db.py模块处在整个系统的中心位置,与其它的任意模块都有着紧密的联系。
该模块仅定义了一个RedisClient()类,该类定义了对Redis队列进行操作的几个通用方法(add,decrease等 )
"""
import random
import re
import redis
from config import *
from errors import PoolEmptyError
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('\d+\.\d+\.\d+\.\d+:\d+', proxy):
print('代理不符合规范', 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('代理', proxy, '当前分数', score, '减1')
# zincrby 对有序集合中指定成员的分数加上增量 increment, 正数是增加, 负数是减少;
return self.db.zincrby(REDIS_KEY, proxy, -1)
else:
print('代理', proxy, '当前分数', score, '移除')
# zrem: 用于移除有序集中的一个或多个成员,不存在的成员将被忽略。
return self.db.zrem(REDIS_KEY, proxy)
def drop(self,proxy):
# 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('代理', 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)
因为爬取的IP地址有存活期,所以要即使更新我们数据库中的代理IP,可以使用的继续存储,不可以使用的直接删除。
核心代码:
"""
Date: 2019--15 09:19
User: yz
Email: 1147570523@qq.com
Desc:
"""
from concurrent.futures import ThreadPoolExecutor
from db import RedisClient
from utils import test_proxy_vaild
class PoolTester(object):
def __init__(self):
self.redis = RedisClient()
def test_single_proxy(self, proxy):
"""
测试单个代理
:param proxy:
:return:
"""
if test_proxy_vaild(proxy):
self.redis.max(proxy)
print("[+] 代理可用", proxy)
else:
self.redis.drop(proxy)
print("[-] 代理不可用", proxy)
def run(self):
"""
测试的主函数
:return:
"""
print("测试器开始运行.......")
try:
count = self.redis.count()
print("当前剩余%d个代理" % (count))
# 使用线程池, 快速检测proxy是否可用
with ThreadPoolExecutor(100) as pool:
pool.map(self.test_single_proxy, self.redis.all())
except Exception as e:
print("测试器发生错误", e)
if __name__ == '__main__':
tester=PoolTester()
tester.run()
当然数据库里的代理IP不会一直可以使用,所以我们要定时更新。
核心代码:
"""
Date: 2019--15 10:06
User: yz
Email: 1147570523@qq.com
Desc:
"""
import time
from multiprocessing import Process
from ProxyPoolfilter import PoolTester
from api import app
from config import *
from spider import PoolGetter
class Scheduler(object):
def schedule_tester(self, cycle=TESTER_CYCLE):
"""定时检测代理IP是否可用?"""
tester = PoolTester()
while True:
print("测试器开始运行....")
tester.run()
# 每隔指定时间进行测试
time.sleep(cycle)
def schedule_getter(self, cycle=GETTER_CYCLE):
"""
定期获取代理
:param cycle:
:return:
"""
getter = PoolGetter()
while True:
print("开始抓取代理")
getter.run()
time.sleep(cycle)
def schedule_api(self):
"""
开启API
"""
app.run(API_HOST, API_PORT)
def run(self):
if TESTER_ENABLED:
print("正在启动TESTER.......")
test_process = Process(target=self.schedule_tester)
test_process.start()
if GETTER_ENABLED:
print("正在启动GETTER......")
getter_process = Process(target=self.schedule_getter)
getter_process.start()
if API_ENABLED:
print("正在启动API........")
api_process = Process(target=self.schedule_api)
api_process.start()
if __name__ == '__main__':
scheduler=Scheduler()
scheduler.run()
最后我们把我们的代理IP放到本机端口里。需要的时候获取。
核心代码:
"""
Date: 2019--15 10:54
User: yz
Email: 1147570523@qq.com
Desc:
"""
from flask import Flask
# __name__, 当前文件的名称
# print(__name__)
from db import RedisClient
app = Flask(__name__)
# url: http://127.0.0.1:5000/
@app.route('/')
def index():
html = """
<h1 style="color:green">欢迎来到代理池监控维护器</h1>
<hr/>
<ul>
<li><a href="/get_proxy/">代理IP的API地址</a></li>
<li><a href="/count/">IP池代理个数</a></li>
</ul>
"""
return html
@app.route('/get_proxy/')
def get_proxy():
r = RedisClient()
proxy = r.random()
return proxy
@app.route('/count/')
def count():
r = RedisClient()
return str(r.count())
if __name__ == '__main__':
# 开启服务
# 共享本机的所有IP, 0.0.0.0
app.run(host='172.25.254.39', port =9999)
到此,我们整个项目就结束了。