1. 代理池概述

1.1 什么是代理池

  • 代理池就是有代理IP组成的池子, 它可以提供多个稳定可用的代理IP

    1.2 为什么要实现代理池

  1. 免费代理都是非常不稳定的, 有10%是可用就很不错了.
  2. 一些收费代理稳定性也不好, 便宜一点只有30%~50%左右是可用.
  3. 注: 如果代理IP提供商, 提供接口很好, 稳定性也很高, 就无需使用代理池

    1.3. 代理池开发环境

  • 平台: Window,可以运行Linux上
  • 开发语言: Python3
  • 开发工具: PyCharm
  • 使用到的主要技术:

    • requests: 发送请求, 获取页面数据
    • lxml: 使用XPATH从页面提取我们想要的数据
    • pymongo: 把提取到数据数据存储到MongoDB数据库中和从MongoDB数据库中读取数据, 进行统计.
    • Flask: 用于提供WEB服务
    • js2py:用于执行js, 解决js + cookie反爬
    • schedule:用于每间隔一段时间, 执行一次相关代码

      2. 代理池的设计

      image.png
  • 代理池工作流程文字描述:

    • 代理IP采集模块 -> 采集代理IP -> 检测代理IP ->如果不可用用, 直接过滤掉, 如果可用, 指定默认分数 -> 存入数据库中
    • 代理IP检测模块 -> 从数据库中获取所有代理IP -> 检测代理IP -> 如果代理IP不可用用, 就把分数-1, 如果分数为0从数据库中删除, 否则更新数据库, 如果代理IP可用, 恢复为默认分值,更新数据库
    • 代理API模块 -> 从数据库中高可用的代理IP给爬虫使用;

      2.1. 代理池的模块及其作用

  • 代理池分五大核心模块:

    • 爬虫模块: 采集代理IP
      • 从代理IP网站上采集代理IP
      • 进行校验(获取代理响应速度, 协议类型, 匿名类型),
      • 把可用代理IP存储到数据库中
    • 代理IP的校验模块: 获取指定代理的响应速度, 支持的协议以及匿名程度
      • 原因: 网站上所标注的响应速度,协议类型和匿名类型是不准确的
      • 这里使用httpbin.org进行检测
    • 数据库模块: 实现对代理IP的增删改查操作
      • 这里使用MongoDB来存储代理IP
    • 检测模块: 定时的对代理池中代理进行检测, 保证代理池中代理的可用性.
      • 从数据库读取所有的代理IP
      • 对代理IP进行逐一检测, 可用开启多个协程, 以提高检测速度
      • 如果该代理不可用, 就让这个代理分数-1, 当代理的分数到0了, 就删除该代理; 如果检测到代理可用就恢复为满分.
    • 代理IP服务接口: 提供高可用的代理IP给爬虫使用
      • 根据协议类型和域名获取随机的高质量代理IP
      • 根据协议类型和域名获取多个高质量代理IP
      • 根据代理IP,不可用域名, 告诉代理池这个代理IP在该域名下不可用, 下次获取这个域名的代理IP时候, 就不会再获取这个代理IP了, 从而保证代理IP高可用性.
  • 代理池的其他模块

    • 数据模型: domain.py:
      • 代理IP的数据模型, 用于封装代理IP相关信息, 比如ip,端口号, 响应速度, 协议类型, 匿名类型,分数等.
    • 程序启动入口: main.py
      • 代理池提供一个统一的启动入口
    • 工具模块:
      • 日志模块: 用于记录日志信息
      • http模块: 用于获取随机User-Agent的请求头
    • 配置文件: settings.py
      • 用于默认代理的分数, 配置日志格式, 文件, 启动的爬虫, 检验的间隔时间 等.

        2.2. 代理池的项目结构

        代理池的项目结构:
        1. -- IPProxyPool
        2. -- core
        3. -- db
        4. -- __init__.py
        5. -- mongo_pool.py
        6. -- proxy_validate
        7. -- __init__.py
        8. -- httpbin_validator.py
        9. -- proxy_spiders
        10. -- __init__.py
        11. -- base_spider.py
        12. -- proxy_spiders.py
        13. -- run_spiders.py
        14. -- proxy_test.py
        15. -- proxy_api.py
        16. -- domain.py
        17. -- utils
        18. -- __init__.py
        19. -- http.py
        20. -- log.py
        21. -- main.py
        22. -- settings.py

        3. 定义代理IP的数据模型类

  • 目标: 用于封装代理信息

  • 步骤:

    • 定义一个类, 继承object
    • 实现init方法, 负责初始化, 包含如下字段:
      • ip: 代理的IP地址
      • port: 代理IP的端口号
      • protocol: 代理IP支持的协议类型,http是0, https是1, https和http都支持是2
      • nick_type: 代理IP的匿名程度, 高匿:0, 匿名: 1, 透明:0
      • speed: 代理IP的响应速度, 单位s
      • area: 代理IP所在地区
      • score: 代理IP的评分, 默认分值可以通过配置文件进行配置. 在进行代理可用性检查的时候, 每遇到一次请求失败就减1份, 减到0的时候从池中删除. 如果检查代理可用, 就恢复默认分值
      • disable_domains: 不可用域名列表, 有些代理IP在某些域名下不可用, 但是在其他域名下可用
    • 创建配置文件: settings.py; 定义MAX_SCORE = 50,

      1. # 代理模型类, 用于封装代理相关信息
      2. class Proxy(object):
      3. def __init__(self, ip, port, protocol=-1, nick_type=-1,speed=-1, area=None, score=50, disable_domains=[]):
      4. self.ip = ip # IP
      5. self.port = port # 端口号
      6. self.protocol = protocol # 代理IP支持协议类型,http是0, https是1, https和http都支持是2
      7. self.nick_type = nick_type # 匿名程度:高匿:0,匿名: 1, 透明:0
      8. self.speed = speed # 速度, 单位s
      9. self.area = area # 所在地区
      10. self.score = score # 代理IP的评分, 在进行代理可用性检查的时候, 每遇到一次请求失败就减1份, 减到0的时候从池中删除. 如果检查代理可用, 就恢复默认分值
      11. self.disable_domains = disable_domains
      12. def __str__(self):
      13. # 返回数据字符串
      14. return str(self.__dict__)

      4. 代理池工具模块

      代理工具模块包含: 日志模块 和 http 两个模块

      4.1 日志模块

      4.1.1 为什么要实现日志模块

  • 能够方便的对程序进行调试

  • 能够记录程序的运行状态
  • 记录错误信息

    4.1.2. 日志的实现

  • 目标: 实现日志模块, 用于记录日志

  • 前提: 日志模块在网上有很多现成的实现, 我们开发的时候, 通常不会再自己写; 而是使用拿来主义. 拿来用就完了.

    4.1.3 代码

    把日志相关配置信息 放到配置文件中
    1. # settings.py
    2. import logging
    3. # 默认的配置
    4. LOG_LEVEL = logging.INFO # 默认等级
    5. LOG_FMT = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s' # 默认日志格式
    6. LOG_DATEFMT = '%Y-%m-%d %H:%M:%S' # 默认时间格式
    7. LOG_FILENAME = 'log.log' # 默认日志文件名称
    ```python

    utils/log.py

    import sys import logging

导入settings中日志配置信息

from settings import LOG_FMT, LOG_DATEFMT, LOG_FILENAME, LOG_LEVEL

class Logger(object):

  1. def __init__(self):
  2. # 1. 获取一个logger对象
  3. self._logger = logging.getLogger()
  4. # 2. 设置format对象
  5. self.formatter = logging.Formatter(fmt=LOG_FMT,datefmt=LOG_DATEFMT)
  6. # 3. 设置日志输出
  7. # 3.1 设置文件日志模式
  8. self._logger.addHandler(self._get_file_handler(LOG_FILENAME))
  9. # 3.2 设置终端日志模式
  10. self._logger.addHandler(self._get_console_handler())
  11. # 4. 设置日志等级
  12. self._logger.setLevel(LOG_LEVEL)
  13. def _get_file_handler(self, filename):
  14. '''返回一个文件日志handler'''
  15. # 1. 获取一个文件日志handler
  16. filehandler = logging.FileHandler(filename=filename,encoding="utf-8")
  17. # 2. 设置日志格式
  18. filehandler.setFormatter(self.formatter)
  19. # 3. 返回
  20. return filehandler
  21. def _get_console_handler(self):
  22. '''返回一个输出到终端日志handler'''
  23. # 1. 获取一个输出到终端日志handler
  24. console_handler = logging.StreamHandler(sys.stdout)
  25. # 2. 设置日志格式
  26. console_handler.setFormatter(self.formatter)
  27. # 3. 返回handler
  28. return console_handler
  29. @property
  30. def logger(self):
  31. return self._logger

初始化并配一个logger对象,达到单例的

使用时,直接导入logger就可以使用

logger = Logger().logger

if name == ‘main‘: logger.debug(“调试信息”) logger.info(“状态信息”) logger.warning(“警告信息”) logger.error(“错误信息”) logger.critical(“严重错误信息”)

  1. <a name="ABarc"></a>
  2. ## 4.2 http模块
  3. 在从代理IP网站上抓取代理IP 和 检验代理IP时候, 为了不容易不服务器识别为是一个爬虫, 我们最好提供随机的User-Agent请求头.
  4. - 目标: 获取随机User-Agent的请求头
  5. - 步骤:
  6. 1. 准备User-Agent的列表
  7. 1. 实现一个方法, 获取随机User-Agent的请求头
  8. - 代码:
  9. ```python
  10. import random
  11. # 1. 准备User-Agent的列表
  12. USER_AGENTS = [
  13. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
  14. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
  15. "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
  16. "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
  17. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
  18. "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
  19. "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
  20. "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
  21. "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
  22. "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
  23. "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
  24. "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
  25. "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
  26. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
  27. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
  28. "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
  29. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
  30. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
  31. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
  32. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
  33. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
  34. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
  35. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
  36. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
  37. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
  38. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
  39. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
  40. "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
  41. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
  42. "Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
  43. "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
  44. "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
  45. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
  46. "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"
  47. ]
  48. # 实现一个方法, 获取随机User-Agent的请求头
  49. def get_request_headers():
  50. headers = {
  51. 'User-Agent': random.choice(USER_AGENTS),
  52. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  53. 'Accept-Language': 'en-US,en;q=0.5',
  54. 'Connection': 'keep-alive',
  55. 'Accept-Encoding': 'gzip, deflate',
  56. }
  57. return headers
  58. if __name__ == '__main__':
  59. print(get_request_headers())
  60. print(get_request_headers())
  61. print(get_request_headers())

5. 代理IP的校验模块

  • 目标: 检查代理IP速度, 支持的协议类型, 以及匿名程度.
  • 步骤:
    • 检查代理IP速度 和 匿名程度;
      • 代理IP速度: 就是发送请求到获取响应的时间间隔
      • 匿名程度检查:
    • 检查代理IP协议类型

配置文件配置:

# settings.py
# 测试代理IP的超时时间
TEST_TIMEOUT = 10
import time
import requests
import json

from utils.http import get_request_headers
from settings import TEST_TIMEOUT
from utils.log import logger
from domain import Proxy


def check_proxy(proxy):
    """
    用于检查指定 代理IP 响应速度, 匿名程度, 支持协议类型
    :param proxy: 代理IP模型对象
    :return: 检查后的代理IP模型对象
    """

    # 准备代理IP字典
    proxies = {
        'http':'http://{}:{}'.format(proxy.ip, proxy.port),
        'https':'https://{}:{}'.format(proxy.ip, proxy.port),
    }

    # 测试该代理IP
    http, http_nick_type, http_speed = __check_http_proxies(proxies)
    https, https_nick_type, https_speed = __check_http_proxies(proxies, False)
    # 代理IP支持的协议类型, http是0, https是1, https和http都支持是2
    if http and https:
        proxy.protocol = 2
        proxy.nick_type = http_nick_type
        proxy.speed = http_speed
    elif http:
        proxy.protocol = 0
        proxy.nick_type = http_nick_type
        proxy.speed = http_speed
    elif https:
        proxy.protocol = 1
        proxy.nick_type = https_nick_type
        proxy.speed = https_speed
    else:
        proxy.protocol = -1
        proxy.nick_type = -1
        proxy.speed = -1

    return proxy


def __check_http_proxies(proxies, is_http=True):
    # 匿名类型: 高匿: 0, 匿名: 1, 透明: 2
    nick_type = -1
    # 响应速度, 单位s
    speed = -1

    if is_http:
        test_url = 'http://httpbin.org/get'
    else:
        test_url = 'https://httpbin.org/get'

    try:
        # 获取开始时间
        start = time.time()
        # 发送请求, 获取响应数据
        response = requests.get(test_url, headers=get_request_headers(), proxies=proxies, timeout=TEST_TIMEOUT)

        if response.ok:
            # 计算响应速度
            speed =  round(time.time() - start, 2)
            # 匿名程度
            # 把响应的json字符串, 转换为字典
            dic = json.loads(response.text)
            # 获取来源IP: origin
            origin = dic['origin']
            proxy_connection = dic['headers'].get('Proxy-Connection', None)
            if ',' in origin:
                #    1. 如果 响应的origin 中有','分割的两个IP就是透明代理IP
                nick_type = 2
            elif proxy_connection:
                #    2. 如果 响应的headers 中包含 Proxy-Connection 说明是匿名代理IP
                nick_type = 1
            else:
                #  3. 否则就是高匿代理IP
                nick_type = 0

            return True, nick_type, speed
        return False, nick_type, speed
    except Exception as ex:
        # logger.exception(ex)
        return False, nick_type, speed


if __name__ == '__main__':
    proxy = Proxy('61.135.217.7', port='80')
    print(check_proxy(proxy))

6. 代理池数据库模块

  • 作用: 用于对proxies集合进行数据库的相关操作
  • 目标: 实现对数据库增删改查相关操作
  • 步骤:
  1. 在init中, 建立数据连接, 获取要操作的集合, 在 del 方法中关闭数据库连接
    2.提供基础的增删改查功能
    2.1 实现插入功能
    2.2 实现修改该功能
    2.3 实现删除代理: 根据代理的IP删除代理
    2.4 查询所有代理IP的功能
    3. 提供代理API模块使用的功能
    3.1 实现查询功能: 根据条件进行查询, 可以指定查询数量, 先分数降序, 速度升序排, 保证优质的代理IP在上面.
    3.2 实现根据协议类型 和 要访问网站的域名, 获取代理IP列表
    3.3 实现根据协议类型 和 要访问网站的域名, 随机获取一个代理IP
    3.4 实现把指定域名添加到指定IP的disable_domain列表中.
    python -m pip install -U pymongo
    
    ```python from pymongo import MongoClient import pymongo import random

from settings import MONGO_URL from utils.log import logger

from domain import Proxy

class MongoPool(object):

def __init__(self):
    # 1.1. 在init中, 建立数据连接
    self.client = MongoClient(MONGO_URL)
    # 1.2  获取要操作的集合
    self.proxies = self.client['proxies_pool']['proxies']

def __del__(self):
    # 1.3 关闭数据库连接
    self.client.close()

def insert_one(self, proxy):
    """2.1 实现插入功能"""

    count = self.proxies.count_documents({'_id': proxy.ip})
    if count == 0:
        # 我们使用proxy.ip作为, MongoDB中数据的主键: _id
        dic = proxy.__dict__
        dic['_id'] = proxy.ip
        self.proxies.insert_one(dic)
        logger.info('插入新的代理:{}'.format(proxy))
    else:
        logger.warning("已经存在的代理:{}".format(proxy))


def update_one(self, proxy):
    """2.2 实现修改该功能"""
    self.proxies.update_one({'_id': proxy.ip}, {'$set':proxy.__dict__})

def delete_one(self, proxy):
    """2.3 实现删除代理: 根据代理的IP删除代理"""
    self.proxies.delete_one({'_id': proxy.ip})
    logger.info("删除代理IP: {}".format(proxy))

def find_all(self):
    """2.4 查询所有代理IP的功能"""
    cursor = self.proxies.find()
    for item in cursor:
        # 删除_id这个key
        item.pop('_id')
        proxy = Proxy(**item)
        yield proxy

def find(self, conditions={}, count=0):
    """
    3.1 实现查询功能: 根据条件进行查询, 可以指定查询数量, 先分数降序, 速度升序排, 保证优质的代理IP在上面.
    :param conditions: 查询条件字典
    :param count: 限制最多取出多少个代理IP
    :return: 返回满足要求代理IP(Proxy对象)列表
    """
    cursor = self.proxies.find(conditions, limit=count).sort([
        ('score', pymongo.DESCENDING),('speed', pymongo.ASCENDING)
    ])

    # 准备列表, 用于存储查询处理代理IP
    proxy_list = []
    # 遍历 cursor
    for item in cursor:
        item.pop('_id')
        proxy = Proxy(**item)
        proxy_list.append(proxy)

    # 返回满足要求代理IP(Proxy对象)列表
    return proxy_list

def get_proxies(self, protocol=None, domain=None, count=0, nick_type=0):
    """
    3.2 实现根据协议类型 和 要访问网站的域名, 获取代理IP列表
    :param protocol: 协议: http, https
    :param domain: 域名: jd.com
    :param count:  用于限制获取多个代理IP, 默认是获取所有的
    :param nick_type: 匿名类型, 默认, 获取高匿的代理IP
    :return: 满足要求代理IP的列表
    """
    # 定义查询条件
    conditions = {'nick_type': nick_type}
    # 根据协议, 指定查询条件
    if protocol is None:
        # 如果没有传入协议类型, 返回支持http和https的代理IP
        conditions['protocol'] = 2
    elif protocol.lower() == 'http':
        conditions['protocol'] = {'$in': [0, 2]}
    else:
        conditions['protocol'] = {'$in': [1, 2]}

    if domain:
        conditions['disable_domains'] = {'$nin': [domain]}


    # 满足要求代理IP的列表
    return self.find(conditions, count=count)

def random_proxy(self, protocol=None, domain=None, count=0, nick_type=0):
    """
    3.3 实现根据协议类型 和 要访问网站的域名, 随机获取一个代理IP
    :param protocol: 协议: http, https
    :param domain: 域名: jd.com
    :param count:  用于限制获取多个代理IP, 默认是获取所有的
    :param nick_type: 匿名类型, 默认, 获取高匿的代理IP
    :return: 满足要求的随机的一个代理IP(Proxy对象)
    """
    proxy_list = self.get_proxies(protocol=protocol, domain=domain, count=count, nick_type=nick_type)
    # 从proxy_list列表中, 随机取出一个代理IP返回
    return random.choice(proxy_list)


def disable_domain(self, ip, domain):
    """
    3.4 实现把指定域名添加到指定IP的disable_domain列表中.
    :param ip: IP地址
    :param domain: 域名
    :return: 如果返回True, 就表示添加成功了, 返回False添加失败了
    """
    # print(self.proxies.count_documents({'_id': ip, 'disable_domains':domain}))

    if self.proxies.count_documents({'_id': ip, 'disable_domains':domain}) == 0:
        # 如果disable_domains字段中没有这个域名, 才添加
        self.proxies.update_one({'_id':ip}, {'$push': {'disable_domains': domain}})
        return True
    return False

if name == ‘main‘: mongo = MongoPool()

# proxy = Proxy('202.104.113.35', port='53281')
# proxy = Proxy('202.104.113.36', port='53281')
# mongo.insert_one(proxy)
# proxy = Proxy('202.104.113.35', port='8888')
# mongo.update_one(proxy)

# proxy = Proxy('202.104.113.35', port='8888')
# mongo.delete_one(proxy)

# for proxy in mongo.find_all():
#     print(proxy)

# dic = { "ip" : "202.104.113.38", "port" : "53281", "protocol" : 0, "nick_type" : 0, "speed" : 8.2, "area" : None, "score" : 50, "disable_domains" : [ "jd.com"] }
# dic = { "ip" : "202.104.113.39", "port" : "53281", "protocol" : 1, "nick_type" : 0, "speed" : 1.2, "area" : None, "score" : 50, "disable_domains" : [ "taobao.com"] }
# dic = { "ip" : "202.104.113.40", "port" : "53281", "protocol" : 2, "nick_type" : 0, "speed" : 4.0, "area" : None, "score" : 50, "disable_domains" : []}
# dic = { "ip" : "202.104.113.41", "port" : "53281", "protocol" : 2, "nick_type" : 0, "speed" : -1, "area" : None, "score" : 49, "disable_domains" : []}
# proxy = Proxy(**dic)
# mongo.insert_one(proxy)

# for proxy in mongo.find():
# for proxy in mongo.find({'protocol':2}, count=1):
#     print(proxy)

# for proxy in mongo.get_proxies(protocol='https'):
# for proxy in mongo.get_proxies(protocol='http', domain='taobao.com'):
#     print(proxy)

# mongo.disable_domain('202.104.113.38', 'taobao.com')
<a name="MfYVd"></a>
# 7. 实现代理池的爬虫模块
<a name="zl89p"></a>
## 7.1 爬虫模块的需求

- 需求: 抓取各个代理IP网站上的免费代理IP, 进行检测, 如果可用存储到数据库中
- 需要抓取代理IP的页面如下:
   - 西刺代理:[https://www.xicidaili.com/nn/1](https://www.xicidaili.com/nn/1)
   - ip3366代理:[http://www.ip3366.net/free/?stype=1&page=1](http://www.ip3366.net/free/?stype=1&page=1)
   - 快代理:[https://www.kuaidaili.com/free/inha/1/](https://www.kuaidaili.com/free/inha/1/)
   - proxylistplus代理:[https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1](https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1)
   - 66ip代理: [http://www.66ip.cn/1.html](http://www.66ip.cn/1.html)
<a name="eOPsH"></a>
## 7.2 爬虫模块的设计思路

- 通用爬虫:通过指定URL列表, 分组XPATH和组内XPATH, 来提取不同网站的代理IP
   - 原因代理IP网站的页面结构几乎都是Table, 页面结构类似
- 具体爬虫: 用于抓取具体代理IP网站 
   - 通过继承通用爬虫实现具体网站的抓取, 一般只需要指定爬取的URL列表, 分组的XPATH和组内XPATH就可以了. 
   - 如果该网站有特殊反爬手段, 可以通过重写某些方法实现反爬
- 爬虫运行模块: 启动爬虫, 抓取代理IP, 进行检测, 如果可用, 就存储到数据库中;
   - 通过配置文件来控制启动哪些爬虫, 增加扩展性; 如果将来我们遇到返回json格式的代理网站, 单独写一个爬虫配置下就好了.
<a name="NEEQ7"></a>
## 7.3 实现通用爬虫

- 目标: 实现可以指定不同URL列表, 分组的XPATH和详情的XPATH, 从不同页面上提取代理的IP,端口号和区域的通用爬虫; 
- 步骤:
   1. 在base_spider.py文件中,定义一个BaseSpider类, 继承object
   1. 提供三个类成员变量:
      - urls: 代理IP网址的URL的列表
      - group_xpath: 分组XPATH, 获取包含代理IP信息标签列表的XPATH
      - detail_xpath: 组内XPATH, 获取代理IP详情的信息XPATH, 格式为: {'ip':'xx', 'port':'xx', 'area':'xx'}
   3. 提供初始方法, 传入爬虫URL列表, 分组XPATH, 详情(组内)XPATH
   3. 对外提供一个获取代理IP的方法 
      - 遍历URL列表, 获取URL
      - 根据发送请求, 获取页面数据
      - 解析页面, 提取数据, 封装为Proxy对象
      - 返回Proxy对象列表
```python
import requests
from utils.http import get_request_headers
from lxml import etree
from domain import Proxy


# 1. 在base_spider.py文件中,定义一个BaseSpider类, 继承object
class BaseSpider(object):

    # 2. 提供三个类成员变量:
    # urls: 代理IP网址的URL的列表
    urls = []
    # group_xpath: 分组XPATH, 获取包含代理IP信息标签列表的XPATH
    group_xpath = ''
    # detail_xpath: 组内XPATH, 获取代理IP详情的信息XPATH, 格式为: {'ip':'xx', 'port':'xx', 'area':'xx'}
    detail_xpath = {}

    # 3. 提供初始方法, 传入爬虫URL列表, 分组XPATH, 详情(组内)XPATH
    def __init__(self, urls=[], group_xpath='', detail_xpath={}):

        if urls:
            self.urls = urls

        if group_xpath:
            self.group_xpath = group_xpath

        if detail_xpath:
            self.detail_xpath = detail_xpath

    def get_page_from_url(self, url):
        """根据URL 发送请求, 获取页面数据"""
        response = requests.get(url, headers=get_request_headers())
        print(url)
        print(response.status_code)
        return response.content

    def get_first_from_list(self, lis):
        # 如果列表中有元素就返回第一个, 否则就返回空串
        return lis[0] if len(lis) != 0 else ''

    def get_proxies_from_page(self, page):
        """解析页面, 提取数据, 封装为Proxy对象"""
        element = etree.HTML(page)
        # 获取包含代理IP信息的标签列表
        trs = element.xpath(self.group_xpath)
        # 遍历trs, 获取代理IP相关信息
        for tr in trs:
            ip = self.get_first_from_list(tr.xpath(self.detail_xpath['ip']))
            port = self.get_first_from_list(tr.xpath(self.detail_xpath['port']))
            area = self.get_first_from_list(tr.xpath(self.detail_xpath['area']))
            proxy = Proxy(ip, port, area=area)
            # print(proxy)
            # 使用yield返回提取到的数据
            yield proxy

    def get_proxies(self):
        # 4. 对外提供一个获取代理IP的方法
        # 4.1 遍历URL列表, 获取URL
        for url in self.urls:
            # print(url)
            # 4.2 根据发送请求, 获取页面数据
            page = self.get_page_from_url(url)
            # 4.3 解析页面, 提取数据, 封装为Proxy对象
            proxies = self.get_proxies_from_page(page)
            # 4.4 返回Proxy对象列表
            yield from proxies

if __name__ == '__main__':

    config = {
        'urls': ['http://www.ip3366.net/free/?stype=1&page={}'.format(i) for i in range(1, 4)],
        'group_xpath': '//*[@id="list"]/table/tbody/tr',
        'detail_xpath': {
            'ip':'./td[1]/text()',
            'port':'./td[2]/text()',
            'area':'./td[5]/text()'
        }
    }

    spider = BaseSpider(**config)
    for proxy in spider.get_proxies():
        print(proxy)

7.4 实现具体的爬虫类

  • 目标: 通过继承通用爬虫, 实现多个具体爬虫, 分别从各个免费代理IP网站上抓取代理IP
  • 步骤:
    1. 实现西刺代理爬虫: http://www.xicidaili.com/nn/1
      • 定义一个类,继承通用爬虫类(BasicSpider)
      • 提供urls, group_xpath 和 detail_xpath
    2. 实现ip3366代理爬虫: http://www.ip3366.net/free/?stype=1&page=1
      • 定义一个类,继承通用爬虫类(BasicSpider)
      • 提供urls, group_xpath 和 detail_xpath
    3. 实现快代理爬虫: https://www.kuaidaili.com/free/inha/1/
      • 定义一个类,继承通用爬虫类(BasicSpider)
      • 提供urls, group_xpath 和 detail_xpath
    4. 实现proxylistplus代理爬虫: https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1
      • 定义一个类,继承通用爬虫类(BasicSpider)
      • 提供urls, group_xpath 和 detail_xpath
    5. 实现66ip爬虫: http://www.66ip.cn/1.html
      • 定义一个类,继承通用爬虫类(BasicSpider)
      • 提供urls, group_xpath 和 detail_xpath
      • 由于66ip网页进行js + cookie反爬, 需要重写父类的get_page_from_url方法 ```python import time import random import requests import re import js2py

from core.proxy_spider.base_spider import BaseSpider from utils.http import get_request_headers

“””

  1. 实现西刺代理爬虫: http://www.xicidaili.com/nn/1 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “””

class XiciSpider(BaseSpider):

# 准备URL列表
urls = ['https://www.xicidaili.com/nn/{}'.format(i) for i in range(1, 11)]
# 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="ip_list"]/tr[position()>1]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
    'ip':'./td[2]/text()',
    'port':'./td[3]/text()',
    'area':'./td[4]/a/text()'
}

“””

  1. 实现ip3366代理爬虫: http://www.ip3366.net/free/?stype=1&page=1 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “”” class Ip3366Spider(BaseSpider):

    准备URL列表

    urls = [‘http://www.ip3366.net/free/?stype={}&page={}'.format(i, j) for i in range(1, 4, 2) for j in range(1, 8)]

    # 分组的XPATH, 用于获取包含代理IP信息的标签列表

    group_xpath = ‘//*[@id=”list”]/table/tbody/tr’

    组内的XPATH, 用于提取 ip, port, area

    detail_xpath = {
     'ip':'./td[1]/text()',
     'port':'./td[2]/text()',
     'area':'./td[5]/text()'
    
    }

“””

  1. 实现快代理爬虫: https://www.kuaidaili.com/free/inha/1/ 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “”” class KaiSpider(BaseSpider):

    准备URL列表

    urls = [‘https://www.kuaidaili.com/free/inha/{}/'.format(i) for i in range(1, 6)]

    # 分组的XPATH, 用于获取包含代理IP信息的标签列表

    group_xpath = ‘//*[@id=”list”]/table/tbody/tr’

    组内的XPATH, 用于提取 ip, port, area

    detail_xpath = {

     'ip':'./td[1]/text()',
     'port':'./td[2]/text()',
     'area':'./td[5]/text()'
    

    }

    当我们两个页面访问时间间隔太短了, 就报错了; 这是一种反爬手段.

    def get_page_from_url(self, url):

     # 随机等待1,3s
     time.sleep(random.uniform(1, 3))
     # 调用父类的方法, 发送请求, 获取响应数据
     return super().get_page_from_url(url)
    

“””

  1. 实现proxylistplus代理爬虫: https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath “””

class ProxylistplusSpider(BaseSpider):

# 准备URL列表
urls = ['https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-{}'.format(i) for i in range(1, 7)]
# # 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="page"]/table[2]/tr[position()>2]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
    'ip':'./td[2]/text()',
    'port':'./td[3]/text()',
    'area':'./td[5]/text()'
}

“””

  1. 实现66ip爬虫: http://www.66ip.cn/1.html 定义一个类,继承通用爬虫类(BasicSpider) 提供urls, group_xpath 和 detail_xpath 由于66ip网页进行js + cookie反爬, 需要重写父类的get_page_from_url方法 “””

class Ip66Spider(BaseSpider):

# 准备URL列表
urls = ['http://www.66ip.cn/{}.html'.format(i) for i in range(1, 11)]
# # 分组的XPATH, 用于获取包含代理IP信息的标签列表
group_xpath = '//*[@id="main"]/div/div[1]/table/tr[position()>1]'
# 组内的XPATH, 用于提取 ip, port, area
detail_xpath = {
    'ip':'./td[1]/text()',
    'port':'./td[2]/text()',
    'area':'./td[3]/text()'
}

# 重写方法, 解决反爬问题
def get_page_from_url(self, url):
    headers = get_request_headers()
    response = requests.get(url, headers=headers)
    if response.status_code == 521:
        # 生成cookie信息, 再携带cookie发送请求
        # 生成 `_ydclearance` cookie信息
        # 1. 确定 _ydclearance 是从哪里来的;
        # 观察发现: 这个cookie信息不使用通过服务器响应设置过来的; 那么他就是通过js生成.
        # 2. 第一次发送请求的页面中, 有一个生成这个cookie的js; 执行这段js, 生成我们需要的cookie
        # 这段js是经过加密处理后的js, 真正js在 "po" 中.
        # 提取 `jp(107)` 调用函数的方法, 以及函数
        result = re.findall('window.onload=setTimeout\("(.+?)", 200\);\s*(.+?)\s*</script> ', response.content.decode('GBK'))
        # print(result)
        # 我希望执行js时候, 返回真正要执行的js
        # 把 `eval("qo=eval;qo(po);")` 替换为 return po
        func_str = result[0][1]
        func_str = func_str.replace('eval("qo=eval;qo(po);")', 'return po')
        # print(func_str)
        # 获取执行js的环境
        context = js2py.EvalJs()
        # 加载(执行) func_str
        context.execute(func_str)
        # 执行这个方法, 生成我们需要的js
        # code = gv(50)
        context.execute('code = {};'.format(result[0][0]))
        # 打印最终生成的代码
        # print(context.code)
        cookie_str = re.findall("document.cookie='(.+?); ", context.code)[0]
        # print(cookie_str)
        headers['Cookie'] = cookie_str
        response = requests.get(url, headers=headers)
        return response.content.decode('GBK')
    else:
        return response.content.decode('GBK')

if name == ‘main‘:

# spider = XiciSpider()
# spider = Ip3366Spider()
# spider = KaiSpider()
# spider = ProxylistplusSpider()

spider = Ip66Spider()
for proxy in spider.get_proxies():
    print(proxy)

# print(Ip3366Spider.urls)

# # 测试: http://www.66ip.cn/1.html
# url = 'http://www.66ip.cn/1.html'
# headers = {
#     'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
#     # 'Cookie': '_ydclearance=35fd4248c8889feb58597e27-a31e-4f84-9edc-f1a22c16a949-1546164684;'
# }
# response = requests.get(url, headers=headers)
# print(response.status_code)
# text = response.content.decode('GBK')
#
# # 生成 `_ydclearance` cookie信息
# # 1. 确定 _ydclearance 是从哪里来的;
# # 观察发现: 这个cookie信息不使用通过服务器响应设置过来的; 那么他就是通过js生成.
# # 2. 第一次发送请求的页面中, 有一个生成这个cookie的js; 执行这段js, 生成我们需要的cookie
# # 这段js是经过加密处理后的js, 真正js在 "po" 中.
# # 提取 `jp(107)` 调用函数的方法, 以及函数
# result = re.findall('window.onload=setTimeout\("(.+?)", 200\);\s*(.+?)\s*</script> ' ,text)
# # print(result)
# # 我希望执行js时候, 返回真正要执行的js
# # 把 `eval("qo=eval;qo(po);")` 替换为 return po
# func_str = result[0][1]
# func_str = func_str.replace('eval("qo=eval;qo(po);")', 'return po')
# # print(func_str)
# # 获取执行js的环境
# context = js2py.EvalJs()
# # 加载(执行) func_str
# context.execute(func_str)
# # 执行这个方法, 生成我们需要的js
# # code = gv(50)
# context.execute('code = {};'.format(result[0][0]))
# # 打印最终生成的代码
# # print(context.code)
# cookie_str = re.findall("document.cookie='(.+?); ", context.code)[0]
# # print(cookie_str)
# headers['Cookie'] = cookie_str
# response = requests.get(url, headers=headers)
# print(response.content.decode('GBK'))
<a name="q0xUO"></a>
## 7.5 实现运行爬虫模块

- 目标: 根据配置文件信息, 加载爬虫, 抓取代理IP, 进行校验, 如果可用, 写入到数据库中
- 思路: 
   - 在run_spider.py中, 创建RunSpider类
   - 提供一个运行爬虫的run方法, 作为运行爬虫的入口, 实现核心的处理逻辑
      - 根据配置文件信息, 获取爬虫对象列表.
      - 获取爬虫对象, 遍历爬虫对象的get_proxies方法, 获取代理IP
      - 检测代理IP(代理IP检测模块)
      - 如果可用,写入数据库(数据库模块)
      - 处理异常, 防止一个爬虫内部出错了, 影响其他的爬虫. 
   - 使用异步来执行每一个爬虫任务, 以提高抓取代理IP效率
      - 在init方法中创建协程池对象
      - 把处理一个代理爬虫的代码抽到一个方法
      - 使用异步执行这个方法
      - 调用**协程的join方法**, 让当前线程等待 协程 任务的完成. 
   - 使用schedule模块, 实现每隔一定的时间, 执行一次爬取任务
      - 定义一个start的类方法
      - 创建当前类的对象, 调用run方法
      - 使用schedule模块, 每隔一定的时间, 执行当前对象的run方法
- 步骤:
   - 在run_spider.py中, 创建RunSpider类
   - 修改 setting.py 增加 代理IP爬虫的配置信息
```python
PROXIES_SPIDERS = [
    # 爬虫的全类名,路径: 模块.类名
    'core.proxy_spider.proxy_spiders.Ip66Spider',
    'core.proxy_spider.proxy_spiders.Ip3366Spider',
    'core.proxy_spider.proxy_spiders.KaiSpider',
    'core.proxy_spider.proxy_spiders.ProxylistplusSpider',
    'core.proxy_spider.proxy_spiders.XiciSpider',
]

实现根据配置文件, 加载爬虫, 把爬虫对象放到列表中的方法

  1. 定义一个列表, 用于存储爬虫对象
  2. 遍历爬虫配置信息, 获取每一个爬虫路径
  3. 根据爬虫路径获取模块名和类名
  4. 使用importlib根据模块名, 导入该模块
  5. 根据类名, 从模块中获取类
  6. 使用类创建对象, 添加到对象列表中

    def _auto_import_instances(self):
     """根据配置信息, 自动导入爬虫"""
     instances = []
     # 遍历配置的爬虫, 获取爬虫路径
     for path in settings.PROXIES_SPIDERS:
         # 根据路径, 获取模块名 和 类名
         module_name, cls_name = path.rsplit('.', maxsplit=1)
         # 根据模块名导入模块
         module = importlib.import_module(module_name)
         # 根据类名, 从模块中, 获取爬虫类
         cls = getattr(module, cls_name)
         # 创建爬虫对象, 添加到列表中
         instances.append(cls())
     # 返回爬虫对象列表
     return instances
    

    实现run方法, 用于运行整个爬虫

  7. 获取代理爬虫列表

  8. 遍历代理爬虫列表
  9. 遍历爬虫的get_proxies()方法, 获取代理IP
  10. 如果代理IP为None, 继续一次循环
  11. 检查代理, 获取代理协议类型, 匿名程度, 和速度
  12. 如果代理速度不为-1, 就是说明该代理可用, 保存到数据库中
  13. 处理下异常, 防止一个爬虫内部错误, 其他爬虫都运行不了

    class RunSpider(object):
    def __init__(self):
       self.proxy_pool = MongoPool()
    
    def run(self):
      """启动爬虫"""
      # 获取代理爬虫
      spiders = self._auto_import_instances()
      # 执行爬虫获取代理
      for spider in spiders:
          try:
              for proxy in spider.get_proxies():
                  if proxy is None:
                      # 如果是None继续一个
                      continue
                  # 检查代理, 获取代理协议类型, 匿名程度, 和速度
                  proxy = check_proxy(proxy)
                  # 如果代理速度不为-1, 就是说明该代理可用
                  if proxy.speed != -1:
                      # 保存该代理到数据库中
                      self.proxy_pool.save(proxy)
          except Exception as e:
              logger.exception(e)
              logger.exception("爬虫{} 出现错误".format(spider))
    

    使用协程池异步来运行每一个爬虫, 以提高爬取的速度

  14. 实现init方法, 创建协程池

  15. 把执行处理每一个爬虫的代码抽取一个方法
  16. 使用异步调用这个方法
  17. 调用协程的join方法, 让当前线程等待队列任务的完成.

    class RunSpider(object):
    def __init__(self):
       self.proxy_pool = MongoPool()
       self.pool = Pool()
    
    ...
    
    def run(self):
       """启动爬虫"""
       # 获取代理爬虫
       spiders = self._auto_import_instances()
       # 执行爬虫获取代理
       for spider in spiders:
          # 使用协程异步调用该方法,提高爬取的效率
          self.pool.apply_async(self.__run_one_spider, args=(spider, ))
    
       # 等待所有爬虫任务执行完毕
       self.pool.join()
    
    def __run_one_spider(self, spider):
       try:
           for proxy in spider.get_proxies():
               if proxy is None:
                   # 如果是None继续一个
                   continue
               # 检查代理, 获取代理协议类型, 匿名程度, 和速度
               proxy = check_proxy(proxy)
               # 如果代理速度不为-1, 就是说明该代理可用
               if proxy.speed != -1:
                   # 保存该代理到数据库中
                   self.proxy_pool.save(proxy)
       except Exception as e:
           logger.exception(e)
           logger.exception("爬虫{} 出现错误".format(spider))
    

    每隔一定的时间, 执行一次爬取任务

  18. 修改 setting.py 文件, 爬虫间隔时间的配置

    # 抓取IP的时间间隔, 单位小时
     SPIDER_INTERVAL = 2
    
  19. 安装 schedule: pip install schedule

  20. 在 RunSpider 中提供start的类方法, 用于启动爬虫的运行, 每间隔指定时间, 重新运行一次.

    @classmethod
    def start(cls):
      # 创建本类对象
      run_spider = RunSpider()
      run_spider.run()
    
      # 每隔 SPIDER_INTERVAL 小时检查下代理是否可用
      schedule.every(settings.SPIDER_INTERVAL).hours.do(run_spider.run)
      while True:
          schedule.run_pending()
          time.sleep(1)
    

    爬虫运行模块完整代码 ```python

    打猴子补丁

    from gevent import monkey monkey.patch_all()

    导入协程池

    from gevent.pool import Pool import importlib import schedule import time

from settings import PROXIES_SPIDERS from core.proxy_validate.httpbin_validator import check_proxy from core.db.mongo_pool import MongoPool from utils.log import logger from settings import RUN_SPIDERS_INTERVAL

class RunSpider(object):

def __init__(self):
    # 创建MongoPool对象
    self.mongo_pool = MongoPool()
    # 3.1 在init方法中创建协程池对象
    self.coroutine_pool = Pool()

def get_spider_from_settings(self):
    """根据配置文件信息, 获取爬虫对象列表."""
    # 遍历配置文件中爬虫信息, 获取每个爬虫全类名
    for full_class_name in PROXIES_SPIDERS:
        # core.proxy_spider.proxy_spiders.Ip66Spider
        # 获取模块名 和 类名
        module_name, class_name = full_class_name.rsplit('.', maxsplit=1)
        # 根据模块名, 导入模块
        module = importlib.import_module(module_name)
        # 根据类名, 从模块中, 获取类
        cls = getattr(module, class_name)
        # 创建爬虫对象
        spider = cls()
        # print(spider)
        yield spider


def run(self):
    # 2.1 根据配置文件信息, 获取爬虫对象列表.
    spiders = self.get_spider_from_settings()
    # 2.2 遍历爬虫对象列表, 获取爬虫对象, 遍历爬虫对象的get_proxies方法, 获取代理IP
    for spider in spiders:
        #  2.5 处理异常, 防止一个爬虫内部出错了, 影响其他的爬虫.
        # 3.3 使用异步执行这个方法
        # self.__execute_one_spider_task(spider)
        self.coroutine_pool.apply_async(self.__execute_one_spider_task,args=(spider, ))

    # 3.4 调用协程的join方法, 让当前线程等待 协程 任务的完成.
    self.coroutine_pool.join()

def __execute_one_spider_task(self, spider):
    # 3.2 把处理一个代理爬虫的代码抽到一个方法
    # 用于处理一个爬虫任务的.
    try:
        # 遍历爬虫对象的get_proxies方法, 获取代理I
        for proxy in spider.get_proxies():
            # print(proxy)
            # 2.3 检测代理IP(代理IP检测模块)
            proxy = check_proxy(proxy)
            # 2.4 如果可用,写入数据库(数据库模块)
            # 如果speed不为-1, 就说明可用
            if proxy.speed != -1:
                # 写入数据库(数据库模块)
                self.mongo_pool.insert_one(proxy)
    except Exception as ex:
        logger.exception(ex)

@classmethod
def start(cls):
    # 4. 使用schedule模块, 实现每隔一定的时间, 执行一次爬取任务
    # 4.1 定义一个start的类方法
    # 4.2 创建当前类的对象, 调用run方法
    rs = RunSpider()
    rs.run()
    # 4.3 使用schedule模块, 每隔一定的时间, 执行当前对象的run方法
    # 4.3.1 修改配置文件, 增加爬虫运行时间间隔的配置, 单位为小时
    schedule.every(RUN_SPIDERS_INTERVAL).hours.do(rs.run)
    while True:
        schedule.run_pending()
        time.sleep(1)

if name == ‘main‘:

# rs = RunSpider()
# rs.run()
RunSpider.start()

# 测试schedule
# def task():
#     print('呵呵')
#
# schedule.every(10).seconds.do(task)
# while True:
#     schedule.run_pending()
#     time.sleep(1)
<a name="WQN1K"></a>
# 8. 实现代理池的检测模块

- 目的: 检查代理IP可用性, 保证代理池中代理IP基本可用
- 思路
   1. 在proxy_test.py中, 创建ProxyTester类
   1. 提供一个 run 方法, 用于处理检测代理IP核心逻辑
      1. 从数据库中获取所有代理IP
      1. 遍历代理IP列表
      1. 检查代理可用性
         - 如果代理不可用, 让代理分数-1, 如果代理分数等于0就从数据库中删除该代理, 否则更新该代理IP
         - 如果代理可用, 就恢复该代理的分数, 更新到数据库中
   3. 为了提高检查的速度, 使用异步来执行检测任务
      1. 把要检测的代理IP, 放到队列中
      1. 把检查一个代理可用性的代码, 抽取到一个方法中; 从队列中获取代理IP, 进行检查; 检查完毕, 调度队列的task_done方法
      1. 通过异步回调, 使用死循环不断执行这个方法,
      1. 开启多个一个异步任务, 来处理代理IP的检测; 可以通过配置文件指定异步数量 
   4. 使用schedule模块, 每隔一定的时间, 执行一次检测任务
      1. 定义类方法 start, 用于启动检测模块
      1. 在start方法中
         1. 创建本类对象
         1. 调用run方法
         1. 每间隔一定时间, 执行一下, run方法
- 步骤
   - 在proxy_test.py中, 创建ProxyTester类
   - 提供一个 run 方法, 用于检查代理IP的可用性
```python
import time
import requests
import json

from utils.http import get_request_headers
from settings import TEST_TIMEOUT
from utils.log import logger
from domain import Proxy


def check_proxy(proxy):
    """
    用于检查指定 代理IP 响应速度, 匿名程度, 支持协议类型
    :param proxy: 代理IP模型对象
    :return: 检查后的代理IP模型对象
    """

    # 准备代理IP字典
    proxies = {
        'http':'http://{}:{}'.format(proxy.ip, proxy.port),
        'https':'https://{}:{}'.format(proxy.ip, proxy.port),
    }

    # 测试该代理IP
    http, http_nick_type, http_speed = __check_http_proxies(proxies)
    https, https_nick_type, https_speed = __check_http_proxies(proxies, False)
    # 代理IP支持的协议类型, http是0, https是1, https和http都支持是2
    if http and https:
        proxy.protocol = 2
        proxy.nick_type = http_nick_type
        proxy.speed = http_speed
    elif http:
        proxy.protocol = 0
        proxy.nick_type = http_nick_type
        proxy.speed = http_speed
    elif https:
        proxy.protocol = 1
        proxy.nick_type = https_nick_type
        proxy.speed = https_speed
    else:
        proxy.protocol = -1
        proxy.nick_type = -1
        proxy.speed = -1

    return proxy


def __check_http_proxies(proxies, is_http=True):
    # 匿名类型: 高匿: 0, 匿名: 1, 透明: 2
    nick_type = -1
    # 响应速度, 单位s
    speed = -1

    if is_http:
        test_url = 'http://httpbin.org/get'
    else:
        test_url = 'https://httpbin.org/get'

    try:
        # 获取开始时间
        start = time.time()
        # 发送请求, 获取响应数据
        response = requests.get(test_url, headers=get_request_headers(), proxies=proxies, timeout=TEST_TIMEOUT)

        if response.ok:
            # 计算响应速度
            speed =  round(time.time() - start, 2)
            # 匿名程度
            # 把响应的json字符串, 转换为字典
            dic = json.loads(response.text)
            # 获取来源IP: origin
            origin = dic['origin']
            proxy_connection = dic['headers'].get('Proxy-Connection', None)
            if ',' in origin:
                #    1. 如果 响应的origin 中有','分割的两个IP就是透明代理IP
                nick_type = 2
            elif proxy_connection:
                #    2. 如果 响应的headers 中包含 Proxy-Connection 说明是匿名代理IP
                nick_type = 1
            else:
                #  3. 否则就是高匿代理IP
                nick_type = 0

            return True, nick_type, speed
        return False, nick_type, speed
    except Exception as ex:
        # logger.exception(ex)
        return False, nick_type, speed


if __name__ == '__main__':

    # proxy = Proxy('202.104.113.35', port='53281')
    proxy = Proxy('61.135.217.7', port='80')
    print(check_proxy(proxy))

9. 实现代理池的API模块

  • 目标:
    • 为爬虫提供高可用代理IP的服务接口
  • 步骤:
    • 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
    • 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
    • 实现给指定的IP上追加不可用域名的服务
  • 实现:
    • 在proxy_api.py中, 创建ProxyApi类
    • 实现初始方法
      • 初始一个Flask的Web服务
      • 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
        • 可用通过 protocol 和 domain 参数对IP进行过滤
        • protocol: 当前请求的协议类型
        • domain: 当前请求域名
      • 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
        • 可用通过protocol 和 domain 参数对IP进行过滤
      • 实现给指定的IP上追加不可用域名的服务
        • 如果在获取IP的时候, 有指定域名参数, 将不在获取该IP, 从而进一步提高代理IP的可用性.
    • 实现run方法, 用于启动Flask的WEB服务
    • 实现start的类方法, 用于通过类名, 启动服务 ```python from flask import Flask from flask import request import json

from core.db.mongo_pool import MongoPool from settings import PROXIES_MAX_COUNT

1. 在proxy_api.py中, 创建ProxyApi类

class ProxyApi(object):

def __init__(self):
    # 2. 实现初始方法
    # 2.1 初始一个Flask的Web服务
    self.app = Flask(__name__)
    # 创建MongoPool对象, 用于操作数据库
    self.mongo_pool = MongoPool()

    @self.app.route('/random')
    def random():
        """
        2.2 实现根据协议类型和域名, 提供随机的获取高可用代理IP的服务
            可用通过 protocol 和 domain 参数对IP进行过滤
            protocol: 当前请求的协议类型
            domain: 当前请求域名
        """
        protocol = request.args.get('protocol')
        domain = request.args.get('domain')
        proxy = self.mongo_pool.random_proxy(protocol, domain, count=PROXIES_MAX_COUNT)

        if protocol:
            return '{}://{}:{}'.format(protocol, proxy.ip, proxy.port)
        else:
            return '{}:{}'.format(proxy.ip, proxy.port)

    @self.app.route('/proxies')
    def proxies():
        """
            2.3 实现根据协议类型和域名, 提供获取多个高可用代理IP的服务
            可用通过protocol 和 domain 参数对IP进行过滤
            实现给指定的IP上追加不可用域名的服务
        """
        # 获取协议: http/https
        protocol = request.args.get('protocol')
        # 域名: 如:jd.com
        domain = request.args.get('domain')

        proxies = self.mongo_pool.get_proxies(protocol, domain, count=PROXIES_MAX_COUNT)
        # proxies 是一个 Proxy对象的列表, 但是Proxy对象不能进行json序列化, 需要转换为字典列表
        # 转换为字典列表
        proxies = [proxy.__dict__ for proxy in proxies]
        # 返回json格式值串
        return json.dumps(proxies)

    @self.app.route('/disable_domain')
    def disable_domain():
        """
        2.4 如果在获取IP的时候, 有指定域名参数, 将不在获取该IP, 从而进一步提高代理IP的可用性.
        """
        ip = request.args.get('ip')
        domain = request.args.get('domain')

        if ip is None:
            return '请提供ip参数'
        if domain is None:
            return '请提供域名domain参数'

        self.mongo_pool.disable_domain(ip, domain)
        return "{} 禁用域名 {} 成功".format(ip, domain)


def run(self):
    """3. 实现run方法, 用于启动Flask的WEB服务"""
    self.app.run('0.0.0.0', port=16888)

@classmethod
def start(cls):
    # 4. 实现start的类方法, 用于通过类名, 启动服务
    proxy_api = cls()
    proxy_api.run()

if name == ‘main‘:

# proxy_api = ProxyApi()
# proxy_api.run()
ProxyApi.start()
<a name="M4y1L"></a>
# 10. 实现代理池的启动入口

- 目标: 把启动爬虫, 启动检测代理IP, 启动WEB服务 统一到一起
- 思路:
   - 开启三个进程, 分别用于启动爬虫, 检测代理IP, WEB服务
- 步骤:
   - 定义一个run方法用于启动动代理池
      - 定义一个列表, 用于存储要启动的进程
      - 创建 启动爬虫 的进程, 添加到列表中
      - 创建 启动检测 的进程, 添加到列表中
      - 创建 启动提供API服务 的进程, 添加到列表中
      - 遍历进程列表, 启动所有进程
      - 遍历进程列表, 让主进程等待子进程的完成
   - 在 if __name__ == '__main__': 中调用run方法
```python
from multiprocessing import Process
from core.proxy_spider.run_spiders import RunSpider
from core.proxy_test import ProxyTester
from core.proxy_api import ProxyApi


def run():
    # 1. 定义一个列表, 用于存储要启动的进程
    process_list = []
    # 2. 创建 启动爬虫 的进程, 添加到列表中
    process_list.append(Process(target=RunSpider.start))
    # 3. 创建 启动检测 的进程, 添加到列表中
    process_list.append(Process(target=ProxyTester.start))
    # 4. 创建 启动提供API服务 的进程, 添加到列表中
    process_list.append(Process(target=ProxyApi.start))

    # 5. 遍历进程列表, 启动所有进程
    for process in process_list:
        # 设置守护进程
        process.daemon = True
        process.start()

    # 6. 遍历进程列表, 让主进程等待子进程的完成
    for process in process_list:
        process.join()

if __name__ == '__main__':
    run()

11.代码

代码