Python-asyncio+aiohttp 异步库的学习

之前写过asyncio+asynico+aiohttp模块实现简单的异步爬虫,趁着今天晚上又重新学习了一波,参考大佬崔庆才的文章。今天把学习的心得以及如何使用的来分享一下。

python使用协程最常用的库莫过于asynico,首先我们需要了解下面几个概念:
1, event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生时候,调用对应的处理方法。
2, coroutine: 中文翻译为协程,在python中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。
我们可以使用async关键字来定义一个方法,这个方法在调用的时,不会被立刻执行而是返回一个协程对象。
3,task: 任务,他是对协程对象的进一步封装,包含了任务的各个状态。
4,future: 代表将来执行或没有执行的任务的结果,实际上和task没有本质区别。
另外我们还需要了解async/await关键字,它是Python3.5才出现的,专门用于定义协程。其中,async定义一个协程,await用来挂起阻塞方法的执行。

这里的两个库的安装我就不讲了,直接pip安装即可。

一: 定义协程

代码 1.1

# 基础知识建议你去大佬那在看看,直接分享一下代码实现的。
async def execute(x):
    print('number:', x)


if __name__ == '__main__':
    start_time = time.time()
    coroutine = execute(1)
    print('coroutine:', coroutine)
    print('After calling execute')
    loop = asyncio.get_event_loop()
    loop.run_until_complete(coroutine)
    print('after calling loop')
    end_time = time.time()
    print('总共花费 %s 时间' % (str(end_time - start_time)))


首先我们引入asynico这个包,这样我们才可以使用async和await,然后我们使用async定义一个execute()方法,方法接受一个数字参数,方法执行后打印这个数字。

随后我们直接调用这个方法,然而这个方法并没有执行,而是返回一个coroutine写成对象,随后我们使用get_event_llop()方法创建了一个事件循环loop,并调用了loop对象的run_until_complete()方法
将协程注册到事件循环loop中,然后启动。最后我们才看到了execute()方法打印了输出结果。
可见,async定义的方法就会变成一个无法直接执行的coroutine对象,必须将其注册到事件循环中才可以执行。

上文我们还提到了task,他是对coroutine对象的进一步封装,它里面相比coroutine对象多了运行状态,比如:running,finished等,我们可以使用
这些状态来获取协程对象的执行情况。

在上面的例子中,当我们将coroutine对象传递给run_until_complete()方法的时候,实际上它进行了一个操作就是将coroutine封装成task对象。

代码 1.2

async def execute(x):
    print('number:', x)
    return x


if __name__ == '__main__':
    coroutine = execute(1)
    print('after calling execute')
    loop = asyncio.get_event_loop()
    task = loop.create_task(coroutine)
    print('task:', task)
    loop.run_until_complete(task)
    print('task:', task)
    print('after calling loop')

从结果来看 我们首先定义loop对象,接着调用了它的create_task()方法将coroutine对象转化为task对象,随后我们打印输出一下,发现它是pending状态,
接着我们将task对象添加到事件循环中得到执行,然后我们再打印输出一下task发现他的状态就变成了finished同事还可以看到其result变成了1,
也就是我们定义execute方法的返回结果。

代码 1.3

async def execute(x):
    print('number:', x)
    return x


if __name__ == '__main__':
    coroutine = execute(1)
    print('after calling execute')
    task = asyncio.ensure_future(coroutine)
    print('task:', task)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    print('task:', task)
    print('after calling loop')


另外定义task对象还有一种方式,就是直接通过ensure_future()方法,返回结果也是task对象,这样的话我们就可以不借助于loop来定义,几十我们还没有声明loop也可以提前定义好
task对象,写法如上

二:绑定回调

代码 2.1

"""我们也可以为某个task绑定一个回调方法,来看看下面的例子"""

async def get_html():
    url = 'https://www.baidu.com'
    response = requests.get(url)
    return response


def callback(task):
    print('response:', task.result())


async def get_page():
    url = 'https://www.baidu.com'
    response = requests.get(url)
    return response.text


if __name__ == '__main__':
    coroutine = get_html()
    task = asyncio.ensure_future(coroutine)
    task.add_done_callback(callback)
    print('task:', task)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    print('task:', task)

代码 2.2

# 但是实际上饿哦们不用回调方法,直接在task运行完毕之后也可以直接调用result()方法获取结果

async def get_html():
    url = 'https://www.baidu.com'
    response = requests.get(url)
    return response


if __name__ == '__main__':
    coroutine = get_html()
    task = asyncio.ensure_future(coroutine)
    print('task:', task)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    print('task:', task.result())

三:多任务协程

代码 3.1

上面的例子我们只执行了一次请求,如果我们想执行多次请求应该怎么办呢?我们可以定义一个task列表,然后使用asyncio的wait()方法即可执行。


async def get_html():
    url = 'https://www.baidu.com'
    response = requests.get(url)
    return response


if __name__ == '__main__':
    tasks = [asyncio.ensure_future(get_html()) for i in range(5)]
    print('task status........', tasks)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    for task in tasks:
        print('task result', task.result())

这里我们使用一个for循环创建了五个task,组成一个列表,然后把这个列表首先传递给了asyncio的wait()方法,然后在将其注册到时间循环中,
就可以发起五个任务了,最后我们再将多任务的运行结果输出出来,就可以了。

代码 3.2

#  前面说了一遍又是async,又是task,又是callback,但发现没有直到现在并没有看出什么优势,反而一大堆的写法感觉更加奇怪和麻烦。下面就来简单解决一下。


start = time.time()


async def request():
    url = 'https://www.baidu.com'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.status_code)


tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('close time:', end - start)

"""
结果耗时:
    close time: 0.6830143928527832
"""
start = time.time()
for i in range(5):
    response = requests.get(url='https://www.baidu.com')
    print(response.status_code)
end = time.time()
print('close time:', end - start)
"""
结果耗时:
    close time: 0.6659989356994629
    
发现和正常请求并没有什么两样,说好的异步操作呢?
    其实想要实现异步操作处理,我们得先要有挂起来的操作,当一个任务等待IO结果的时候,可以挂起当前任务,转而去执行其他任务,
    这样我们才能充分的利用好资源,而之前的程序都是没有挂起来的,怎么可能实现异步呢?
    那么我们知道await可以将耗时的等待的操作挂起,让出控制权,当协程执行遇到await时间循环就会将本协程挂起,转而去执行别的协程,
    直到其他的协程挂起或执行完毕。
    
    那么考虑了一下 我们为什么不能把每一次的请求给挂起来呢?

四:协程实现

# 抄袭大佬原话,说实话大佬就是厉害想到的很可以,借用。
上面的代码中,我们用一个网络请求作为示例,这就是一个耗时等待的操作,因为我们请求网页之后需要等待页面响应并返回结果。耗时等待的操作一般都是 IO 操作,比如文件读取、网络请求等等。协程对于处理这种操作是有很大优势的,当遇到需要等待的情况的时候,程序可以暂时挂起,转而去执行其他的操作,从而避免一直等待一个程序而耗费过多的时间,充分利用资源。

为了表现出协程的优势,我们需要先创建一个合适的实验环境,最好的方法就是模拟一个需要等待一定时间才可以获取返回结果的网页,上面的代码中使用了百度,但百度的响应太快了,而且响应速度也会受本机网速影响,所以最好的方式是自己在本地模拟一个慢速服务器,这里我们选用 Flask。

代码 4.1

from flask import Flask
import time
 
app = Flask(__name__)
 
@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'
 
if __name__ == '__main__':
    app.run(threaded=True)

代码 4.2

# 接下来我们再重新使用上面的方法请求一遍:
import asyncio
import requests
import time
 
start = time.time()
 
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
end = time.time()
print('Cost time:', end - start)

依然还是顺次执行的,耗时 15 秒,平均一个请求耗时 3 秒,说好的异步处理呢?

其实,要实现异步处理,我们得先要有挂起的操作,当一个任务需要等待 IO 结果的时候,可以挂起当前任务,转而去执行其他任务,这样我们才能充分利用好资源,上面方法都是一本正经的串行走下来,连个挂起都没有,怎么可能实现异步?想太多了。

要实现异步,接下来我们再了解一下 await 的用法,使用 await 可以将耗时等待的操作挂起,让出控制权。当协程执行的时候遇到 await,时间循环就会将本协程挂起,转而去执行别的协程,直到其他的协程挂起或执行完毕。

所以,我们可能会将代码中的 request() 方法改成如下的样子:

代码 4.3

# 这里我有
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await requests.get(url)
    print('Get response from', url, 'Result:', response.text)


if __name__ == '__main__':
    start = time.time()

    tasks = [asyncio.ensure_future(request()) for _ in range(5)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

    end = time.time()
    print('close time:', end - start)

"""
TypeError: object Response can't be used in 'await' expression
为什么会报错?意思就是requests返回的对象不能和await一起使用,那就要看看await后面的对象的格式了
A native coroutine object returned from a native coroutine function,一个原生 coroutine 对象。
A generator-based coroutine object returned from a function decorated with types.coroutine(),一个由 types.coroutine() 修饰的生成器,这个生成器可以返回 coroutine 对象。
An object with an await__ method returning an iterator,一个包含 __await 方法的对象返回的一个迭代器。

既然await后面可以跟一个coroutine对象,那么我用async把请求的方法改成coroutine对象不就可以了吗?说改就改

代码 4.4

async def get(url):
    return requests.get(url)


async def get_html():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response.status_code)


if __name__ == '__main__':
    start = time.time()
    tasks = [asyncio.ensure_future(get_html()) for _ in range(5)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

    end = time.time()
    print('close time:', end - start)

"""
结果耗时:
    close time: 15.13431945236206
从结果来看还是不行,它还不是异步执行,仅仅将涉及到IO操作的代码封装到async修饰的方法里面是不可行的!!所以就需要
aiohttp派上

五:使用 aiohttp

代码 5.1

aiohttp是一个支持异步请求的库,利用它和asyncio配合我们可以非常方便地实现异步请求操作

安装:
    pip install aiohttp
官方文档地址:
    https://aiohttp.readthedocs.io/
"""
import aiohttp


async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result


async def get_html():
    url = 'http://127.0.0.1:5000/'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response)


if __name__ == '__main__':
    start = time.time()
    tasks = [asyncio.ensure_future(get_html()) for _ in range(5)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

    end = time.time()
    print('close time:', end - start)

"""
结果耗时:
    close time: 3.0441458225250244
成功了,耗时时间变少了。
代码里面我们使用了await,后面跟了get方法,在执行这五个协程的时候,如果遇到了await,那么就会当当前的协程挂起,转而去执行其他协程,
直到其他的协程也挂起或执行完毕,在进行下一个协程。

开始运行时间循环会执行第一个task,针对第一个task,当执行到第一个await跟着的get()方法时,他被挂起,但这个get()方法是非阻塞的,挂起之后立刻执行,
创建了ClientSession对象,紧着这遇到第二个await然后就被挂起了,由于耗时,所以一值没有唤醒,紧着这寻找没有被挂起的协程继续执行,
3秒过后都哟了响应。
怎么样?这就是异步操作的便捷之处,当遇到阻塞式操作时,任务被挂起,程序接着去执行其他的任务,而不是傻傻地等着,这样可以充分利用 CPU 时间,
而不必把时间浪费在等待 IO 上。

有人就会说了,既然这样的话,在上面的例子中,在发出网络请求后,既然接下来的 3 秒都是在等待的,在 3 秒之内,CPU 可以处理的 task 数量远不止这些,
那么岂不是我们放 10 个、20 个、50 个、100 个、1000 个 task 一起执行,最后得到所有结果的耗时不都是 3 秒左右吗?因为这几个任务被挂起后都是一起等待的。

理论来说确实是这样的,不过有个前提,那就是服务器在同一时刻接受无限次请求都能保证正常返回结果,也就是服务器无限抗压,另外还要忽略 IO 传输时延,
确实可以做到无限 task 一起执行且在预想时间内得到结果。

代码 5.2

# 那么将任务数量改成100个看怎么样?
import aiohttp


async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    # session.close()
    return result


async def get_html():
    url = 'http://127.0.0.1:5000/'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response)


if __name__ == '__main__':
    start = time.time()
    tasks = [asyncio.ensure_future(get_html()) for _ in range(100)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    end = time.time()

结果耗时:
    close time: 3.533999443054199
多出来的时间都是IO延迟了。显然效果是非常明显的咯。

六 :使用多进程

代码 6.1

# 那么可不可以考虑使用进程和这个进行结合来提高速度呢?先看一下多进程下速度的提高。
# 正常访问


def get_html():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)


if __name__ == '__main__':
    start = time.time()
    for _ in range(100):
        get_html()

    end = time.time()
    print('Cost time:', end - start)

"""
结果耗时:
    Cost time: 305.16639709472656
"""

# 进程的使用
import multiprocessing


def get_html(_):
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)


if __name__ == '__main__':
    start = time.time()
    cpu_count = multiprocessing.cpu_count()
    print('this computer has %s CPU' % cpu_count)
    pool = multiprocessing.Pool(cpu_count)
    pool.map(get_html, range(100))
    end = time.time()
    print('Cost time:', end - start)
    
结果耗时:
    Cost time: 86.07402276992798
可以看出确实是降低了时间。那么能考虑结合一波吗?说来就来.

七:与多进程的结合

# 与多进程的结合
"""
别人已经开发好了一个库叫做:aiomultiprocess
了解:https://www.youtube.com/watch?v=0kXaLh8Fz3k
安装方法:
    pip install aiomultiprocess
"""
import asyncio
import aiohttp
import time
from aiomultiprocess import Pool


async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result


async def get_html():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result


if __name__ == '__main__':
    start = time.time()
    coroutine = get_html()
    task = asyncio.ensure_future(coroutine)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)

    end = time.time()
    print('Cost time:', end - start)

结果耗时:
    Cost time: 3.1156570434570312
因为自己写的接口的原因可能不是很快,实际爬虫中,速度应该是很快的。

以上是今天晚上自己的练习,虽然大佬讲解的很详细,但是还是建议自己手写一遍,并真正去用到自己的代码上去,自己在看之前写的代码,就很容易理解所表达的意思。如果你看到我这篇文章有疑问,别怀疑我是模仿崔庆才大佬的博客写的,只不过自己手写了一遍。
崔大佬的原文链接:https://cuiqingcai.com/6160.html
该睡觉了。晚安地球。

猜你喜欢

转载自blog.csdn.net/weixin_42812527/article/details/84555074