Python爬虫实战 | (18) 搭建IP代理池

在本篇博客中我们将完成IP代理池的搭建。IP代理池是通用的,创建好后,可供多个爬虫任务共同使用。

搭建IP代理池主要分为4个基本模块:存储模块、获取模块、检测模块、接口模块。

1)存储模块: 负责存储代理IP。

2)获取模块: 定时在各大代理网站抓取代理IP,并保存到数据库(redis)中

3)检测模块: 定时检测数据库中的代理IP,判断能否正常访问网页。

4)接口模块: 提供代理IP的接口。

还有配置文件:setting.py 包含一些变量/参数,供各个模块使用;调度模块:scheduler.py 用于调度运行四个基本模块。

setting.py:

#配置文件
# Redis数据库地址
REDIS_HOST = '127.0.0.1'  #localhost

# Redis端口
REDIS_PORT = 6379

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

REDIS_KEY = 'proxies' #有序集合

# 代理分数
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 = 'http://www.baidu.com' #通用测试网站

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

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

# 最大批测试量
BATCH_TEST_SIZE = 10
  • 获取模块

定时在各大代理网站抓取代理IP,并保存到数据库中。

代理可以是免费公开代理也可以是付费代理,付费代理会更加稳定。代理的形式都是IP 加端口,此模块尽量从不同来源获取,尽量抓取高匿代理。

getter.py

#获取模块

import re
import requests
from requests.exceptions import ConnectionError
from db import RedisClient
from setting import *
import sys


class Getter():
    def __init__(self):
        self.redis = RedisClient()

    def is_over_threshold(self):
        """
        判断是否达到了代理池最大限制
        """
        if self.redis.count() >= POOL_UPPER_THRESHOLD:
            return True
        else:
            return False

    def get_page(self, url, options={}):
        """
        抓取代理
        :param url: 代理网址
        :param options:
        :return:
        """
        base_headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 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'
        }
        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

    def crawl_ip3366(self): #爬取ip3366网站的代理IP
        for page in range(1, 4):
            start_url = 'http://www.ip3366.net/free/?stype=1&page={}'.format(page)
            html = self.get_page(start_url)
            ip_address = re.compile('<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>') #提取Ip和端口
            # \s * 匹配空格,起到换行作用
            re_ip_address = ip_address.findall(html)
            for address, port in re_ip_address:
                result = address + ':' + port
                print('成功获取到代理', result)
                self.redis.add(result) #加入数据库

    def crawl_kuaidaili(self):#爬取快代理网站的代理IP
        for i in range(1, 4):
            start_url = 'http://www.kuaidaili.com/free/inha/{}/'.format(i)
            html = self.get_page(start_url)
            if html:
                ip_address = re.compile('<td data-title="IP">(.*?)</td>') #提取ip
                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
                    print('成功获取到代理', address_port)
                    self.redis.add(address_port)#加入数据库

    def crawl_xicidaili(self):#爬取xici代理网站的代理IP
        for i in range(1, 4):
            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 = self.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>')#提取ip
                    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
                        print('成功获取到代理', address_port)
                        self.redis.add(address_port)#加入数据库

    def run(self):
        print('获取器开始执行')
        if not self.is_over_threshold():
            self.crawl_ip3366()
            self.crawl_kuaidaili()
            self.crawl_xicidaili()


if __name__ == '__main__':
    get = Getter()
    get.run()
  • 存储模块

负责存储抓取下来的代理。

首先要保证代理不重复, 要标识代理的可用情况(给代理IP一个分值),还要动态实时处理每个代理,所以一种比较高效和方便的存储方式就是使用Redis 的有序集合。

Redis:REmoteDIctionaryServer是一个基于内存的高效的key-value形式的非关系型数据库,支持存储多种数据结构:字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

db.py

#存储模块

import redis
from error import PoolEmptyError
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 RedisClient(object):
    def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
        """
        初始化
        :param host: Redis 地址
        :param port: Redis 端口
        :param password: Redis密码
        """
        #连接redis数据库
        self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)

    def add(self, proxy, score=INITIAL_SCORE):
        """
        添加代理,设置分数为初始分值 10
        :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, {proxy: score})

    def exists(self, proxy):
        """
        判断是否存在
        :param proxy: 代理
        :return: 是否存在
        """
        return not self.db.zscore(REDIS_KEY, proxy) == None

    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)

    def max(self, proxy):
        """
        将代理设置为MAX_SCORE
        :param proxy: 代理
        :return: 设置结果
        """
        print('代理', proxy, '可用,设置为', MAX_SCORE)
        return self.db.zadd(REDIS_KEY, {proxy: MAX_SCORE})

    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, -1, proxy)
        else:
            print('代理', proxy, '当前分数', score, '移除')
            return self.db.zrem(REDIS_KEY, proxy)

    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


if __name__ == '__main__':
    #测试存储模块
    conn = RedisClient()
    conn.add('203.150.123.247:8080')
  • 检测模块

定时检测数据库中的代理,判断能否正常访问网页。

设置一个检测链接,通常爬取哪个网站就检测哪个网站,这样更有针对性,如果要做一个通用型的代理,那可以设置百度等链接用于检测。

需标识每一个代理的状态,为其设置分数标识,初始值为10。以100 分为最高值,代表可用,分数越少代表越不可用。检测一次,如果代理可用,可以将分数标识设置为100 分;如果代理不可用,将分数标识减1分,当分数减到一定阈值(如0)后删除该代理。

tester.py

#检测模块
import asyncio
import aiohttp
import time
import sys

try:
    from aiohttp import ClientError
except:
    from aiohttp import ClientProxyConnectionError as ProxyConnectionError
from db import RedisClient
from setting import *


class Tester(object):
    def __init__(self):
        self.redis = RedisClient()

    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) #设为100分
                        print('代理可用', proxy)
                    else:
                        self.redis.decrease(proxy) #不可用 -1分
                        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)


if __name__ == '__main__':
    test = Tester()
    test.run()
  • 接口模块

提供代理IP的接口。

如果直接连接数据库获取对应的数据,就需要知道数据库的连接信息,并且要配置连接,比较安全和方便的方式是提供一个Web API 接口,通过访问接口拿到可用代理。

另外,需要随机返回某个代理,获取排名中最高分数(100分)的代理,若存在多个,从中随机选取一个;否则随机返回一个代理。

api.py

#接口模块
from flask import Flask, g
from db import RedisClient
from setting import *

__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(host=API_HOST,port=API_PORT)
  • 调度模块

调度获取、存储、检测和接口四个基本模块,定时获取IP并存储,定时进行检测,然后开启接口提供IP代理。scheduler.py:

#调度模块

import time
from multiprocessing import Process
from api import app
from getter import Getter
from tester import Tester
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()


if __name__ == '__main__':
    scheduler = Scheduler()
    scheduler.run()

web api:访问地址,即可获取IP代理:

查看redis数据库:

在简单爬虫程序中(requests)使用搭建的IP代理池:

#在爬虫程序中使用构建的IP代理池

import requests

proxy = requests.get('http://127.0.0.1:5555/random').text #从IP代理池接口中获取 IP代理
print(proxy)
proxies = {
    'http': 'http://' + proxy,
    'https': 'https://' + proxy
}
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)

完整项目

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值