如何让你写的爬虫速度像坐火箭一样快【并发请求】

首先,我们需要知道什么是并发,这里的并发指的是“并行发送请求”,意思就是一次性发出多个请求,从而达到节省时间的效果!那么并发和不并发的区别在哪呢?简单来说就是这样子的:

把爬虫比喻成工人,在不并发的情况下,一个工人一次只能做一件事情,所以必须要下载完一个图片才能继续下载下一个。

顺序执行的情况

而在并发的情况下,就有很多个工人一起在干活,每个工人都被分配了一件事情做,所以可以同时下载多个图片,速度自然就快了很多。

并发的情况

当然,上面说的这个例子只是从一个宏观的角度上来看并发,实际在做的时候要让你的爬虫能并发请求的方式是分为多线程、多进程、协程三种的,并不是每一种方式在运行时的效果都像上面说的这样,这里先不做深入探讨,因为这不是本文的重点。我们现在只需要知道,只要能让爬虫并发请求,就能同时下载多个图片,让速度快得飞起,这样就够了。

那么我们要用上面说的三种方式里的哪一种来实现并发请求呢?这还用问吗?当然是选择代码最简单、改动最小,并且最容易看懂的协程啊!在Python3.4之后Python就引入了一个叫做asyncio的库,原生支持了异步IO,而在3.5之后Python又支持了asyncawait这两个语法,使得写异步代码可以像写同步代码一样简单易读。

刚刚又提到了两个词,同步和异步,这两个词的含义其实就跟上面的并发差不多,同步代码就是顺序执行的,而异步则不是,这里同样不做深入探讨,先知道有这么个东西就行了。

看到这里肯定会有人开始有疑问了,虽然前面说我们要用协程来实现并发请求,但是后面说的却是什么Python支持原生异步,那么这个异步跟协程的关系又是什么呢?

其实很简单,协程可以让你写异步代码的时候能像写同步代码一样简单,在Python3中写协程代码的核心语法就是asyncawait这两个,举个简单的例子吧:

1 def func():
2    print(1)
3    time.sleep(10)
4    print(2)

这是一段普通的函数,它属于同步代码,里面的time.sleep是普通函数,也属于同步代码。

1 async def func():  # 调用协程函数的那个函数也需要是一个协程函数
2    print(1)
3    await asyncio.sleep(10)  # 调用协程函数的时候要在前面加await
4    print(2)

而这是一个协程函数,它属于异步代码,里面的asyncio.sleep是协程函数,也属于异步代码。

它们的区别显而易见,用协程来写异步代码,除了需要换成异步的库以外,就只是多了个asyncawait而已,是不是非常简单?

那么我们在了解了怎么写协程代码之后,就能开始优化那段慢成龟速的代码了吗?答案是否定的,那段代码中使用了requests库进行网络请求,而requests是一个同步库,不能在异步环境下使用;同样,文件操作用的openfile.write也是同步的,也不能在异步环境下使用。

所以在开始之前我们还需要了解两个库,分别是aiohttp和aiofiles,aiohttp是一个异步网络请求库,而aiofiles是一个异步文件操作库。(aiofiles是基于线程池实现的,并不是真正的原生异步,但问题不大,不影响使用)

切记,异步代码不能与同步代码混用,否则如果同步代码耗时过长,异步代码就会被阻塞,失去异步的效果。而网络请求和文件操作是整个流程中最耗时的部分,所以我们必须使用异步的库来进行操作!否则就白搞了!

好了,先来看看aiohttp的用法吧,官方文档上的示例大致如下:

1 async with aiohttp.ClientSession() as session:
2    async with session.get(url) as resp:
3        result = await resp.text()

是不是觉得很麻烦,不像requests库那么方便?还觉得两层async with很丑?有没有办法让它像requests库一样方便呢?

答案是有的,有一个叫作aiohttp-requests的库,它能让上面的这段代码变成这样:

1 resp = await requests.get(url)
2 result = await resp.text()

清爽多了对吧?我们等下就用它了!记得装这个库的前提是要先装aiohttp哦!

然后我们来看看aiofiles的用法,官方文档上的示例如下:

1 async with aiofiles.open('filename', mode='r') as f:
2    contents = await f.read()
3 print(contents)

嗯,这个用起来就和用同步代码操作文件差不多了,没啥可挑剔的,直接用就完事了。

提示:aiohttp-requests默认是创建并使用了session的,对于一些需要不保留Cookie进行请求的场景需要自己实例化一个Requests类,并指定cookie_jar为aiohttp.DummyCookieJar

了解完了要用的库之后我们就可以开始对贴子中的代码进行魔改了,如果你用的不是Python3.5以上版本的话需要先准备一下环境。除了版本号大于等于3.5的Python以外,你还需要安装以下几个库:

  • aiohttp(异步网络请求库)
  • aiohttp-requests(让aiohttp用起来更方便的库)
  • aiofiles(异步文件操作库)
  • pillow(其实就是PIL库,代码中的图片操作有用到)

执行一下pip install aiohttp aiohttp-requests aiofiles pillow一次性装完,如果存在多个不同版本的Python环境记得区分好。

然后我们打开编辑器,开始改代码,首先调整一下导包的部分,将里面的requests替换成aiohttp-requests,像这样:

然后搜索一下requests,看看哪些地方用到了它。

接着把所有搜到的部分都给改成异步请求的。

同时不要忘了将所有调用过requests.get的函数都变成协程函数。

然后我们把文件操作的部分也换成异步的,使用aiofiles.open代替open

最主要的部分都换好了,接着我们将原先在if __name__ == '__main__':下的代码移到一个新写的协程函数run中,并且将调用前面协程函数的部分都加上await

再导入一下asyncio库,然后在if __name__ == '__main__':下写出这样的代码:

上面这个是Python3.7之后才能用的写法,低于Python3.7要这样写:

现在我们就可以运行一下看看修改后的代码能不能跑通了。

这里报了个错,从错误堆栈中可以看出问题是出在response = await requests.get(url=url, headers=headers)这里的,原因是self.session._request方法没有key为url的参数。这个问题很好解决,只需要将url=url变成url就好了(本来也就没必要这么指定参数写)。将代码中所有用到requests.get并且存在url=url这种写法的都做一下调整:

调整完之后再运行一次就正常了,效果和原先的代码相同。

注意!仅仅是这样并不会让速度发生很大的变化!我们最后还需要将这一堆代码中最耗时且是顺序执行、没有并发请求的部分单独放到一个协程函数中,并且用asyncio.gather来并发调用(由于原本的逻辑较为混乱,这里除了并发请求以外还进行了一些其他的微调,主要是计数和文件路径的部分,无关紧要)。

运行一下看看效果,刚运行起来一瞬间就刷了一排的下载完成,跟修改之前比起来简直是天差地别。

这就是并发请求的威力!我们仅仅是对他原本的代码进行了一些微调,把最耗时的下载图片部分简单粗暴地使用asyncio.gather并发执行了一下,速度就从龟爬变成了像坐火箭一样快!(其实代码中还有很多可以优化的点,这里就不一一拿出来讲了)

最后给大家提个醒:

虽然并发请求非常牛逼,可以让你的爬虫变得飞快,但它也不是不存在任何问题的!

如果你的并发请求数量过大(又称并发数过高),你的爬虫就相当于是在对他人的服务器进行Dos攻击(拒绝服务攻击)了!

举个例子,你在爬一个小网站的时候为了自己爬的速度更快,对并发请求的数量毫无限制,使得你的爬虫一次性发出了几百、上千个请求,但一般的小网站根本扛不住这么高的并发!几乎会在一瞬间就被你的爬虫给打爆掉!试想一下,如果你是站长,看到这样的情形你会怎么想?

如果你不能理解这个例子所产生的效果是什么样的,可以自己搭建一个Web服务,只放一个简单的页面,然后开个几百并发去请求这个页面,这样你就能切身地体会到别人是什么感受了。

所以记住,一定要合理控制并发请求的数量,不要对对方网站造成过大的压力!你给别人留活路,别人才会给你留活路!

本文分享自微信公众号 - JAVAandPython君(JAVAandPythonJun)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏志学Python

第1天:谁来给我讲讲Python?

作为无基础的初学者,只想先大概了解一下Python,随便编个小程序,并能看懂一般的程序,那些什么JAVA啊、C啊、继承啊、异常啊通通不懂怎么办,于是我找了很多资...

9840
来自专栏磐创AI技术团队的专栏

使用skimage处理图像数据的9个技巧|视觉进阶

我们非常熟悉结构化(表格)数据的预处理步骤。你可以找到缺失的值然后添补它,然后检测并处理异常值,等等这些步骤。这有助于我们建立更好、更健壮的机器学习模型。但是当...

11460
来自专栏磐创AI技术团队的专栏

PyTorch专栏(十六):使用字符级RNN进行名字分类

【磐创AI 导读】:本篇文章讲解了PyTorch专栏的第五章中的使用字符级RNN进行名字分类。查看专栏历史文章,请点击下方蓝色字体进入相应链接阅读。查看关于本专...

30210
来自专栏编程创造城市

Python高级进阶#003 pyqt5与qtdesigner对照分析

ObjectName属性:表示窗体对象的名称 。对应的python代码中设置窗体名称的方法setObjectName

15310
来自专栏咖啡拿铁

再见!SimpleDateFormat

SimpleDateFormat is a concrete class for formatting and parsing dates in a local...

8820
来自专栏APP自动化测试

AI探索(二)Tensorflow环境准备

Tensorflow支持Windows/Mac/Linux等三种操作系统, 其中windows下python需要安装3.5以上的版本

8840
来自专栏数据云团

Django实战-ORM 数据库配置

Django网络应用开发的5项基础核心技术包括模型(Model)的设计,URL 的设计与配置,View(视图)的编写,Template(模板)的设计和Form(...

7110
来自专栏APP自动化测试

AI探索(四)NumPy库的使用

是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。

10430
来自专栏GitHubDaily

很强!GitHub 中文项目排行榜新鲜出炉!

不久前,有 GitHub 用户吐槽说,GitHub 的每日趋势榜不按照国家和地区来区分,使得榜单上总会有很多点赞量很大的中文项目,有时候甚至会占据半壁江山。这位...

45520
来自专栏工作记录

Python3 成环引用所导致的莫名报错

12720

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励