Skip to content

Aiohttp库

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

前面我们所写的爬虫程序都是同步运行的,就是爬取页面的机制是串行的,也就是说必须等到第一个页面的访问、下载、采集、入库等一系列操作都完成以后,爬虫才会去爬取第二个页面,而在访问、下载过程中 CPU 资源是闲置的,而在采集、入库操作中网络资源是闲置的,假如某个步骤时间过长,必然会延长采集时间,降低爬虫效率。

爬虫效率

首先我们写一个测试脚本,比较单线程、多线程、多进程分别请求测试地址 10 次的效率,这个测试地址对每次响应增加了 5 秒延迟。具体代码如下:

python
import time
import requests
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor

# 爬取次数10次
times = 10
# 工作单元
unit = 10

# 爬虫耗时装饰器
def timer(func):
    def deco(name):
        start = time.time()
        func(name)
        stop = time.time()
        print(f'{name}爬虫耗时:{stop - start:.2f}秒!')
    return deco

# 爬虫发送请求
def request():
    requests.get('https://www.httpbin.org/delay/5')

@timer
def one(name):
    for _ in range(times):
        request()

@timer
def two(name):
    # 启用线程池,最大工作单元为10个
    with ThreadPoolExecutor(max_workers=unit) as pool:
        for _ in range(times):
            pool.submit(request)

@timer
def three(name):
    # 启用进程池,最大工作单元为10个
    with ProcessPoolExecutor(max_workers=unit) as pool:
        for _ in range(times):
            pool.submit(request)

if __name__ == '__main__':
    one('单线程')    # 输出:单线程爬虫耗时:64.96秒!
    two('多线程')    # 输出:多线程爬虫耗时:7.95秒!
    three('多进程')  # 输出:多进程爬虫耗时:16.16秒!

从上面代码的运行的结果比较下来,单线程、多线程、多进程采集效率如下:

  • 单线程爬虫耗时最长,这是由于每个页面都至少等待 5 秒才能加载出来,因此 10 个页面至少要花费 50 秒,加上网站本身的负载,总的爬取时间为 64.96 秒,所以如果用单线程这么爬取,总耗时将会非常大。
  • 多进程爬虫耗时居中,这是因为多进程启动了多个爬虫,可以并行的爬取网页,因此成倍的缩短了爬取时间,但由于启动多进程需要更多的资源,因此启动时间偏长,导致耗时增大。
  • 多线程爬虫耗时最短,这是因为整个过程中只有一个进程,启动时间短,而且是并发的爬取网页,刚好适用于爬虫这种 I/O 密集型任务,因此效率最高。

异步爬取

在前面我们讲任务类型的时候,提到过爬虫属于 I/O 密集型任务,而对于 I/O 密集型任务适用的方法就是多线程、协程。虽然上面例子中多线程爬虫的效率是最高了,但多线程本质是多个线程在轮换执行,不能真正地实现异步。如果使用协程其效率还会进一步提升,因为协程是一个线程异步执行,避免了线程之间的切换操作,从而节省了时间提升了效率,有时甚至可以提升成百倍。想要实现一个异步爬虫,就必须在两个方面实现异步操作:

  • 在任务调度方面实现异步,这就需要使用到协程,可以借助于我们之前学习的 asyncio 异步库,通过里面的事件循环来异步调度。
  • 在网络请求方面实现异步,上面的例子中,我们是通过 requests 发起请求的,但该请求库只能发送同步请求,并不支持异步请求,也就是说从向服务器请求网页到服务器响应并返回网页这一过程是同步的。如果在单线程爬虫中下载网页时间太长则会导致阻塞,而且 CPU 性能和网络资源在这过程中也得不到充分的使用,这时我们就需要考虑使用 aiohttp 库了。

安装 Aiohttp

Aiohttp 是一个基于 asyncio 和部分 C 语言实现的 HTTP 框架,它可以帮助我们以异步 I/O 的方式地实现 HTTP 请求,也就是说,我们只管去发送网页的请求,不管服务器是否已经返回数据,这样可以让爬虫程序的效率大大提高。安装 aiohttp 三方库需要 Python3.5.3 以后的版本,执行下面安装命令:

bash
pip install aiohttp

使用方法

使用 aiohttp 和使用 requests 在方法上是比较相似的,但在写法上有明显区别,具体如下:

  • 使用 aiohttp 发送 GET 请求,代码如下:
python
import aiohttp

async def download(url):
    # 支持异步的上下文管理器,可以自动的关闭会话连接。
    async with aiohttp.ClientSession() as session:
        # 使用session来异步的发送GET请求
        async with session.get(url) as response:
            # 挂起response的字节响应
            result = await response.content.read()
            # 挂起response的文本响应
            result = await response.text()
            # 挂起response的json响应
            result = await response.json()
  • 使用 aiohttp 发送 POST 请求,代码如下:
python
# POST请求,data即POST表单提交的参数
async def download(url):
    async with aiohttp.ClientSession() as session:
        async with session.post(url, data={'k': 'v'}) as response:
            result = await response.text()

# POST请求,json即提交的json参数
async def download(url):
    async with aiohttp.ClientSession() as session:
        async with session.post(url, json={'k': 'v'}) as response:
            result = await response.text()
  • 使用 aiohttp 设置请求的参数,代码如下:
python
# 设置请求头
async def download(url):
    async with aiohttp.ClientSession() as session:
        headers = {
            'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
        }
        async with session.get(url, headers=headers) as response:
            result = await response.text()

# 设置超时(超时时间为10秒,超时抛出TimeoutError异常)
async def download(url):
    timeout = aiohttp.ClientTimeout(total=10)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.get(url) as response:
            result = await response.text()

# 设置代理(不支持https代理)
async def download(url):
    async with aiohttp.ClientSession() as session:
        proxy = "http://124.113.251.151:8085"
        async with session.get(url, proxy=proxy) as response:
            result = await response.text()

建议

注意在 aiohttp 中设置代理使用的是 proxy 参数名,而在 requests 中设置代理使用的是 proxies 参数名。

重要

对于一些需要的任务对象的操作,前面需要加 await 来挂起,若返回的不是协程对象,则不需要修饰。

异步案例

现在,我们将上面的爬虫例子用 aiohttp 库结合 asyncio 库来重写,具体代码如下:

python
import time
import aiohttp
import asyncio

# 爬取次数10次
times = 10

# 爬虫耗时装饰器
def timer(func):
    def deco(name):
        start = time.time()
        func(name)
        stop = time.time()
        print(f'{name}爬虫耗时:{stop - start:.2f}秒!')
    return deco

# 使用async将发送请求方法封装为一个协程对象
async def request():
    # 引入aiohttp.ClientSession类建立session对象去打开网页
    async with aiohttp.ClientSession() as session:
        # 异步发送GET请求
        url = 'https://www.httpbin.org/delay/5'
        async with session.get(url) as response:
            # 等待网络响应属于I/O操作,需要await挂起,交出执行权
            await response.text()

@timer
def spider(name):
    # 创建一个事件循环对象
    loop = asyncio.get_event_loop()
    # 用列表保存10个request协程对象
    tasks = [request() for _ in range(times)]
    # asyncio.wait接受由协程对象组成的列表,再将他们注册到事件循环中执行
    loop.run_until_complete(asyncio.wait(tasks))

if __name__ == '__main__':
    spider('异步')
'''
输出:异步爬虫耗时:7.52秒!
注释:这里我们真正意义上的实现了异步爬虫,它的用时比多线程更短,虽然效率没有太多,但这仅仅只是爬取10次的效率,如果爬取1000次、10000次,那么提升的效率是很明显的,这就是异步高效率。
'''

并发限制

Aiohttp 可以支持非常高的并发量,如几万、十万、百万都是能做到的,但面对如此高的并发量,目标网站可能无法在短时间内响应,而且由瞬间将目标网站爬挂掉的危险,这需要我们控制一下爬取的并发量。一般情况下,可以借助 asyncio 的 Semaphore 来控制并发量,因此可以将上面代码修改为:

python
import time
import aiohttp
import asyncio

# 爬取次数10次
times = 10
# 借助Semaphore创建一个信号量对象,限制并发量为5个
semaphore = asyncio.Semaphore(5)

# 爬虫耗时装饰器
def timer(func):
    def deco(name):
        start = time.time()
        func(name)
        stop = time.time()
        print(f'{name}爬虫耗时:{stop - start:.2f}秒!')
    return deco

# 使用async将发送请求方法封装为一个协程对象
async def request():
    # 有了信号量的控制,最大协程数量被限制在5个
    async with semaphore:
        async with aiohttp.ClientSession() as session:
        url = 'https://www.httpbin.org/delay/5'
        async with session.get(url) as response:
            await response.text()

@timer
def spider(name):
    # 创建一个事件循环对象
    loop = asyncio.get_event_loop()
    # 用列表保存10个request协程对象
    tasks = [request() for _ in range(times)]
    # asyncio.wait接受由协程对象组成的列表,再将他们注册到事件循环中执行
    loop.run_until_complete(asyncio.wait(tasks))

if __name__ == '__main__':
    spider('异步')
'''
输出:异步爬虫耗时:15.14秒!
注释:可以看到这里的对并发进行了限制,采集时间就是上面没有并发限制时间的2倍。
'''

异步读写

上面我们使用 aiohttp 库实现了异步发送 HTTP 请求,获得了极高的采集效率。假如,爬虫所爬取的内容是图片,那么该如何写入图片的字节数据呢?这时有人会想到我们前面所学的 open() 方法,但是该方法不支持异步 I/O 操作,因此这里就需要考虑使用 aiofiles 库了。

安装 Aiofiles

aiofiles 是一个 Python 库,它允许你以异步的方式读写文件。在异步编程框架(如 asyncio)中,aiofiles 发挥着重要作用,因为它不会阻塞事件循环,从而提高了程序的整体性能,你可以将其应用于接口异步日志这个场景。安装 aiofiles 三方库需要执行下面安装命令:

pip install aiofiles

异步案例

为了方便起见,我们这里还是使用上面的例子,只不过爬取对象从网页变为了图片,代码如下:

python
import asyncio
import aiohttp
import aiofiles

async def request(url):
    file_name = url.split("/")[-1]
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            # 等待网络响应属于I/O操作,需要await挂起,交出执行权
            content = await response.content.read()
            # 使用aiofiles异步写入文件
            async with aiofiles.open(file_name, mode='wb') as f:
                # 等待写入完成属于I/O操作,需要await挂起,交出执行权
                await f.write(content)

async def main():
    urls = [
        'https://bai.du.com/1.png'
        'https://bai.du.com/2.png'
        'https://bai.du.com/3.png'
    ]
    tasks = []
    for url in urls:
        task = asyncio.create_task(request(url))
        tasks.append(task)
    await asyncio.wait(tasks)


if __name__ == '__main__':
    main()

归纳总结

从使用来看:requests 库使用较为简单,但不支持异步请求;aiohttp 库的使用稍复杂,且必须配合 asyncio 库来使用,但支持异步请求。

从结果输出顺序看:单线程爬虫的结果顺序任务顺序一致,多进程爬虫、多线程爬虫、异步爬虫的结果顺序和任务顺序不一致。这是由于执行请求顺序不确定性造成的,其原理就是谁先处理完就输出谁。

从时间来看:单线程爬虫耗时 64.96 秒;多进程爬虫耗时 16.16 秒;多线程爬虫耗时 7.95 秒;异步爬虫耗时 7.52 秒。

从方法来看:爬虫属于“I/O密集型任务”,适合的方法就是使用“多线程”或“协程”。