python3编写网络爬虫18-代理池的维护

一、代理池的维护

上面我们利用代理可以解决目标网站封IP的问题 在网上有大量公开的免费代理 或者我们也可以购买付费的代理IP
但是无论是免费的还是付费的,都不能保证都是可用的 因为可能此IP被其他人使用来爬取同样的目标站点而被封禁
或者代理服务器突然故障或者网络繁忙 一旦选用了一个不可用的代理,这势必会影响爬虫的工作效率

1.准备工作

需要安装Redis数据库并启动服务 另外还需要安装aiohttp、requests、redis-py、pyquery、flask库

redis数据库安装 下载地址

http://github.com/MSOpenTech/redis/releases 

下载对应版本 next安装

aiohttp(之前说过requests库是一个阻塞式HTTP请求库,当发送一个请求后 程序会一直等待服务器响应
直到得到响应后 程序才会进行下一步处理 aiohttp提供异步Web服务的库 使用异步请求库进行数据抓取时会大大提高效率)

安装

 pip install aiohttp

验证 import aiohttp 无报错

requests 安装 

pip install requests

验证 import requests

redis-py 安装 

pip install redis

验证

import redis 
redis.VERSION

 输出版本信息表示安装成功

pyquery 安装 pip install

验证 import pyquery 无报错

flask (Flask 是一个轻量级的Web服务程序 简单 易用 灵活 这里主要做一些API服务)

安装 

pip install flask

验证 创建test.py文件 代码如下:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()

可以发现 系统会在5000端口开启Web服务 直接访问 http://127.0.0.1:5000/


2.代理池目标 基本思路

需要分几个步骤 来实现易用高效的代理池

基本模块分4块

1.存储模块:负责存储抓取下来的代理 保证代理不重复 标识代理可用情况 动态实时处理每个代理
比较高效和方便的存储方式是使用redis的SortedSet 有序集合

2.获取模块: 需要定时在各大代理网站抓取代理,代理可以是免费的也可以是付费的,代理形式都是IP加端口
此模块尽量从不同的来源获取,尽量抓取高匿代理 抓取成功后将可用代理保存到数据库中

3.检测模块: 需要定时检测数据库中的代理 需要设置一个检测链接 (最好爬取哪个网站就检测哪个网站 这样更有针对性)
如果要设置一个通用型代理 可以设置百度等链接检测 另外需要标识每个代理状态 设置分数标识 100分代表可用
分数越少越不可用 检测一次 如果代理可用 可以将分数标识立即设置为100满分 也可以在原基础上加1
如果代理不可用 可以将分数减1 当分数到一定阀值 代理就从数据库移除 通过分数标识 就可以辨别代理的可用
情况 选用时更有针对性

4.接口模块:需要API来提供对外服务的接口 其实可以直接连接数据库来选取对应的数据 但这样就需要数据库的连接信息
并且配置连接 比较安全和方便的方式就是 提供一个Web API 接口 通过访问接口即可拿到可用代理
这样就能保证每个可用代理都可以拿到 实现负载均衡

3.代理池架构

看图

存储 使用 redis有序集合 来做代理去重和状态标识 也是中心模块和基础模块 将其他模块串联起来

获取 定时从代理网站获取代理 将获取的代理传递给存储模块 并保存到数据库

检测 定时通过存储模块获取所有代理 并对代理进行检测 根据检测结果对代理设置不同标识

接口 通过Web API 提供服务接口 接口通过连接数据库并通过Web形式返回可用代理


4.代理池实现

存储模块

定义常量 最大分数 最小分数 初始分数 以及redis连接信息 地址 端口 密码 redis_key 是有序集合的键名
可以通过它获取代理存储所使用的有序集合

# Redis数据库地址

REDIS_HOST = '127.0.0.1'

# Redis端口

REDIS_PORT = 6379

# Redis密码,如无填None

REDIS_PASSWORD = None

REDIS_KEY = 'proxies'

# 代理分数

MAX_SCORE = 100
MIN_SCORE = 0
INITIAL_SCORE = 10

import redis
from random import choice

定义redisclient类 操作redis有序集合

class RedisClient(object):
  #初始化方法 参数是连接redis的连接信息 
  def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
    """
      初始化
      :param host: Redis 地址
      :param port: Redis 端口
      :param password: Redis密码
    """
    self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
  #向数据库添加代理并设置分数 默认是10 返回添加结果
  def add(self, proxy, score=INITIAL_SCORE):
    """
      添加代理,设置分数为最高
      :param proxy: 代理
      :param score: 分数
      :return: 添加结果
    """
    if not re.match('\d+\.\d+\.\d+\.\d+\:\d+', proxy):
      print('代理不符合规范', proxy, '丢弃')
      return
    if not self.db.zscore(REDIS_KEY, proxy):
      return self.db.zadd(REDIS_KEY, score, proxy)
  # 随机获取代理 首先获取100分代理 然后随机返回一个 如果不存在 就按照排名获取前100名 然后随机返回 否则抛出异常
  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
  #代理检测无效是设置分数减一 如果分数是最低值 那么代理就删除
  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
    #将代理的分数设置为100 代理有效时设置
  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)
  #返回所有列表
  def batch(self, start, stop):
    """
      批量获取
      :param start: 开始索引
      :param stop: 结束索引
      :return: 代理列表
    """
    return self.db.zrevrange(REDIS_KEY, start, stop - 1)

定义好了这些方法 可以在后续模块调用此类 来连接和操作数据库 获取随机可用代理只需要调用 random()方法


获取模块

获取模块逻辑相对简单 定义一个Crawler 来从各大网站抓取代理

import json
from .utils import get_page
from pyquery import PyQuery as pq

#定义元类 遍历attrs参数 判断是否是crawl开头 是就加入到CrawlFunc属性中
#如果要做拓展 添加一个crawl开头的方法 仿照其他方法定义生成器 就可以抓取网站的代理
class ProxyMetaclass(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)
        count += 1
    attrs['__CrawlFuncCount__'] = count
    return type.__new__(cls, name, bases, attrs)


class Crawler(object, metaclass=ProxyMetaclass):
  #将所有crawl 开头的方法调用一遍 获取每个方法返回的代理 组合成列表返回
  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_ip3366(self):
  for page in range(1, 4):
    start_url = 'http://www.ip3366.net/free/?stype=1&page={}'.format(page)
    html = get_page(start_url)
    ip_address = re.compile('<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>')
    # \s * 匹配空格,起到换行作用
    re_ip_address = ip_address.findall(html)
    for address, port in re_ip_address:
      result = address+':'+ port
      yield result.replace(' ', '')
  
def crawl_kuaidaili(self):
  for i in range(1, 4):
    start_url = 'http://www.kuaidaili.com/free/inha/{}/'.format(i)
    html = get_page(start_url)
    if html:
      ip_address = re.compile('<td data-title="IP">(.*?)</td>') 
      re_ip_address = ip_address.findall(html)
      port = re.compile('<td data-title="PORT">(.*?)</td>')
      re_port = port.findall(html)
      for address,port in zip(re_ip_address, re_port):
        address_port = address+':'+port
        yield address_port.replace(' ','')

def crawl_xicidaili(self):
  for i in range(1, 3):
    start_url = 'http://www.xicidaili.com/nn/{}'.format(i)
    headers = {
      'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
      'Cookie':'_free_proxy_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJWRjYzc5MmM1MTBiMDMzYTUzNTZjNzA4NjBhNWRjZjliBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMUp6S2tXT3g5a0FCT01ndzlmWWZqRVJNek1WanRuUDBCbTJUN21GMTBKd3M9BjsARg%3D%3D--2a69429cb2115c6a0cc9a86e0ebe2800c0d471b3',
      'Host':'www.xicidaili.com',
      'Referer':'http://www.xicidaili.com/nn/3',
      'Upgrade-Insecure-Requests':'1',
    }
    html = get_page(start_url, options=headers)
    if html:
      find_trs = re.compile('<tr class.*?>(.*?)</tr>', re.S)
      trs = find_trs.findall(html)
      for tr in trs:
        find_ip = re.compile('<td>(\d+\.\d+\.\d+\.\d+)</td>') 
        re_ip_address = find_ip.findall(tr)
        find_port = re.compile('<td>(\d+)</td>')
        re_port = find_port.findall(tr)
        for address,port in zip(re_ip_address, re_port):
          address_port = address+':'+port
          yield address_port.replace(' ','')

def crawl_ip3366(self):
  for i in range(1, 4):
    start_url = 'http://www.ip3366.net/?stype=1&page={}'.format(i)
    html = get_page(start_url)
    if html:
      find_tr = re.compile('<tr>(.*?)</tr>', re.S)
      trs = find_tr.findall(html)
      for s in range(1, len(trs)):
        find_ip = re.compile('<td>(\d+\.\d+\.\d+\.\d+)</td>')
        re_ip_address = find_ip.findall(trs[s])
        find_port = re.compile('<td>(\d+)</td>')
        re_port = find_port.findall(trs[s])
        for address,port in zip(re_ip_address, re_port):
          address_port = address+':'+port
          yield address_port.replace(' ','')

def crawl_iphai(self):
  start_url = 'http://www.iphai.com/'
  html = get_page(start_url)
  if html:
    find_tr = re.compile('<tr>(.*?)</tr>', re.S)
    trs = find_tr.findall(html)
    for s in range(1, len(trs)):
      find_ip = re.compile('<td>\s+(\d+\.\d+\.\d+\.\d+)\s+</td>', re.S)
      re_ip_address = find_ip.findall(trs[s])
      find_port = re.compile('<td>\s+(\d+)\s+</td>', re.S)
      re_port = find_port.findall(trs[s])
      for address,port in zip(re_ip_address, re_port):
        address_port = address+':'+port
        yield address_port.replace(' ','')

def crawl_data5u(self):
  start_url = 'http://www.data5u.com/free/gngn/index.shtml'
  headers = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
    'Cache-Control': 'max-age=0',
    'Connection': 'keep-alive',
    'Cookie': 'JSESSIONID=47AA0C887112A2D83EE040405F837A86',
    'Host': 'www.data5u.com',
    'Referer': 'http://www.data5u.com/free/index.shtml',
    'Upgrade-Insecure-Requests': '1',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36',
  }
  html = get_page(start_url, options=headers)
  if html:
    ip_address = re.compile('<span><li>(\d+\.\d+\.\d+\.\d+)</li>.*?<li class=\"port.*?>(\d+)</li>', re.S)
    re_ip_address = ip_address.findall(html)
    for address, port in re_ip_address:
      result = address + ':' + port
      yield result.replace(' ', '')

在这里实现了几个示例 抓取无忧代理 云代理 西刺代理 快代理 66免费代理等网站 这些方法都定义生成器 通过yield返回代理
先获取网页 然后通过pyquery 解析 IP 加 端口的形式返回

再定义一个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):
    """
      判断是否达到了代理池限制
    """
    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.__CrawlFuncCount__):
        callback = self.crawler.__CrawlFunc__[callback_label]
        # 获取代理
        proxies = self.crawler.get_proxies(callback)
        sys.stdout.flush()
        for proxy in proxies:
          #添加数据库
          self.redis.add(proxy)

检测模块

使用异步请求库aiohttp 对于响应速度比较快的网站来说requests同步请求和aiohttp异步请求差距没那么大
但是检测代理一般需要十多秒甚至几十秒时间 这个时候使用异步请求库 优势就体现出来了 效率可能会提高几十倍

VALID_STATUS_CODES = [200]#状态码 如果目标网站不同 可以更改
TEST_URL = 'http://www.baidu.com' #测试网站 建议设置成抓取需求网站 (有可能代理是可用的 但是被目标网站封了)
BATCH_TEST_SIZE = 100#批量测试最大值

class Tester(object):
  def __init__(self):
    self.redis = RedisClient()
    #检测单个代理的可用情况 async代表异步
    async def test_single_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')
        real_proxy = 'http://' + proxy
        print('正在测试', proxy)
        async with session.get(TEST_URL, proxy=real_proxy, timeout=15, allow_redirects=False) as response:
          if response.status in VALID_STATUS_CODES:
            self.redis.max(proxy)
            print('代理可用', proxy)
          else:
            self.redis.decrease(proxy)
            print('请求响应码不合法 ', response.status, 'IP', proxy)
    except (ClientError, aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, AttributeError):
      self.redis.decrease(proxy)
      print('代理请求失败', proxy)

  def run(self):
    """
      测试主函数
      :return:
    """
    print('测试器开始运行')
    try:
      count = self.redis.count()
      print('当前剩余', count, '个代理')
      for i in range(0, count, BATCH_TEST_SIZE):
        start = i
        stop = min(i + BATCH_TEST_SIZE, count)
        print('正在测试第', start + 1, '-', stop, '个代理')
        test_proxies = self.redis.batch(start, stop)
        loop = asyncio.get_event_loop()
        tasks = [self.test_single_proxy(proxy) for proxy in test_proxies]
        loop.run_until_complete(asyncio.wait(tasks))
        sys.stdout.flush()
        time.sleep(5)
    except Exception as e:
      print('测试器发生错误', e.args)

aiohttp 官网: http://aiohttp.readthedocs.io/


接口模块

如果要获取可用代理 可以使用redisclient类 连接redis 调用random方法 但是会有弊端

1.如果其他人使用代理池 就需要知道redis用户名和密码
2. 如果代理池需要部署在远程服务器端 redis只允许本地连接 就不能远程直连获取代理
3.如果爬虫主机没有redis模块 或者爬虫不是python语言编写的 就无法获取代理
4.如果redisclient类或者数据库结构有更新 那么爬虫端就要同步更新

为了使代理池作为一个独立服务运行 增加了接口模块 上面的缺点也可以避免

使用轻量级Flask库 实现接口模块

定义Flask对象 和三个接口 首页 随机代理页 获取数量页
运行后Flask会启动Web服务 只需要访问对应接口就可以获取可用代理

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 '<h2>Welcome to Proxy Pool System</h2>'


@app.route('/random')
def get_proxy():
  """
    Get a proxy
    :return: 随机代理
  """
  conn = get_conn()
  return conn.random()


@app.route('/count')
def get_counts():
  """
    Get the count of proxies
    :return: 代理池总量
  """
  conn = get_conn()
  return str(conn.count())


if __name__ == '__main__':
  app.run()

调度模块

调用以上三个模块 将三个模块通过多进程形式运行

from multiprocessing import Process
from proxypool.api import app
from proxypool.getter import Getter
from proxypool.tester import Tester

class Scheduler():
  def schedule_tester(self, cycle=TESTER_CYCLE):#测试模块
    """
      定时测试代理
    """
    tester = Tester()
    while True:
      print('测试器开始运行')
      tester.run()
      time.sleep(cycle)

  def schedule_getter(self, cycle=GETTER_CYCLE):#获取模块
    """
      定时获取代理
    """
    getter = Getter()
    while True:
      print('开始抓取代理')
      getter.run()
      time.sleep(cycle)

  def schedule_api(self):
    """
      开启API
    """
    app.run(API_HOST, API_PORT)#接口模块
    #启动入口 判断三个模块开关 如果开启就新建一个Process进程 调用start方法运行 三个进程可以并行执行 互不干扰
  def run(self):
    print('代理池开始运行')

    if TESTER_ENABLED:
      tester_process = Process(target=self.schedule_tester)
      tester_process.start()

    if GETTER_ENABLED:
      getter_process = Process(target=self.schedule_getter)
      getter_process.start()

    f API_ENABLED:
      api_process = Process(target=self.schedule_api)
      api_process.start()

以上内容便是整个代理池的架构和实现逻辑

整理代码 将代理运行

付费代理 例如讯代理(按天或者按量收费) 阿布云代理 搭建代理隧道 设置固定域名

二、模拟登录

很多情况下 页面的某些信息需要登录才可以查看 对于爬虫来说 需要爬取信息就需要模拟登录

前面我们说过 会话和Cookies的用法 简单说就是打开网页然后模拟登录 客户端生成cookies 里面保存了sessionID信息
登陆后发送给服务器 服务器进行判断 如果会话有效 服务器就判断用户登录了


模拟登录github 抓取个人信息

环境准备 requests 和 lxml库

分析登录过程

先退出Github 同时清除cookie 然后登录github 打开开发者工具 查看第一个session请求

观察Form Data 和 Headers


代码实现

import requests
from lxml import etree

定义Login类 设置参数变量
class Login(object):
  def __init__(self):
    self.headers = {
      'Referer' : 'https://github.com/',
      'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
      'Host':'github.com'
    }
    self.login_url = 'https://github.com/login'
    self.post_url = 'https://github.com/session'
    self.logined_url = 'https://github.com/settings/profile'
    self.session = requests.Session()

  #提取token
  def token(self):
    response = self.session.get(self.login_url,headers = self.headers)
    selector = etree.HTML(response.text)
    token = selector.xpath('//*[@id="login"]/form/input[2]/@value')[0]
    return token
  #登录
  def login(self,email,password):
    post_data = {
      'commit' : 'Sign in',
      'utf8' : '',
      'authenticity_token' : self.token(),
      'login': email,
      'password' : password,
    }
    # 请求首页
    response = self.session.post(self.post_url,data=post_data,headers=self.headers)
    if response.status_code == 200:
      response = self.session.get(self.logined_url,headers=self.headers)
      # 请求个人信息页
      if response.status_code == 200:
        self.profile(response.text)
  #抓取头像 和 邮箱
  def profile(self,html):
    selector = etree.HTML(html)
    img = selector.xpath('//*[@id="profile_26692417"]/dl/dd/img/@src')
    email = selector.xpath('//select[@id="user_profile_email"]/option[@value!=""]/text()')
    print(img,email)

if __name__ == "__main__":
  login = Login()
  login.login(email='[email protected]',password= 'liuxiaosong.163.com')

猜你喜欢

转载自www.cnblogs.com/liuxiaosong/p/10368385.html