爬虫项目实战:代理池监控维护器

项目背景

在爬取一些大网站的时候,总会出现被反爬技术阻碍的情况,限制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()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值