系列文章总目录:【2022 年】Python3 爬虫学习教程,本教程内容多数来自于《Python3网络爬虫开发实战(第二版)》一书,目前截止 2022 年,可以将爬虫基本技术进行系统讲解,同时将最新前沿爬虫技术如异步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技术、WebAssembly、大规模分布式、Docker、Kubernetes 等,市面上目前就仅有《Python3 网络爬虫开发实战(第二版)》一书了,点击了解详情。
在上一节中,我们介绍了异步爬虫的基本原理和 asyncio 的基本用法,并且在最后简单提及了使用 aiohttp 来实现网页爬取的过程。在本节中,我们来介绍一下 aiohttp 的常见用法。
1. 基本介绍
前面介绍的 asyncio 模块内部实现了对 TCP、UDP、SSL 协议的异步操作,但是对于 HTTP 请求来说,我们就需要用到 aiohttp 来实现了。
aiohttp 是一个基于 asyncio 的异步 HTTP 网络模块,它既提供了服务端,又提供了客户端。其中我们用服务端可以搭建一个支持异步处理的服务器,就是用来处理请求并返回响应的,类似于 Django、Flask、Tornado 等一些 Web 服务器。而客户端可以用来发起请求,类似于使用 requests 发起一个 HTTP 请求然后获得响应,但 requests 发起的是同步的网络请求,aiohttp 则是异步的。
本节中,我们主要了解一下 aiohttp 客户端部分的用法。
2. 基本实例
首先,我们来看一个基本的 aiohttp 请求案例,代码如下:
1 |
import aiohttp |
这里我们使用 aiohttp 来爬取我的个人博客,获得了源码和响应状态码并输出出来,运行结果如下:
1 |
html: |
这里网页源码过长,只截取输出了一部分。可以看到,这里我们成功获取了网页的源代码及响应状态码 200,也就完成了一次基本的 HTTP 请求,即我们成功使用 aiohttp 通过异步的方式来进行了网页爬取。当然,这个操作用之前讲的 requests 也可以做到。
可以看到,其请求方法的定义和之前有了明显的区别,主要有如下几点:
- 首先在导入库的时候,我们除了必须要引入 aiohttp 这个库之外,还必须要引入 asyncio 这个库。因为要实现异步爬取,需要启动协程,而协程则需要借助于 asyncio 里面的事件循环来执行。除了事件循环,asyncio 里面也提供了很多基础的异步操作。
- 异步爬取方法的定义和之前有所不同,在每个异步方法前面统一要加
async
来修饰。 with as
语句前面同样需要加async
来修饰。在 Python 中,with as
语句用于声明一个上下文管理器,能够帮我们自动分配和释放资源。而在异步方法中,with as
前面加上async
代表声明一个支持异步的上下文管理器。- 对于一些返回
coroutine
的操作,前面需要加await
来修饰。比如response
调用text
方法,查询 API 可以发现,其返回的是coroutine
对象,那么前面就要加await
;而对于状态码来说,其返回值就是一个数值类型,那么前面就不需要加await
。所以,这里可以按照实际情况处理,参考官方文档说明,看看其对应的返回值是怎样的类型,然后决定加不加await
就可以了。 - 最后,定义完爬取方法之后,实际上是
main
方法调用了fetch
方法。要运行的话,必须要启用事件循环,而事件循环就需要使用 asyncio 库,然后使用run_until_complete
方法来运行。
注意:在 Python 3.7 及以后的版本中,我们可以使用
asyncio.run(main())
来代替最后的启动操作,不需要显示声明事件循环,run
方法内部会自动启动一个事件循环。但这里为了兼容更多的 Python 版本,依然还是显式声明了事件循环。
3. URL 参数设置
对于 URL 参数的设置,我们可以借助于 params
参数,传入一个字典即可,示例如下:
1 |
import aiohttp |
运行结果如下:
1 |
{ |
这里可以看到,其实际请求的 URL 为 https://httpbin.org/get?name=germey&age=25,其 URL 请求参数就对应了 params
的内容。
4. 其他请求类型
另外,aiohttp 还支持其他请求类型,如 POST、PUT、DELETE 等,这和 requests 的使用方式有点类似,示例如下:
1 |
session.post('http://httpbin.org/post', data=b'data') |
要使用这些方法,只需要把对应的方法和参数替换一下即可。
5. POST 请求
对于 POST 表单提交,其对应的请求头的 Content-Type
为 application/x-www-form-urlencoded
,我们可以用如下方式来实现,代码示例如下:
1 |
import aiohttp |
运行结果如下:
1 |
{ |
对于 POST JSON 数据提交,其对应的请求头的 Content-Type
为 application/json
,我们只需要将 post
方法的 data
参数改成 json
即可,代码示例如下:
1 |
async def main(): |
运行结果如下:
1 |
{ |
可以发现,其实现也和 requests 非常像,不同的参数支持不同类型的请求内容。
6. 响应
对于响应来说,我们可以用如下方法分别获取响应的状态码、响应头、响应体、响应体二进制内容、响应体 JSON 结果,示例如下:
1 |
import aiohttp |
运行结果如下:
1 |
status: 200 |
这里我们可以看到有些字段前面需要加 await
,有的则不需要。其原则是,如果它返回的是一个 coroutine
对象(如 async
修饰的方法),那么前面就要加 await
,具体可以看 aiohttp 的 API,其链接为:https://docs.aiohttp.org/en/stable/client_reference.html。
7. 超时设置
对于超时设置,我们可以借助 ClientTimeout
对象,比如这里要设置 1 秒的超时,可以这么实现:
1 |
import aiohttp |
如果在 1 秒之内成功获取响应的话,运行结果如下:
1 |
200 |
如果超时的话,会抛出 TimeoutError
异常,其类型为 asyncio.TimeoutError
,我们再进行异常捕获即可。
另外,声明 ClientTimeout
对象时还有其他参数,如 connect
、socket_connect
等,详细可以参考官方文档:https://docs.aiohttp.org/en/stable/client_quickstart.html#timeouts。
8. 并发限制
由于 aiohttp 可以支持非常大的并发,比如上万、十万、百万都是能做到的,但对于这么大的并发量,目标网站很可能在短时间内无法响应,而且很可能瞬时间将目标网站爬挂掉,所以我们需要控制一下爬取的并发量。
一般情况下,我们可以借助于 asyncio 的 Semaphore
来控制并发量,示例如下:
1 |
import asyncio |
这里我们声明了 CONCURRENCY
(代表爬取的最大并发量)为 5,同时声明爬取的目标 URL 为百度。接着,我们借助于 Semaphore
创建了一个信号量对象,将其赋值为 semaphore
,这样我们就可以用它来控制最大并发量了。怎么使用呢?这里我们把它直接放置在对应的爬取方法里面,使用 async with
语句将 semaphore
作为上下文对象即可。这样的话,信号量可以控制进入爬取的最大协程数量,即我们声明的 CONCURRENCY
的值。
在 main
方法里面,我们声明了 10000 个 task
,将其传递给 gather
方法运行。倘若不加以限制,这 10000 个 task
会被同时执行,并发数量太大。但有了信号量的控制之后,同时运行的 task
的数量最大会被控制在 5 个,这样就能给 aiohttp 限制速度了。
9. 总结
本节我们了解了 aiohttp 的基本使用方法,更详细的内容还是推荐大家到官方文档查阅,详见 https://docs.aiohttp.org/。