在进行网页爬虫的项目时,常常会因为爬取的频率过高而触发 反爬虫机制 ,这时候,面临两个选择:
- 休息片刻。一般反爬虫机制不会进行永久的 IP 封禁,只是暂时限制访问而已,等待封禁时间结束再进行爬取即可。当然对于某些拥有黑名单机制的网站,如果封禁次数过多,封禁的时间也会随着这个次数而提高。
- 更换 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 注意以下几点:
- 注意设置子模块的循环时间和循环条件。如,tester 是一直持续不断地运行的;getter 同时受到代理池容量和爬虫爬取频率上限的约束等。
- 子模块并行运行,互不干涉。
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. 后续优化思路
- Redis 是单线程运行的,虽然读写的效率非常之高,但是既然使用了 docker,不妨尝试一下使用 redis 集群,来实现一个超大规模的 proxypool?当然这也只是一个构想,等后面更加深入地学习两者之后,再做讨论吧。
- 在爬虫获取代理方面写的相对比较粗糙,不排除被反爬虫机制检测到的可能,姑且先用着吧,看看后续的效果具体如何。
后记
这次搭建 proxy pool,虽然使用的都是一些已经学习过的技术,但是实际操作的过程中,还有有很多的意外的收获的。
- 在阅读Python3WebSpider 的代码时,第一次在项目中实践了元类编程,果然这些知识只有到投入使用的时候,才会有更加深刻的体会呢
- 通过官方文档和一些辅助资料深入地学习了 flask 的应用上下文、应用全局变量的知识,以往使用 session 的方法虽然也不失为一种策略,但是这样果然合适更 cool 一点。
- 每一次使用 docker,都仿佛是重新学习了一遍,看来还是基础不够扎实啊。
疫情形式还是十分地严峻啊,不过宅居在乡下,吃喝不愁,与世隔绝的日子虽然寂寞,但是也算是新的一种体验吧。