前言
去年实习的时候做过爬虫,但是理解的很浅,所以今年趁着在家的时候,又打算认真地学习一下。刚好看到崔庆才老师拉勾网的教程 52讲轻松搞定网络爬虫,于是就跟着学了一下。到代理池这时,由于一些基础东西没整明白,耽误了好一阵,好在整个项目代码量不大。断断续续的,终于在今天对着 github 敲完了一遍,有的地方做了一下小改动。在这里记录一下搭建过程和最后结果。
具体参考崔老师的博客:[Python3网络爬虫开发实战] 9.2-代理池的维护 和项目地址:ProxyPool
代理池介绍
爬虫的反爬方法有很多,最简单的就是更改 headers。不过有当用同一 IP 爬取一个网站过于频繁的时候,这个 IP 很容易会被网站封掉。所以通常,为了爬取网站时自己的 IP 被封,都会使用代理。而且为了爬虫运行正常,一般会经常更换代理。使用代理的优势不止防止被封这一点,有时候也可以达到突破访问限制,访问一些正常访问不了的网站。
那么如何获取 IP 呢?其实网上有很多免费的代理网站,不过里面的 IP 地址大多是不好使的,不过付费的很多时候又没有必要。这时候就需要代理池了。代理池的作用是爬取很多 IP 代理,保存起来,然后验证代理是否可用,当需要使用的时候,就可以从其中取出可用的代理。是不是感觉很酷?
从功能上看,代理池的基本架构就分为四部分:
-
获取模块(Getter):从各个免费的代理网站上爬取 IP 代理。
-
存储模块(Store): 将爬取的 IP 代理存起来,当需要使用时,从中取出有效的代理。这里如何存取更新是关键,使用 redis 中的有序集合(Sorted Set)进行存储。给每个代理设置一个分数,当刚存入数据库时,初始分数设为 10。测试更新时,如果检测到代理可用,则设成最大值 100;否则,如果检测到不可用,则每次减一,直到减到最小,删除该代理。
-
测试模块(Tester): 测试是通过使用代理访问一个特定网站,看访问是否成功。每次测试,都会更新一下代理的分数。
-
接口模块(Server): 由于是存放的数据库中的,如果每次取用都要暴露数据库信息,不太方便,也不安全。因此,这里使用 flask 做了一个简单的后台服务端。
下面是代理池的框架流程(盗图):

这就是代理池的基本思路,代码也不多,项目的可扩展性比较好,下面具体介绍一下每个模块。
代理池代码解释
获取模块
获取模块是爬取各大免费 IP 代理网站,获取到 IP。免费网站主要有: 西刺免费代理、 快代理、66免费代理等等。有一点比较好的是,这些网站结构比较简单,代理比较好爬,如果不考虑反爬措施的话,还是很亲民的。
代理池中,所有的获取代理代码都继承自 BaseCrawler:
# -*- coding:utf-8 -*-
from retrying import retry
import requests
from loguru import logger
import random
class BaseCrawler(object):
USER_AGENT_LIST = [
'MSIE (MSIE 6.0; X11; Linux; i686) Opera 7.23',
'Opera/9.20 (Macintosh; Intel Mac OS X; U; en)',
'Opera/9.0 (Macintosh; PPC Mac OS X; U; en)',
'iTunes/9.0.3 (Macintosh; U; Intel Mac OS X 10_6_2; en-ca)',
'Mozilla/4.76 [en_jp] (X11; U; SunOS 5.8 sun4u)',
'iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:5.0) Gecko/20100101 Firefox/5.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20120813 Firefox/16.0',
'Mozilla/4.77 [en] (X11; I; IRIX;64 6.5 IP30)',
'Mozilla/4.8 [en] (X11; U; SunOS; 5.7 sun4u)',
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/67.0.3396.62 Safari/537.36'
]
urls = [] # 网站的列表页(即包含 IP 的网页)
# 定义重试次数
@retry(stop_max_attempt_number=3, retry_on_result=lambda x: x is None)
def fetch(self, url, **kwargs):
''' 爬取 url '''
headers = {
"Host": "www.xicidaili.com",
"User-Agent": random.choice(self.USER_AGENT_LIST),
}
try:
response = requests.get(url, headers=headers, **kwargs)
if response.status_code == requests.codes.ok:
return response.text
except requests.ConnectionError:
return
@logger.catch
def crawl(self):
''' 爬取网站的主函数 '''
for url in self.urls:
logger.info(f'fetching {url}')
html = self.fetch(url)
for proxy in self.parse(html):
logger.info(f'fetched proxy {proxy.string()} from {url}')
yield proxy
这里增加了一些爬取时的头部信息。
所有爬取网站的代码,都可以通过继承它来放到项目中,扩展性很好,我尝试写了个西刺代理网站的代码:
# -*- coding:utf-8 -*-
from proxypool.crawlers.base import BaseCrawler
from proxypool.schemas.proxy import Proxy
from pyquery import PyQuery as pq
BASE_URL = 'https://www.xicidaili.com/nn/{page}'
MAX_PAGE = 200
class XicidailiCrawler(BaseCrawler):
'''
xicidaili crawler, https://www.xicidaili.com/nn/
'''
urls = [BASE_URL.format(page=page) for page in range(1, MAX_PAGE + 1)]
def parse(self, html):
'''parse html file to get proxies
:param html: the text of website
:type html: str
:return: the generator of proxies
:rtype: Proxy
'''
doc = pq(html)
for tr in doc('#ip_list tr:gt(0)').items():
host = tr.find('td:nth-child(2)').text()
port = tr.find('td:nth-child(3)').text()
if host and port:
proxy = Proxy(host=host, port=port)
yield proxy
if __name__ == '__main__':
crawler = XicidailiCrawler()
for proxy in crawler.crawl():
print(proxy)
代码可以运行,不过西刺代理有一些反爬机制,不能太过频繁(我就是太快了,导致它把我的 IP 给封掉了),所以并没有放到最后的项目中。
存储模块
存储时,使用的是 Redis 数据库。由于之前没使用过,现学了一下,感觉这个库有点酷啊。跟 MongoDB 一样,是 key-value 类型的数据库,不过 Redis 支持五种数据类型,这里用的是 Sorted Set 有序集合类型。
需要注意的是,使用前要安装好 Redis。存储的代码如下:
# -*- coding:utf-8 -*-
import redis
from proxypool.exceptions.empty import PoolEmptyException
from proxypool.schemas.proxy import Proxy
from proxypool.setting import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD,\
REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MIN, PROXY_SCORE_INIT
from random import choice
from typing import List
from loguru import logger
from proxypool.utils.proxy import is_valid_proxy, convert_proxy_or_proxies
REDIS_CLIENT_VERSION = redis.__version__
IS_REDIS_VERSION_2 = REDIS_CLIENT_VERSION.startswith('2.')
class RedisClient(object):
'''
代理池的 redis 连接
'''
def __init__(self,
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
**kwargs):
'''
初始化 redis 客户端
:param host: redis host
:param port: redis port
:param password: redis password
'''
self.db = redis.StrictRedis(host=host,
port=port,
password=password,
decode_responses=True,
**kwargs)
def add(self, proxy: Proxy, score=PROXY_SCORE_INIT) -> int:
'''
将代理加到 redis 中,并设置初始分数
:param proxy: 代理,格式 ip:port, 例如 8.8.8.8:888
:param score: 代理初始化的分数
:type score: int
:return: 成功添加的数量
'''
if not is_valid_proxy(f'{proxy.host}:{proxy.port}'):
logger.info(f'invalid proxy {proxy}, throw it')
return
# if not self.db.exists(proxy):
# 将代理添加到有序集合中
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, score, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): score})
def random(self) -> Proxy:
'''
获取随机代理
1. 最开始,尝试获取最大分数的代理
2. 如果不存在, 尝试通过排行榜获取
3. 如果还是不存在,报错
:return: proxy, like 8.8.8.8:888
'''
# 1. 尝试获取最大分数的代理
proxies = self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MAX,
PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# 没有的话,通过排名获取
proxies = self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MIN,
PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# 如果都没有, 报错
raise PoolEmptyException
def decrease(self, proxy: Proxy) -> int:
'''
降低代理的分数,如果比最小值还低,则删除
:param proxy: proxy
:return: new score
'''
score = self.db.zscore(REDIS_KEY, proxy.string())
# 当前分数比最小值大
if score and score > PROXY_SCORE_MIN:
logger.info(f'{proxy.string()} current score {score}, decrease 1')
if IS_REDIS_VERSION_2:
return self.db.zincrby(REDIS_KEY, proxy.string(), -1)
return self.db.zincrby(REDIS_KEY, -1, proxy.string())
# 当前分数比最小值小
else:
logger.info(f'{proxy.string()} current score {score}, remove')
return self.db.zrem(REDIS_KEY, proxy.string())
def max(self, proxy: Proxy) -> int:
'''
将代理分数设成最大
:param proxy: 代理
:return: 新的分数
'''
logger.info(f'{proxy.string()} is valid, set to {PROXY_SCORE_MAX}')
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, PROXY_SCORE_MAX, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): PROXY_SCORE_MAX})
def count(self) -> int:
'''
获取代理的数量
:return: 数量
:rtype: int
'''
return self.db.zcard(REDIS_KEY)
def all(self) -> List[Proxy]:
'''
获取所有代理
:return: 代理列表
'''
return convert_proxy_or_proxies(
self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MIN, PROXY_SCORE_MAX))
def batch(self, start, end) -> List[Proxy]:
'''
获取一批特定区间中的代理
:param start: 代理的起始索引
:param end: 代理的结束索引
:return: 代理列表
'''
return convert_proxy_or_proxies(
self.db.zrevrange(REDIS_KEY, start, end - 1))
if __name__ == '__main__':
conn = RedisClient()
conn.add(Proxy(host='8.8.8.8', port='888'))
result = conn.random()
print(result)
这里主要是记录了一下数据库的增删改查。增加代理时设置初始分数;更改代理时,设置分数减一或是变为最大值;删除时
测试模块
测试时,需要使用一个测试网站,用代理去访问它。通常是要爬取哪个网站就用哪个网站进行测试,不过通用的话,使用的是百度,毕竟比较稳定。
测试时,根据代理是否可用,设置数据库中的代理分数。如果可用,直接设成最大值;不可用,分数减一。当分数减到比最小值还小时,删除该代理。
测试代码如下:
# -*- coding:utf-8 -*-
import asyncio
import aiohttp
from loguru import logger
from proxypool.schemas.proxy import Proxy
from proxypool.storages.store_by_redis import RedisClient
from proxypool.setting import TEST_TIMEOUT, TEST_BATCH, TEST_URL, TEST_VALID_STATUS
from aiohttp import ClientProxyConnectionError, ServerDisconnectedError, ClientOSError, ClientHttpProxyError
from asyncio import TimeoutError
EXCEPTIONS = (
ClientProxyConnectionError,
ConnectionRefusedError,
TimeoutError,
ServerDisconnectedError,
ClientOSError,
ClientHttpProxyError
)
class Tester(object):
''' 代理池的 tester, 用于测试在队列中的代理 '''
def __init__(self):
''' 初始化 redis '''
self.redis = RedisClient()
self.loop = asyncio.get_event_loop()
async def test(self, proxy: Proxy):
''' 测试一个代理
:param proxy: Proxy object
:return:
'''
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
try:
logger.debug(f'testing {proxy.string()}')
async with session.get(TEST_URL, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT,
allow_redirects=False) as response:
if response.status in TEST_VALID_STATUS:
self.redis.max(proxy)
logger.debug(f'proxy {proxy.string()} is valid, set max score')
else:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')
except EXCEPTIONS:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')
@logger.catch
def run(self):
''' 测试主函数 '''
logger.info('stating tester...')
count = self.redis.count()
logger.debug(f'{count} proxies to test')
for i in range(0, count, TEST_BATCH):
start, end = i, min(i + TEST_BATCH, count)
logger.debug(f'testing proxies from {start} to {end} indices')
proxies = self.redis.batch(start, end)
tasks = [self.test(proxy) for proxy in proxies]
self.loop.run_until_complete(asyncio.wait(tasks))
if __name__ == '__main__':
tester = Tester()
tester.run()
接口模块
接口模块不是必须的,其实前三个模块组合起来就已经实现了代理池的所有功能了。接口模块只是让项目更完善,将底层封装起来,项目更完整。
这里做接口使用的是 flask,我同样也没接触过,不过好在上手很容易,项目中也没用到多难的东西,很容易理解。直接上代码:
# -*- coding:utf-8 -*-
from flask import Flask, g
from proxypool.storages.store_by_redis import RedisClient
from proxypool.setting import API_HOST, API_PORT, API_THREADED
app = Flask(__name__)
def get_conn():
''' 获取 redis 客户端对象
:return: redis 客户端
'''
if not hasattr(g, 'redis'):
g.redis = RedisClient()
return g.redis
@app.route('/')
def index():
''' 获取主界面,可以自定义自己的界面(这里就使用教程的默认界面了)
:return:
'''
return '<h2>Welcome to Proxy Pool System —— by rocketeerli</h2>'
@app.route('/random')
def get_proxy():
''' 获取一个随机代理
:return: 一个随机代理
'''
conn = get_conn()
return conn.random().string()
@app.route('/count')
def get_count():
''' 获取代理的数目
:return: 代理数目
:rtype: int
'''
conn = get_conn()
return str(conn.count())
if __name__ == '__main__':
app.run(host=API_HOST, port=API_PORT, threaded=API_THREADED)
接口模块的测试结果如下:

配置文件
这里有许多的配置项,在 setting.py 文件中,截取一部分如下:
# 代理分数
PROXY_SCORE_MAX = 100
PROXY_SCORE_MIN = 0
PROXY_SCORE_INIT = 10
# 代理数目
PROXY_NUMBER_MAX = 50000
PROXY_NUMBER_MIN = 0
# 测试代理的循环时间
CYCLE_TESTER = env.int('CYCLE_TESTER', 20)
# 获取代理的循环时间
CYCLE_GETTER = env.int('CYCLE_GETTER', 100)
# 测试的信息
TEST_URL = env.str('TEST_URL', 'http://www.baidu.com')
TEST_TIMEOUT = env.int('TEST_TIMEOUT', 10) # 测试的超时时间,默认 10 秒
TEST_BATCH = env.int('TEST_BATCH', 20) # 异步测试时,每次的 task 数目
TEST_VALID_STATUS = env.list('TEST_VALID_STATUS', [200, 206, 302]) # 测试网站返回的正确状态码
可以自行根据自己的需求进行更改。
最后
最后,附上我的练习代码地址,在原代码基础上稍微做了点小更改。
虽然用的时间比较长,但不得不说,这个项目还是比较好的。代码量虽然不多,但确实让我学到了很多东西,对 python 语言也有了一个更深的认识。最重要的是,对新手很友好。不多bb,强烈推荐崔大的教程。
&spm=1001.2101.3001.5002&articleId=106020935&d=1&t=3&u=1468534aa67e47b5b846db5f455c0d93)
1159

被折叠的 条评论
为什么被折叠?



