作为一个爬虫开发者,使用IP代理是必要的一步,我们可以在网上找到免费的高匿IP,比如西刺代理。但是,这些免费的代理大部分都是不好用的,经常会被封禁。所以我们转而考虑购买付费代理。可是,作为一个程序员首先想的当然是构建自己的IP代理池供自己美滋滋的使用。
使用Redis的有序集合存储代理,我们建立一个RedisClient类来统一实现所有操作redis的方法。
import redis
from random import choice
MAX_SCORE = 100
MIN_SCORE = 0
INITAL_SCORE = 10
HOST = 'localhost'
PORT = 6379
DB_NUM=4
PASSWORD = None
KEY = 'proxies'
class RedisClient:
def __init__(self,host=HOST,pwd=PASSWORD,port=PORT):
'''
初始化,连接redis
:param host: Redis 地址
:param pwd: Redis 密码
:param port: Redis 端口
'''
self.db = redis.StrictRedis(host=host,port=port,password=pwd,decode_responses=True,db=DB_NUM)
def add(self,proxy,score=INITAL_SCORE):
'''
添加代理
:param proxy: 待添加代理
:param score: 标识的分数
:return:
'''
if not self.db.zscore(KEY,proxy):
return self.db.zadd(KEY,score,proxy)
def random(self):
'''
随机获取代理,首先获取标识分数最高的,如果不存在则按照排序进行获取
:return:
'''
result = self.db.zrevrange(KEY,MAX_SCORE,MAX_SCORE)
if len(result):
return choice(result)
else:
result = self.db.zrevrange(KEY,MIN_SCORE,MAX_SCORE)
if len(result):
return choice(result)
else:
print('无代理可获取')
return None
def decrease(self,proxy):
'''
将指定代理的标识分数减1,如果标识值小于MIN_SCORE则将它从redis中移除
:param proxy:
:return:
'''
score = self.db.zscore(KEY,proxy)
if score and score > MIN_SCORE:
print('代理',proxy,'当前分数',score,'减1')
return self.db.zincrby(KEY,proxy,-1)
else:
print('代理', proxy, '当前分数', score, '移除')
return self.db.zrem(KEY,proxy)
def exists(self,proxy):
'''
判断代理是否存在
:return:
'''
return not self.db.zscore(KEY,proxy)==None
def max(self,proxy):
'''
如果新代理检测可以使用在将标识值设置为MAX_SCORE
:param proxy: 新代理
:return:
'''
print('新代理',proxy,'可用,设置为',MAX_SCORE)
return self.db.zadd(KEY,MAX_SCORE,proxy)
def count(self):
'''
返回当前数据库中的代理总数
:return:
'''
return self.db.zcard(KEY)
def all(self):
'''
返回所有的代理,用于检测
:return:
'''
return self.db.zrangebyscore(KEY,MIN_SCORE,MAX_SCORE)
我目前从西刺代理,66和快代理这三个地方获取代理。
class Crawler:
#爬取66ip
def crawl_66(self,page=1):
for num in range(1,page+1):
url = 'http://www.66ip.cn/{}.html'.format(num)
html = get_html(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_xici(self,page=4):
for num in range(1,page+1):
url = 'http://www.xicidaili.com/nn/{}'.format(num)
html = get_html(url)
if html:
doc = pq(html)
trs = doc('#ip_list tr:gt(0)').items()
for tr in trs:
ip = tr.find('td:nth-child(2)').text()
port = tr.find('td:nth-child(3)').text()
yield ':'.join([ip,port])
#爬取快代理
def crawl_kuai(self,page=4):
for num in range(1,page+1):
url = 'https://www.kuaidaili.com/free/inha/{}/'.format(num)
html = get_html(url)
if html:
doc = pq(html)
trs = doc('#list 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])
使用Crawler类即可获取IP代理,我们再创建一个Getter类,它调用Crawler类的方法获取代理,并将代理存储在Redis中。
THRESHOLD = 10000#数据库中最多存储10000条IP
class Getter:
def __init__(self):
self.redis = RedisClient()
self.crawler = Crawler()
def is_arrive_threshold(self):
'''
判断当前数据库中的IP条目数是否到达设置的阈值
:return:
'''
if self.redis.count() >= THRESHOLD:
return True
else:
return False
def run(self):
print('Getter Run...')
if not self.is_arrive_threshold():
#未到达阈值才继续获取
'''
调用Crawler类的crawl_*方法,并存储
'''
pass
Getter类的run方法中需要依次调用爬取方法,这本没有错,不过这不太适合扩展,因为一旦Crawler类中添加的一个网站的爬取方法时这里需要我们修改。违反了开闭原则。
如果我们在run方法中能获取Crawler类的所有方法就好了,这有点类似于JAVA中的反射。在原代码上修改以下代码可以实现此要求:
创建元类,Crawler类继承这个类则可以获取到它的所有crawl_开头的方法
class ProxyCrawlerMetaclass(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)#将crawl_开头的方法名添加进attrs中
count+=1
attrs['__CrawlFuncCount__'] = count
return type.__new__(cls,name,bases,attrs)
Crawler继承元类,添加get_proxies方法
class Crawler(object,metaclass=ProxyCrawlerMetaclass):
def get_proxies(self,callback):
'''
通过回调函数名执行对应的方法,返回获取到的所有代理
:param callback: 回调函数名
:return:
'''
proxies = []
for proxy in eval('self.{}()'.format(callback)):#eval()函数可以执行代码
print('get_proxies success')
proxies.append(proxy)
return proxies
完善run方法
def run(self):
print('Getter Run...')
if not self.is_arrive_threshold():
#未到达阈值才继续获取
'''
调用Crawler类的crawl_*方法
'''
for callback_label in range(self.crawler.__CrawlFuncCount__):
callback = self.crawler.__CrawlFunc__[callback_label]
proxies = self.crawler.get_proxies(callback)
for proxy in proxies:
self.redis.add(proxy)
元类是Python中的一个难点,几乎很少使用。如果想要了解请查看此文。
至此,完整代码点击下载,失效联系博主。
首先我们需要明白的是,检测是异步进行的,我使用aiohttp库,直接使用pip install aiohttp
安装即可。
import aiohttp
import asyncio
import time
from aiohttp import ClientError
from DB.RedisClient import RedisClient
TEST_URL = 'http://www.baidu.com'#测试网址,自定义修改
BATCH_SIZE = 100#一次测试的代理数目
RETURN_CODE = [200]#正确的返回码
class Tester:
def __init__(self):
self.redis = RedisClient()
async def test_proxy(self,proxy):
'''
测试指定的代理
:param proxy: 待测试的代理
:return:
'''
conn = aiohttp.TCPConnector(verify_ssl=False)
async with aiohttp.ClientSession(connector=conn) as session:
try:
if isinstance(proxy,bytes):
proxy = proxy.decode('UTF-8')
test_proxy = 'http://'+proxy
print('测试:',proxy)
async with session.get(TEST_URL,proxy=test_proxy,timeout=5) as resp:
if resp.status in RETURN_CODE:
self.redis.max(proxy)#检测到代理可用,将标识分数设置为MAX_SCORE
else:
self.redis.decrease(proxy)#代理不可用,标识分数减1
except (AttributeError, TimeoutError, ClientError, aiohttp.ClientConnectorError):
self.redis.decrease(proxy)#代理不可用,标识分数减1
def run(self):
'''
测试main方法
:return:
'''
print('开始测试...')
try:
proxies = self.redis.all()
loop = asyncio.get_event_loop()
#批量测试
for i in range(0,len(proxies),BATCH_SIZE):
test_proxies = proxies[i:i+BATCH_SIZE]#获取测试代理
tasks = [self.test_proxy(proxy) for proxy in test_proxies]
loop.run_until_complete(asyncio.wait(tasks))
time.sleep(5)
except Exception as e:
print('测试错误',e.args)
使用async关键字修饰方法表示此方法是异步的。RETURN_CODE含有访问正确的返回码,最常见的就是200,也许还有其他的状态码。
至此,完整代码点击下载,失效联系博主。
现在我已经能获取代理,存储代理和检测代理了。诸如获取天气信息中我们都是使用一个API进行获取信息的,所有接口服务模块我们使用Flask来搭建Web服务。直接使用pip3 install flask
安装即可。学习请看这:http://flask.pocoo.org/。
from flask import Flask,g
from DB.RedisClient 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!
'
@app.route('/get')
def get_proxy():
'''
随机获取代理
:return: 随机代理
'''
conn = get_conn()
return conn.random()
@app.route('/count')
def get_counts():
'''
获得数据库中的代理总量
:return: 代理总数
'''
print('get counts()')
conn = get_conn()
return str(conn.count())
if __name__=='__main__':
app.run()
开启web服务,在5000端口监听。
至此,完整代码点击下载,失效联系博主。
我们完成了主要模块的编写,最后一步我们需要将上面的模块整合,统一调度。
from GetIP import Getter
from WebService.WebServer import app
from TEST import TestIP
from multiprocessing import Process
import time
TEST_CYCLE = 20
GET_CYCLE = 20
TEST_ENABLE = True
GET_ENABLE = True
class Scheduler:
def schedule_test(self,cycle = TEST_CYCLE):
'''
定时检测代理
:param cycle: 检测间隔时间
:return:
'''
tester = TestIP()
while True:
print('测试开始...')
tester.run()
time.sleep(cycle)
def schedule_get(self,cycle=GET_CYCLE):
'''
定时获取IP
:param cycle: 获取间隔时间
:return:
'''
getter = Getter()
while True:
print('开始获取IP...')
getter.run()
time.sleep(cycle)
def run(self):
'''
总体调度
:return:
'''
print('代理池开始运行...')
if TEST_ENABLE:
tester_process = Process(target=self.schedule_test())
tester_process.start()
if GET_ENABLE:
getter_process = Process(target=self.schedule_get())
getter_process.start()
最后我们只需要执行run方法即可开启代理池了,再执行WebServer的run方法开启web服务。