基本模块:
- 存储模块: 使用Redis的有序集合负责存储抓取下来的代理,保证代理不重复
- 获取模块:需要定时在各大代理网站抓取代理,代理的形式都是IP加端口,尽量从不同来源获取,尽量抓取高匿代理。
- 检测模块:需要定时检测数据库中的代理,设置一个检测链接,最好是爬取哪个网站就检测哪个网站
- 接口模块:需要用API来提供对外服务的接口,由于可用代理可能有多个,那么我们可以设置一个随机返回某个可用代理的接口,这样就能保证每个可用代理都可以取到实现负载均衡
存储模块:使用redis的有序集合,有序集合的每个元素都有一个分数字段,分数是可以重复的。根据分数对集合进行排序,可以作为判断一个代理是否可用的标志。
操作数据库的有序集合
MAX_SCORE=100
MIN_SCORE=0
INITIAL_SCORE=10
REDIS_HOST='localhost'
REDIS_PORT=6379
REDIS_PADDWORD=None
REDIS_KEY='proxies'
import redis
from random import choice
class RedisClient(object):
def __init__(self,host=REDIS_HOST,port=REDIS_PORT,password=REDIS_PADDWORD):
"""
初始化
:param host:Redis 地址
:param port: Redis 端口
:param password: Redis 密码
"""
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 self.db.zscore(REDIS_KEY,proxy):
return self.db.zadd(REDIS_KEY,score,proxy)
#随机获取代理
def random(self):
"""
随机获取有效代理,首先尝试获取最高分数代理,如果最高分数不存在,则按照排名获取,否则异常
:return: 随即代理
"""
result=self.db.zrangebyscore(REDIS_KEY,MAX_SCORE,MAX_SCORE)
if len(result):
return choice(result)
else:
result=self.db.zrevrange(REDIS_KEY,0,100)
if len(result):
return choice(result)
else:
raise PoolEmptyError
#检测代理无效时分数减1
def decrease(self,proxy):
"""
代理值减一分,分数小于最小值,则代理删除
:param proxy: 代理
:return: 修改后的代理分数
"""
score=self.db.zscore(REDIS_KEY,proxy)
if score and score > MIN_SCORE:
print('代理',proxy,'当前分数',score,'减1')
return self.db.zincrby(REDIS_KEY,proxy,-1)
else:
print('代理',proxy,'当前分数',score,'移除')
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:数量
"""
return self.db.zcard(REDIS_KEY)
#返回所有的代理列表
def all(self):
"""
获取全部代理
:return: 全部代理列表
"""
return self.db.zrangebyscore(REDIS_KEY,MIN_SCORE,MAX_SCORE)
获取模块
import json
from .utils import get_page
from pyquery import PyQuery as pq
class ProxyMetaclass(type):
def __new__(cls, name,bases,attrs):
count=0
attrs['__CrawlFunc__']=[]
# 判断方法开头是否是Crawl,如果是则将其加入到__CrawlFunc__属性中,就成功将所有以crawl开头的方法定义成了一个属性,动态获取到所有以crawl开头的方法列表
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):
proxies=[]
for proxy in eval("self.{}()".format(callback)):
print('成功获取到代理',proxy)
proxies.append(proxy)
return proxies
def crawl_daili66(self,page_count=4):
"""
获取代理66
:param page_count:页码
:return: 代理
"""
start_url='http://www.66ip.cn/{}/html'
urls=[start_url.format(page)for page in range(1,page_count+1)]
for url in urls:
print('Crawling',url)
html=get_page(url)
if html:
doc=pq(html)
trs=doc('.containerbox table tr:gt(0)').items()
for tr in trs:
ip=tr.find('td:nth-child(1)').text()
port=tr.find('td:nth-child(2)').text()
yield ':'.join([ip,port])
def crawl_proxy360(self):
"""
获取Proxy360
:return: 代理
"""
start_url='http://www.proxy360.cn/Regio.China'
print('Crawling',start_url)
html=get_page(start_url)
if html:
doc=pq(html)
lines=doc('div[name="list_proxy_ip"]').items()
for line in lines:
ip=line.find('.tbBottomLine:nth-child(1)').text()
port=line.find('.tbBottomLine:nth-child(2)').text()
yield ':'.join([ip,port])
def crawl_goubanjia(self):
"""
获取Goubanjia
:return: 代理
"""
start_url='http://www.goubanjia.com/free/gngn/index.shtml'
html=get_page(start_url)
if html:
doc=pq(html)
tds=doc('td.ip').items()
for td in tds:
td.find('p').remove()
yield td.text().replace(' ','')
#定义一个Getter类,用来动态调用所有以crawl开头的方法
from db import RedisClient
from crawler import Crawler
POOL_UPPER_THRESHOLD=10000
class Getter():
def __init__(self):
self.redis=RedisClient()
self.crawler=Crawler()
def is_over_threshold(self):
"""
判断你是否达到了代理池的限制
:return:
"""
if self.redis.count()>=POOL_UPPER_THRESHOLD:
return True
else:
return False
def run(self):
print('获取器开始执行')
if not self.is_over_threshold():
for callback_label in range(self.crawler.__CrawFuncCount__):
callback=self.crawler.CrawlFunc__[callback_label]
proxies=self.crawler.get_proxies(callback)
for proxy in proxies:
self.redis.add(proxy)
检测模块
VALID_STATUS_CODES=[200]
TEST_URL='http://www.baidu.com'
BATCH_TEST_SIZE=100
class Tester(object):
def __init__(self):
self.redis=RedisClient()
async def test_single_proxy(self,proxy):
"""
测试单个代理
:param proxy: 单个代理
:return: None
"""
conn=aiohttp.TCPConnector(verify_sal=False)
async with aiohttp.ClientSession(connector=conn)as session:
try:
if isinstance(proxy,bytes):
proxy=proxy.decode('utf-8')
real_proxy='http://'+proxy
print('正在测试',proxy)
async with session.get(TEST_URL,proxy=real_proxy,timeout=15) as response:
if response.status in VALID_STATUS_CODES:
self.redis.max(proxy)
print('代理可用',proxy)
else:
self.redis.decrease(proxy)
print('请求响应码不合法',proxy)
except(ClientError,clientConnectorError,TimeoutError,AttributeError):
self.redis.decrease(proxy)
print('代理请求失败',proxy)
#获取所有的代理列表
def run(self):
"""
测试主函数
:return: None
"""
print('测试器开始运行')
try:
proxies=self.redis.all()
loop=asyncio.get_event_loop()
#批量测试
for i in range(0,len(proxies),BATCH_TEST_SIZE):
test_proxies=proxies[i:i+BATCH_TEST_SIZE]
tasks=[self.test_single_proxy(proxy) for proxy in test_proxies]
loop.run_until_complete(asyncio.wait(tasks))
time.sleep(5)
except Exception as e:
print('测试器发送错误',e.args)
接口模块
from flask import Flask,g
from db import RedisClient
__all__=['app']
app=Flask(__name__)
def get_conn():
if not hasattr(g,'redis'):
g.redis=RedisClient()
return g.redis
@app.route('/')
def index():
return 'Welcome to Proxy Pool System
'
@app.route('/random')
def get_proxy():
"""
获取随机可用代理
:return: 随机代理
"""
conn=get_conn()
return conn.random()
@app.route('/count')
def get_counts():
"""
获取代理池总量
:return: 代理池总量
"""
conn=get_conn()
return str(conn.count())
调度模块
TEST_CYCLE=20
GETTER_CYCLE=20
TESTER_ENABLD=True
GETTER_ENABLD=True
API_ENABLD=True
from multiprocessing import Process
from api import app
from getter import Getter
from tester import Tester
class Scheduler():
def schedule_tester(self,cycle=TEST_CYCLE):
"""
定时测试代理
:param cycle:
:return:
"""
tester=Tester()
while True:
print('测试器开始运行')
tester.run()
time.sleep(cycle)
def schedule_api(self):
"""
开启API
:return:
"""
app.run(API_HOST,API_PORT)
def run(self):
print('代理池开始运行')
if TESTER_ENABLD:
tester_process=Process(target=self.schedule_tester)
tester_process.start()
if GETTER_ENABLD:
getter_process=Process(target=self.schedule_getter)
getter_process=start()
if API_ENABLD:
api_process=Process(target=self.schedule_api())
api_process.start()
if __name__ == '__main__':
app.run()
ADSL通过拨号的方式上网,需要输入ADSL账号和密码,每次拨号就更换一个IP。