(廿九)Python爬虫:IP代理池的开发

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_25343557/article/details/82793075

      作为一个爬虫开发者,使用IP代理是必要的一步,我们可以在网上找到免费的高匿IP,比如西刺代理。但是,这些免费的代理大部分都是不好用的,经常会被封禁。所以我们转而考虑购买付费代理。可是,作为一个程序员首先想的当然是构建自己的IP代理池供自己美滋滋的使用。


需求分析

  • [1] 抓取IP代理:
    从各大免费代理网站定时爬取一定数量的IP代理。
  • [2] 存储IP代理:
    将抓取到的IP代理存储在数据库中。
  • [3] 代理检测:
    由于我们抓取的免费代理不一定是可用的,所以我们需要定时对数据库中的代理进行检测。检测原理就是使用该代理访问我们的目标网站,通用的就是访问百度即可。
  • [4] API接口服务:
    使用Flask搭建web服务,通过使用相应的接口即可从数据库中取出可用的代理。

详情设计

  • [1] 数据库:
    为提高访问速度选择Redis数据库,并且为了能够去重选择集合。同时,我们需要根据代理的性能进行排序,最终使用有序集合。
  • [2] 检测算法:
    每一个存储在数据库中的代理我们设置一个标识,新获取的代理标识为10,一旦检测成功则置为100。每次检测代理时,如果检测成功,标识加1,否则减1。一旦标识小于某一阈值时则将该代理移除。

功能实现

存储代理模块

      使用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 '<h2>Welcome!</h2>'

@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服务。

完整代码:https://github.com/WingedCat/IPpool
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_25343557/article/details/82793075