zoukankan      html  css  js  c++  java
  • 爬虫高性能asyncio+ahttpio

    async实现协程,异步编程

    我们都知道,现在的服务器开发对于IO调度的优先级控制权已经不再依靠系统,都希望采用协程的方式实现高效的并发任务,如js、lua等在异步协程方面都做的很强大。

    python在3.4版本也加入了协程的概念,并在3.5确定了基本完善的语法和实现方式。同时3.6也对其进行了如解除了await和yield在同一个函数体限制等相关的优化。

    asyncio是python3.4版本引入到标准库,python2x没有加这个库,毕竟python3x才是未来啊,哈哈!python3.5又加入了async/await特性。

    在学习asyncio之前,要先搞清楚同步/异步的概念

    • event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
    • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
    • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
    • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
    • async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。

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

    1、创建协程

    首先定义一个协程,在def前加入async声明,就可以定义一个协程函数。

    一个协程函数不能直接调用运行,只能把协程加入到事件循环loop中。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。

    例如:

    import asyncio
    
    async def func(a):
        print('leiting':a)
    corouine = func(1)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(func())
    
    

    在上面的例子中,当我们将 coroutine 对象传递给 run_until_complete() 方法的时候,实际上它进行了一个操作就是将 coroutine 封装成了 task 对象,我们也可以显式地进行声明, 如下所示 :

    import asyncio
    
    async def execute(x):
        print('Number:', x)
        return x
    
    coroutine = execute(1)
    print('Coroutine:', coroutine)
    print('After calling execute')
    
    loop = asyncio.get_event_loop()
    
    task = loop.create_task(coroutine)
    print('Task1:', task)
    #当我们将 coroutine 对象传递给 run_until_complete() 方法的时候,实际上它进行了一个操作就是将 coroutine 封装成了 task 对象,我们也可以显式地进行声明
    loop.run_until_complete(task)
    print('Task:', task)
    print('After calling loop')
    
    结果
    #Coroutine: <coroutine object execute at 0x0000017A398CB3C8>
    #After calling execute
    #Task1: <Task pending coro=<execute() running at D:/Python/项目位置/test.py:17>>
    #Number: 1
    #Task: <Task finished coro=<execute() done, defined at D:/Python/项目位置/test.py:17> result=1>
    #After calling loop
    

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

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

    import asyncio
    
    async def execute(x):
        print('Number:', x)
        return x
    
    coroutine = execute(1)
    print('Coroutine:', coroutine)
    print('After calling execute')
    
    task = asyncio.ensure_future(coroutine)
    print('Task1:', task)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    
    结果
    #Coroutine: <coroutine object execute at 0x0000016A2C34B3C8>
    #After calling execute
    #Task1: <Task pending coro=<execute() running at D:/Python/项目位置/test.py:17>>
    #Number: 1
    #Task: <Task finished coro=<execute() done, defined at D:/Python/项目位置test.py:17> result=1>
    #After calling loop
    

    发现其效果都是一样的。

    绑定回调

    (1)调用add_done_callback()方法为某个task绑定一个回调方法。我们将 callback() 方法传递给了封装好的 task 对象,这样当 task 执行完毕之后就可以调用 callback() 方法了,同时 task 对象还会作为参数传递给 callback() 方法,调用 task 对象的 result() 方法就可以获取返回结果了

    import asyncio
    import requests
    async def request():
        url = 'https://www.baidu.com'
        status = requests.get(url)
        status = status.text
        return status
    def callback(task):
        print('Status:', task.result())
    coroutine = request()
    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)直接调用task运行完毕之后直接调用result()方法获取结果

    import asyncio
    import requests
    async def request():
        url = 'https://www.baidu.com'
        status = requests.get(url)
        return status
    coroutine = request()
    task = asyncio.ensure_future(coroutine)
    print('Task:', task)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    print('Task:', task)
    print('Task Result:', task.result())
    
    #Task: <Task pending coro=<request() running at D:/Python/项目位置/test.py:53>>
    #Task: <Task finished coro=<request() done, defined at D:/Python/项目位置/test.py:53> result=<Response [200]>>
    #Task Result: <Response [200]>
    

    3、多任务协程

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

    import asyncio
    import requests
    async def request():
        url = 'https://www.baidu.com'
        status = requests.get(url)
        return status
    tasks = [asyncio.ensure_future(request()) for _ in range(5)]
    print('Tasks:', tasks)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    for task in tasks:
        print('Task Result:', task.result())
        
    Task Result: <Response [200]>
    Task Result: <Response [200]>
    Task Result: <Response [200]>
    Task Result: <Response [200]>
    Task Result: <Response [200]>
    

    4、协程实现

    (1)使用 await 可以将耗时等待的操作挂起,让出控制权。当协程执行的时候遇到 await,时间循环就会将本协程挂起,转而去执行别的协程,直到其他的协程挂起或执行完毕。

    import asyncio
    import requests
    import time
    start = time.time()
    async def get(url):
        return requests.get(url)
    async def request():
        url = 'https://www.baidu.com'
        print('Waiting for', url)
        response = await 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('Cost time:', end - start)
    
    Waiting for https://www.baidu.com
    Get response from https://www.baidu.com Result 200
    Waiting for https://www.baidu.com
    Get response from https://www.baidu.com Result 200
    Waiting for https://www.baidu.com
    Get response from https://www.baidu.com Result 200
    Waiting for https://www.baidu.com
    Get response from https://www.baidu.com Result 200
    Waiting for https://www.baidu.com
    Get response from http
    
    

    5、使用aiohttp

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

    在这里我们将请求库由 requests 改成了 aiohttp,通过 aiohttp 的 ClientSession 类的 get() 方法进行请求

    import asyncio
    import aiohttp
    import time
    start = time.time()
    async def get(url):
        session = aiohttp.ClientSession()
        response = await session.get(url)
        result = await response.text()
        await session.close()
        return result
    async def request():
        url = 'http://www.newsmth.net/nForum/#!mainpage'
        print('Waiting for', url)
        result = await get(url)
        print('Get response from', url, 'Result:', result)
    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)
    
    

    代码里面我们使用了 await,后面跟了 get() 方法,在执行这五个协程的时候,如果遇到了 await,那么就会将当前协程挂起,转而去执行其他的协程,直到其他的协程也挂起或执行完毕,再进行下一个协程的执行。

    开始运行时,时间循环会运行第一个 task,针对第一个 task 来说,当执行到第一个 await 跟着的 get() 方法时,它被挂起,但这个 get() 方法第一步的执行是非阻塞的,挂起之后立马被唤醒,所以立即又进入执行,创建了 ClientSession 对象,接着遇到了第二个 await,调用了 session.get() 请求方法,然后就被挂起了,由于请求需要耗时很久,所以一直没有被唤醒,好第一个 task 被挂起了,那接下来该怎么办呢?事件循环会寻找当前未被挂起的协程继续执行,于是就转而执行第二个 task 了,也是一样的流程操作,直到执行了第五个 task 的 session.get() 方法之后,全部的 task 都被挂起了。所有 task 都已经处于挂起状态,那咋办?只好等待了。3 秒之后,几个请求几乎同时都有了响应,然后几个 task 也被唤醒接着执行,输出请求结果,最后耗时,3 秒!

    怎么样?这就是异步操作的便捷之处,当遇到阻塞式操作时,任务被挂起,程序接着去执行其他的任务,而不是傻傻地等着,这样可以充分利用 CPU 时间,而不必把时间浪费在等待 IO 上。

  • 相关阅读:
    半夜删你代码队 Day6冲刺
    半夜删你代码队 Day5冲刺
    半夜删你代码队 Day4冲刺
    半夜删你代码队 Day3冲刺
    Scrum 冲刺博客集合
    团队项目6——复审与事后分析
    Alpha阶段项目复审
    事后Postmortem会议
    团队作业5-测试与发布
    Scrum 冲刺第七天
  • 原文地址:https://www.cnblogs.com/kai-/p/11974584.html
Copyright © 2011-2022 走看看