在进行网页爬虫的项目时,常常会因为爬取的频率过高而触发 反爬虫机制 ,这时候,面临两个选择:

  1. 休息片刻。一般反爬虫机制不会进行永久的 IP 封禁,只是暂时限制访问而已,等待封禁时间结束再进行爬取即可。当然对于某些拥有黑名单机制的网站,如果封禁次数过多,封禁的时间也会随着这个次数而提高。
  2. 更换 IP。既然我的 IP 被封了,那么我换一个其他的 IP 不就行了。

显然,第二种方法更优于第一种,并且更加符合 geek 的风格。但是问题在于,从哪里寻找这样一个 IP 地址呢?

很多的教材都有免费代理网站这样一个概念,但是免费代理面临的一个问题就是,很多的人都在使用,所以往往刚解封就又被封禁了,有的甚至直接就挂在网站的黑名单里。所以,代理网站提供的 IP 虽然多,但是可用的却非常的少。

小数量的 IP 跟没有一样,那么如何白嫖大量的免费的有效的IP 代理呢?

代理网站有很多,假设每个网站能够提供两位数级别的可用 IP,那么统合起来就是一个十分可观的数字了。用来应付个人级的爬虫项目游刃有余。

但是,这又引出一个问题。每次进行爬虫项目都要先对上千上万的 IP 地址进行一次过滤? 这无疑是非常地影响效率的。

所以,基于以上的种种问题,引出一个解决方案,建立一个代理池(proxy pool)

这个想法在某些的教材上面出现过,并且也有类似的项目( 但是大多数都已经失效了 ),这里笔者决定从零开始,维护一个属于自己的代理池项目。

游戏开始

进行一个项目,首先要对项目进行一个大致的规划,这个规划没有必要太过于详细,因为作为一个经验不是很充足的菜鸟程序员,永远不知道开发过程中会遇到什么问题,又会迸发出什么样令人拍案的灵感(agile development)。

预计有以下几个模块:

  • getter:负责从各个免费代理网站上获取代理,并写入数据库
  • tester:负责对数据库中的代理进行测试,并进行评分
  • server:随机从数据库中返回代理
  • scheduler:调度器,调度 getter、tester、api 三者的运行
  • database:数据库接口

预计使用到一下的技术:

  • request、bs4 等:爬虫相关的技术,主要用于获取各个代理网站的代理
  • flask:制作主页面和 api
  • redis:代理数据库。由于我们是需要完成一个评分机制的代理池,所以可以使用 redis 的有序集合结构,功能简洁,实现便利。
  • aiohttp:通过协程提高测试效率
  • MySQL:协同日志数据库

以上的项目主体框架思路来自于 GitHub 的几个热门代理池项目

1. 选取免费代理网站

笔者尝试过很多的代理网站,诸如西刺、快代理、齐云等国内知名的网站,但是发现可用的 IP 比例实在是太少,后来想了想,大抵是国内的爬虫学习者和个人开发者多引用这些网站,所以导致网站的 IP 大量被封吧。

于是笔者去搜索了一些外部的网站,如free proxy list等,IP 的可用率确实提高了很多。

至于用不用国内的免费代理,就看个人的需求吧。

2. 完成 getter 模块

getter 模块负责获取免费代理,写入代理池.

这里以free_proxy为例,因为笔者发现这个网站的 IP 地址使用了Base64 加密,所以 IP 地址被爬虫新手”迫害”的程度较低:D。

import base64
res_ip = re.findall('Base64.decode\("(.*?)"\)',req.text)
res_port = re.findall('style=\'\'>(\d*)</span>',req.text)
proxy_list = []
for index in range(len(res_ip)):
  proxy = base64.b64decode(res_ip[index]).decode('utf-8')+':'+res_port[index]
  proxy_list.append({'http':proxy,'https':proxy})

得到输出结果如下,也就是我们所需要的代理,将其写入 redis 数据库,getter 的任务到此也就完成了。

['167.99.146.167:80',
 '138.197.5.192:8888',
 '157.245.123.27:8888',
 '103.11.65.160:9090',
 '104.248.1.178:8080',
 '198.23.239.245:80',
 '204.101.4.42:4145',
 '159.89.123.57:8080',
 '159.203.44.177:3128',
 '159.203.87.130:3128',
 '35.194.36.69:3128',
 '45.55.159.57:27720',
 '142.93.57.37:80',
 '64.251.21.59:80',
 '104.237.227.198:54321',
 '198.23.143.5:1080',
 '162.223.89.69:1080',
 '138.197.164.82:8080',
 '167.172.135.255:8080',
 '167.114.112.84:80',
 '68.183.128.131:9999',
 '64.235.204.107:8080',
 '38.142.63.146:31596',
 '168.169.146.12:8080',
 '198.50.177.44:44699',
 '104.218.60.89:4145',
 '165.234.102.177:8080',
 '178.128.176.96:80',
 '64.227.51.227:8118',
 '23.244.28.27:3128']

当然,仅仅是这一个网站是远远不够的,后面还可以自己选择一些网站并完成对应的 getter 子模块。

补充说明:

由于我们会通过每一个代理 IP 评分进行代理池的迭代更新,所以在写入数据库时需要一个初始评分。

3. 完成 tester 模块

从 redis 中按照评分的排序批量抽取制定数量的代理 IP,进行测试,并根据测试的结果对对应的评分进行修改。

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
        async with session.get(TEST_URL, headers=headers, proxy=real_proxy
                                # , timeout=15
                , allow_redirects=False
                                ) as response:
            if response.status in VALID_STATUS_CODES:
                self.redis.max(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)

这里使用协程机制,可以同时对多个 IP 地址进行连接测试。

4. 完成 server

server 模块使用flask搭建,flask 作为轻量级的 python web 开发框架,天然适合应付这种简洁的 web 网页开发。

为了实现任务,我们需要如下的功能:

  • 随机从 redis 中获取一个高分的代理地址
  • 给出一个主页来查看代理池的大致信息(剩余代理数量、爬虫的运行概况、代理池的健康程度等)
  • 固定 redis 和 mysql 的连接

5. 完成 scheduler

scheduler 的核心任务负责调度 server、getter、tester 的运行,拥有如下的子功能:

  • 容量检测&&启停器:检测代理池中可用 IP 的数量。如果超出上限值,则暂停爬虫模块;如果低于下限值,则启动爬虫模块;如果低于临界值,则向管理员发出警告,并休眠进程。
  • 日志记录:
    • 统计请求 IP 的请求次数
    • 记录错误信息|非法信息
    • 分时段固化代理池的基础信息

关于 scheduler 注意以下几点:

  1. 注意设置子模块的循环时间和循环条件。如,tester 是一直持续不断地运行的;getter 同时受到代理池容量和爬虫爬取频率上限的约束等。
  2. 子模块并行运行,互不干涉。
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:
            if not self.is_over_threshold():
                print('开始抓取代理')
                getter.run()
                time.sleep(cycle)
            else:
                time.sleep(cycle*5)

    def schedule_server(self):
        app.run(API_HOST, API_PORT)

    def is_over_threshold(self):
        return self.redis.count() >= POOL_UPPER_THRESHOLD:

    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_server)
            api_process.start()

这样一来,调度器的编写也就完成了。

6. 补充说明

使用 Docker 进行懒人部署,Dockerfile 的配置内容如下。

FROM python:3.7
WORKDIR /app
COPY . /app
RUN pip install pip -U \
    && pip install -r requirements.txt
EXPOSE 6800
CMD ["python","run.py"]

如此,proxypool 的代建也就完成了,以后再使用爬虫,就只需要从这个代理池里获取代理,就可以进行高强度的爬虫作业咯。

7. 后续优化思路

  1. Redis 是单线程运行的,虽然读写的效率非常之高,但是既然使用了 docker,不妨尝试一下使用 redis 集群,来实现一个超大规模的 proxypool?当然这也只是一个构想,等后面更加深入地学习两者之后,再做讨论吧。
  2. 在爬虫获取代理方面写的相对比较粗糙,不排除被反爬虫机制检测到的可能,姑且先用着吧,看看后续的效果具体如何。

后记

这次搭建 proxy pool,虽然使用的都是一些已经学习过的技术,但是实际操作的过程中,还有有很多的意外的收获的。

  • 在阅读Python3WebSpider 的代码时,第一次在项目中实践了元类编程,果然这些知识只有到投入使用的时候,才会有更加深刻的体会呢
  • 通过官方文档和一些辅助资料深入地学习了 flask 的应用上下文、应用全局变量的知识,以往使用 session 的方法虽然也不失为一种策略,但是这样果然合适更 cool 一点。
  • 每一次使用 docker,都仿佛是重新学习了一遍,看来还是基础不够扎实啊。

疫情形式还是十分地严峻啊,不过宅居在乡下,吃喝不愁,与世隔绝的日子虽然寂寞,但是也算是新的一种体验吧。