免费代理池的搭建

在做爬虫的时候,由于高频访问,经常会出现IP被封禁的情况,因为服务器检测到某个IP在单位时间内访问次数超过某个阈值时,会认为是爬虫程序在访问,便直接拒绝服务。因此,一般的处理手段是我们可以使用代理,来伪装IP,让服务器无法识别由我们本机发起的请求。
网络上有大量免费且公开的代理可以供我们使用,但这些单利并不能保证都可以使用,因为同样的代理可能被其他人拿来爬虫使用而遭到封禁,因此,在真正使用之前,我们需要对这些免费代理进行筛选,剔除那些不能使用的。保留下可以用的,来构建一个代理池,供我们爬虫使用。

准备工作

代理池将被存储在Redis数据库中,因此使用前需要先安装好Redis,另外需安装aiohttp、requests、redis-py、pyquery、Flask库。

本文目标

我们要实现以下四个模块,来构建高效的代理池:
存储模块:负责存储抓取下来的代理。保证代理可用且不重复,使用Redis来村塾
获取模块:使用简单的爬虫程序到各大免费代理网站爬取代理。代理形式都是IP+端口
检测模块:获取到的代理不一定都能使用,因此需要对抓到的每个代理,针对未来将要爬取的网站进行检测,新获取的代理分数设置为10。测试过程中,如果可用,则分值设为100,不可用,分值减1。循环不断的测试,减到一定阈值后,从代理库移除,不再使用。
接口模块:需要用API来提供对外服务的借口。为了便于后续使用,简单的做法是用一个轻量级的Flask来实现一个webAPI借口。

获取模块

请求request的执行函数

import requests
from requests.exceptions import ConnectionError

base_headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36',
    'Accept-Encoding': 'gzip, deflate, sdch',
    'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7'
}
def get_page(url, options={}):
    """
    抓取代理
    :param url:
    :param options:
    :return:
    """
    headers = dict(base_headers, **options)
    print('正在抓取', url)
    try:
        response = requests.get(url, headers=headers)
        print('抓取成功', url, response.status_code)
        if response.status_code == 200:
            return response.text
    except ConnectionError:
        print('抓取失败', url)
        return None

定义一个类,从各大免费代理网站获取代理

import json
import re
from pyquery import PyQuery as pq


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)
    #该类定义为Crawler的元类。attrs属性包含了Crawler类中定义的所有变量及方法。
class Crawler(object, metaclass = ProxyMetaclass):
    def get_proxies(self, callback):
        proxies = []
        for proxy in eval('self.{}()'.format(callback)):
            """
            eval将字符串str当成有效的表达式来求值并返回计算结果
            """
            print('成功获取代理', proxy)
            proxies.append(str(proxy))
        return proxies
    
    def crawl_daili666(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()
                    """:nth-child(n) 选择器匹配属于其父元素的第 N 个子元素,不论元素的类型。 css选择器知识"""
                    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)
            print('crawling', start_url)
            html = get_page(start_url)
            if html:
                doc = pq(html)
                trs  = doc('.table tbody tr').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_kuaidaili(self):
        for page in range(1,4):
            start_url = 'http://www.kuaidaili.com/free/inha/{}/'.format(page)
            print('crawling', start_url)
            html = get_page(start_url)
            if html:
                doc = pq(html)
                trs  = doc('.table tbody tr').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_xicidaili(self):
        for page in range(1,3):
            start_url = 'http://www.xicidaili.com/nn/{}'.format(page)    
            print('crawling', start_url)
            html = get_page(start_url)           
            if html:
                doc = pq(html)
                trs  = doc('.odd').items()
                for tr in trs:
                    ip = tr.find('td:nth-child(2)').text() #此处涉及jquery语句。
                    port = tr.find('td:nth-child(3)').text()
                    yield ':'.join([ip, port])
    
    def crawl_data5u(self):
        for page in range(1,3):
            start_url = 'http://www.data5u.com/free/gngn/index.shtml'     
            print('crawling', start_url)
            html = get_page(start_url)           
            if html:
                doc = pq(html)
                trs  = doc('.wlist ul li ul:gt(0)').items()
                for tr in trs:
                    ip = tr.find('span:nth-child(1)').text()
                    port = tr.find('span:nth-child(2)').text()
                    yield ':'.join([ip, port])

这里借助了元类,来实现这样的逻辑,可以用get_proxy方法调用Crawler类中,所有以crawl为开头的方法。后续如果要扩展,只需添加一个crawl开头的方法即可,不用考虑其他部分。
定义一个Getter类,来动态获取所有以crawl开头的方法,并将所有获取到的代理加入代理池。

from setting import *
import sys

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)

存储模块

使用redis数据库存储。使用redis的有序集合,集合的每一个元素都是不重复的,这里代理便是集合的每一个元素,每个代理相应有一个分数值。
基本逻辑是,新获取到的代理设置为10.在后续检测时,如果代理不可用,值减1分,分数减至0时,剔除;如果可用,则立即设置为100分。
现在定义一个类来操作数据库的有序集合,定义相关的方法来实现分数的设置,代理的获取等。

import redis
from setting import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_KEY
from setting import MAX_SCORE, MIN_SCORE, INITIAL_SCORE
from random import choice
import re

class PoolEmptyError(Exception):

    def __init__(self):
        Exception.__init__(self)

    def __str__(self):
        return repr('代理池已经枯竭')

class RedisClient(object):
    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)
    
    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):
            print('代码不存在,可加入')
            return self.db.zadd(REDIS_KEY,{proxy:score})
        """注意,此处将简单的参数传入改为字典形式"""
    
    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
    
    def max(self, proxy):
        """
        将代理设置为MAX_SCORE
        :param proxy: 代理
        :return: 设置结果
        """
        print('代理', proxy, '可用,设置为', MAX_SCORE)
        return self.db.zadd(REDIS_KEY,{proxy:MAX_SCORE})
    
    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)


if __name__ == '__main__':
    conn = RedisClient()
    
    result = conn.add('127.0.0.1:28', 10)
    print(result)

检测模块

根据具体的爬虫网站,对代理进行检测。
由于代理数量很多,为了提高检测效率,使用异步请求库aiohttp来进行检测。
requests作为一个同步请求库,发出一个请求,程序需要等待网页加载完毕后才能继续执行,这个过程会阻塞等待相应,有一个等待时间。而异步请求库就可以在这个等待时间里做其他事情,比如调度其他请求或解析网页等。

import asyncio
import aiohttp
import time
import sys
try:
    from aiohttp import ClientError
except:
    from aiohttp import ClientProxyConnectionError as ProxyConnectionError

from setting import *


class Tester(object):
    def __init__(self):
        self.redis = RedisClient()
    
    async def test_single_proxy(self, proxy): #方法前加async代表这个方法是异步的。测试单个代理的可用情况
        """
        测试单个代理
        :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)

接口模块

为了使代理池作为一个独立的服务运行,增加一个接口模块,并以webAPI的形式暴露可用代理

from flask import Flask, g

__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()

这里声明了一个Flask对象,定义了三个接口,分别是首页、随机代理页和获取代理总数量页

调度模块

将以上所定义的几个模块通过多线程的方式运行起来。注意需要在命令行下运行多线程

import time
from multiprocessing import Process
from setting import *


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)
    
    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()
        
        if API_ENABLED:
            api_process = Process(target=self.schedule_api)
            api_process.start()

程序启动入口是run()。3个调度方法结构很清晰,比如schedule_tester()方法,首先声明一个Tester对象,然后进入死循环不断测试,执行完一轮就休眠一段时间,后重新测试。由于使用多线程,因此三个程序可以并行执行,互不干扰。

配置文件

最后是相关的配置文件,保存为setting放入和上述代码相同的文件夹下,当然也可以放入程序中。但为了良好的编码规范,还是单独放置。

# Redis数据库地址
REDIS_HOST = '127.0.0.1'

# Redis端口
REDIS_PORT = 6379

# Redis密码,如无填None
REDIS_PASSWORD = None

REDIS_KEY = 'weixin'

# 代理分数
MAX_SCORE = 100
MIN_SCORE = 0
INITIAL_SCORE = 10

VALID_STATUS_CODES = [200, 302]

# 代理池数量界限
POOL_UPPER_THRESHOLD = 50000

# 检查周期
TESTER_CYCLE = 20
# 获取周期
GETTER_CYCLE = 300

# 测试API,建议抓哪个网站测哪个
TEST_URL = 'https://weixin.sogou.com/weixin?type=2&query=nba'

# API配置
API_HOST = '0.0.0.0'
API_PORT = 5555

# 开关
TESTER_ENABLED = True
GETTER_ENABLED = True
API_ENABLED = True

# 最大批测试量
BATCH_TEST_SIZE = 10

运行程序

将代码整合到一期,整个程序保存为run.py,在命令行中运行:python ****\run.py python后为文件路径

import sys
import io
def main():
    try:
        s = Scheduler()
        s.run()
    except:
        main()
if __name__ == '__main__':
    main()

最后Redis中存储的代理:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值