12--协程

一 协程概念

# 1 协程
又称微线程(纤程),是一种用户态的轻量级线程

# 2 子程序
在所有的语言中都是层级调用的
比如A中调用B,B在执行过程中调用C
C执行完返回,B执行完返回,最后是A执行完毕

这是通过栈实现的,一个函数就是一个执行的子程序
子程序的调用总是有一个入口、一次返回,调用的顺序是明确的


# 3 理解协程
线程是系统级别的,由操作系统调度
协程是程序级别,由程序员根据需求自己调度

协程就是 把一个线程中的一个个函数称为子程序,那么一个子程序在执行的过程中,可以被中断,再去执行别的子程序

也就是说同一个线程下的一段代码1执行执行着就中断,然后去执行另一段代码2
当再次回来执行代码1时,接着从之前的中断的位置继续向下执行


# 4 优点
1.最大的优势就是协程极高的执行效率
  因为子程序切换不是线程切换,而是由程序自身控制
  因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
    
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突
  在协程中,控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多


# 5 缺点
1.无法利用多核CPU,协程的本质是单个线程
  它不能同时将多个CPU的多个核心使用上,失去了标准线程使用多CPU的能力

2.进行阻塞操作(操作IO),会阻塞整个程序

二 同步与异步

2.1 概念

# 0 前言
python由于Cpython解释器的GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病
然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率

IO密集型就是磁盘的读取数据和输出数据非常大的时候就是属于IO密集型
由于IO操作的运行时间远远大于cpu、内存运行时间
所以任务的大部分时间都是在等待IO操作完成
IO的特点是cpu消耗小,所以,IO任务越多,cpu效率越高,当然不是越多越好,有一个极限值。


# 1 同步
指完成事务的逻辑
先执行第一个事务,如果阻塞了,会一直等待
直到这个事务完成,再执行第二个事务,顺序执行

# 2 异步
是和同步相对的,
异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果
直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果

2.2 代码

  • 同步

    import time
    
    def run(index):
        print("lucky is a good man", index)
        time.sleep(2)
        print("lucky is a nice man", index)
    
    for i in range(1, 5):
        run(i)
    
  • 异步

    说明:后面的课程中会使用到asyncio模块,现在的目的是使同学们理解异步思想

    import time
    import asyncio
    
    
    async def run(i):
        print("lucky is a good man", i)
        # 模拟一个耗时IO
        await asyncio.sleep(2)
        print("lucky is a nice man", i)
    
    
    if __name__ == "__main__":
        loop = asyncio.get_event_loop()
        tasks = []
        t1 = time.time()
    
        for url in range(1, 5):
            coroutine = run(url)
            task = asyncio.ensure_future(coroutine)
            tasks.append(task)
        loop.run_until_complete(asyncio.wait(tasks))
        t2 = time.time()
        print("总耗时:%.2f" % (t2 - t1))
    

三 asyncio 模块

3.1 概述

# 1 asyncio模块
  是python3.4版本引入的标准库,直接内置了对异步IO的操作

# 2 编程模式
  是一个消息循环,我们从asyncio模块中直接获取一个EventLoop的引用
  然后把需要执行的协程,扔到EventLoop中执行,就实现了异步IO

# 3 说明
  到目前为止实现协程的不仅仅只有asyncio,tornado和gevent都实现了类似功能

    
# 4 关键字的说明
关键字        说明 
event_loop  : 事件(消息)循环,程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数

coroutine   : 协程对象,指的是 调用一个使用async关键字定义的函数
               该函数的调用不会立即执行函数,而是会返回一个协程对象
               协程对象需要注册到事件循环,由事件循环调用,才会执行

task        : 任务 
              一个协程对象就是一个原生可以挂起的函数
              任务则是对协程进一步封装,其中包含了任务的各种状态

future      : 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别

async/await : python3.5用于定义协程的关键字
               async 定义一个协程函数,协程函数里必须有 通过关键字 await 表明的阻塞时的代码
               await 用于挂起 阻塞的异步调用接口  
               # 就是若某个接口方法是用 async 定义的,那么该接口方法调用时,就需要 用 await 挂起
            
# 通俗来讲:
用await标识的代码,人为的知道要发生IO操作,cpu执行会阻塞
所以让cpu切换到,其他任务或协程对象执行
                    
                    
# await 可等待对象  
使用await可以针对耗时操作进行挂起,就与生成器的yield一样
即让出当前的执行权,直到可等待对象有结果返回时,再重新获得可以被继续执行的权利

协程遇到await,消息循环会挂起该协程,执行别的协程,直到其他协程也会挂起或者执行完毕,在进行下一次执行


# 可等待对象 
协程(Coroutine)、任务(Task)、期货(Future)

eg:time.sleep() 模拟阻塞耗时操作,就不能用 await 挂起  因为time.sleep()返回的不是一个可等待对象
需要换成 await asyncio.sleep(n)  

3.2 基本使用

  • 定义一个协程

    import asyncio
    
    
    # 通过async关键字定义了一个协程,协程是不能直接运行的,需要将协程放到消息循环中
    async def run(x):
        print("waiting:%d"%x)
        await asyncio.sleep(x)  # 各个协程对象中的 阻塞等待,也需要用异步模块中的sleep()
        print("结束run")
    
        
    if __name__ == "__main__":
        # 1.得到一个协程对象
        coroutine = run(2)
    
        # 2.创建一个事件(消息)循环
        loop = asyncio.get_event_loop()
    
        # 3.将协程对象加入到消息循环,由消息循环来执行
        loop.run_until_complete(coroutine)
    
    
        ### 2、3步 或者使用 asyncio.run(协程对象)   ***
        asyncio.run(coroutine)   
        
        # 注意:
            asyncio.run(coroutine) 可能会出现问题 RuntimeError: Event loop is closed
            因为Windows系统 底层中,消息(事件)循环处理完 协程任务后,会自动关闭
            而asyncio.run() 源代码中,本来就是 调用2/3步的代码,再 loop.close()  重复关闭了,就报错
    
    
    ### asyncio.run() 函数  ***
    接收一个协程对象  在执行时,总会创建一个新的事件循环,并在结束后关闭循环
    
    理想情况下: run()函数应当被作为程序的总入口,并且只会被调用一次
    如果同一线程中还有其它事件循环在运行,则此方法不能被调用
    
    
    # eg:
    async def main():  # 主入口函数中,创建协程 或 任务等  上述1-3步
        await run(2)   # 当前挂起阻塞, 就会切换到协程run 执行
    
    if __name__ == "__main__":
        asyncio.run(main())
    
  • 获取返回值(通过:添加回调函数)

    import time
    import asyncio
    
    async def run(url):
        print("开始向'%s'要数据……"%(url))
        # 向百度要数据,网络IO
        await asyncio.sleep(5)
        
        data = "'%s'的数据"%(url)
        print("给你数据")
        return data
    
    # 定义一个回调函数
    def call_back(future):   # future指的是运行中 或者 运行结束的 协程(任务)对象
        print("call_back:", future.result())
    
    # async关键词定义后的函数,调用 返回的是协程对象
    coroutine = run("百度")  
    
    # 创建一个任务对象
    task = asyncio.ensure_future(coroutine)
    
    # 给任务添加回调函数地址,在任务结束后,会调用该回调函数
    task.add_done_callback(call_back)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(task)
    

3.3 Task 概念及用法

  • Task 概念
# 1 Task 任务
是python中与事件循环进行交互的一种主要方式

# 2 创建 Task
意思就是把协程封装成 Task实例,并追踪协程的 运行/完成状态,用于未来获取协程的结果

# 3 Task 的核心作用: 在事件循环中,添加多个并发任务
具体来说,是通过 asyncio.create_task(coroutine) 创建Task,再将任务加入事件循环中,等待被调度执行

# 注意:
asyncio.create_task()  # Python3.7以后的版本支持 
loop.create_task()  # 在此之前的写法


# 4 任务的执行
单任务:
协程封装为Task后,创建的任务立即被加入到事件循环中,并不会阻塞当前的程序
当主协程(创建该任务的协程) await Task 时,主协程阻塞,消息循环才切换到该任务协程,该任务才会被执行


多任务:
当多个Task被加入一个 task_list 的时候,添加Task的过程中 Task不会执行
必须要用 await asyncio.wait(task_list) 或 await asyncio.gather(task_list) 将 Task对象加入事件循环中,异步执行


#  5 一般在开发中,常用的写法是这样的:
1.先创建 task_list 空列表;
2.然后用 asyncio.create_task() 创建 Task
3.再把 Task 对象加入 task_list
4.最后使用 await asyncio.wait(task_list) 或 await asyncio.gather(*task_list)
  将Task对象加入事件循环中异步执行

# 注意:
创建 Task 对象时,除了可以使用 asyncio.create_task() 之外,
还可以用最低层级的 loop.create_task() 或 asyncio.ensure_future() 
  • 创建单个任务的三种方式
import asyncio

async def run(x):
    print("waiting:%d"%x)
    await asyncio.sleep(x)
    print("结束run")


### 创建任务 方式一: asyncio.ensure_future()
# 创建协程对象
coroutine = run(2)

# 创建任务
task = asyncio.ensure_future(coroutine)

# 创建事件(消息)循环
loop = asyncio.get_event_loop()
# 将任务加入到消息循环,并执行
loop.run_until_complete(task)


### 创建任务 方式二: loop.create_task()  # 需要单独 先注册实例化 loop 消息循环
coroutine = run(2)

loop = asyncio.get_event_loop()

task = loop.create_task(coroutine)

loop.run_until_complete(task)



### 创建任务 方式三: asyncio.create_task()    ***
async def main():
    coroutine = run(2)
    task1 = asyncio.create_task(coroutine)
    await task1

asyncio.run(main())
  • Task 简单用法:单个任务
import asyncio

async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return "test"


async def main():
    print("main start")

    # python 3.7及以上版本的写法
    task1 = asyncio.create_task(func())

    # python3.7以前的写法
    # task2 = asyncio.ensure_future(func())
    print("main end")

    ret1 = await task1  # 执行该任务
    # ret1 = await task2

    print(ret1)


# python3.7以后的写法
asyncio.run(main())

# python3.7以前的写法
# loop = asyncio.get_event_loop()
# loop.run_until_complete(main())


# 原理解读:为啥把创建任务/协程的函数  也封装成异步函数?
asyncio.run(main()) 或 loop 消息循环的方式,实质只是把 main() 主函数添加到事件循环,并异步执行
在主函数执行过程中,依次创建了 func的协程对象和任务,但此时,只是创建任务,并立即添加到事件循环中,但没有执行
再通过  await 任务,表明主函数的这个协程 阻塞了,切换到其他协程/任务 执行
消息循环中,也就只有func的任务协程 执行
  • Task 用法实例:多个任务
import asyncio
import arrow

def current_time():
    '''
    获取当前时间
    '''
    cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')
    return cur_time


async def func(sleep_time):
    func_name_suffix = sleep_time # 使用 sleep_time (函数 I/O 等待时长)作为函数名后缀,以区分任务对象
    print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}")
    await asyncio.sleep(sleep_time)
    print(f"[{current_time()}]函数{func.__name__}-{func_name_suffix} 执行完毕")
    return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】"

async def run():
    task_list = []
    for i in range(5):
        task = asyncio.create_task(func(i))
        task_list.append(task)
        
    done, pending = await asyncio.wait(task_list)
    for done_task in done:
        print((f"[{current_time()}]得到执行结果 {done_task.result()}"))
        
        
if __name__ == '__main__':
    asyncio.run(run())

3.4 协程嵌套与返回值

### 协程嵌套
使用async可以定义协程,协程用于耗时的io操作,也可以封装更多的io操作过程,这样就实现了嵌套的协程
即一个协程中await了另外一个协程,如此连接起来


### 协程的返回值
# 方式1:通过添加回调函数的方式,异步的 自动打印结果   见下面 3.5 多任务案例

# 方式2:通过 多任务的执行方法 不同,依次循环打印任务的结果
done, pending = asyncio.wait(task_list)

results = asyncio.gather(*task_list)      gather  v.收集

# asyncio.wait(tasks)
将多个任务的列表,又变成 被等待执行的 协程对象 <coroutine object wait at 0x7f80f43408c0>
  • asyncio.waitasyncio.gather的异同

    1. 异同点综述
    ### 相同:
    从功能上看, asyncio.wait 和 asyncio.gather 实现的效果是相同的
    都是把所有 Task 任务结果收集起来
    
    ### 不同: 
    # 1 方法参数不同
    asyncio.wait(task_list)    # 参数是任务对象的列表
    syncio.gather(*task_list)  # 参数是任务对象,用*列表 打散传递的
    
    # 2 返回值的不同
    asyncio.wait 会返回两个值: done 和 pending  n.待定
      done    :已完成的协程Task列表
      pending :超时未完成的协程Task列表
      # 需通过 future.result(),获得Task的result
    
    
    syncio.gather 返回的是所有已完成 Task的result
      不需要再进行调用或其他操作,就可以得到全部结果
    
    1. asyncio.wait 用法

    最常见的写法是: await asyncio.wait(task_list)

    import asyncio
    import arrow
    
    def current_time():
        '''
        获取当前时间
         '''
        cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')
        return cur_time
    
    async def func(sleep_time):
        func_name_suffix = sleep_time # 使用 sleep_time (函数 I/O 等待时长)作为函数名后缀,以区分任务对象
        print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}")
        await asyncio.sleep(sleep_time)
        print(f"[{current_time()}]函数{func.__name__}-{func_name_suffix} 执行完毕")
        return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】"
    
    async def main():
        task_list = []
        for i in range(5):
            task = asyncio.create_task(func(i))
            task_list.append(task)
    
        done, pending = await asyncio.wait(task_list)
        for done_task in done:
            print((f"[{current_time()}]得到执行结果 {done_task.result()}"))
    
    if __name__ == '__main__':
        asyncio.run(main())
        
        
        
    ### 外部获取结果
    async def main():
        task_list = []
        for i in range(5):
            task = asyncio.create_task(func(i))
            task_list.append(task)
            
        return await asyncio.wait(task_list)
    
    
    if __name__ == '__main__':
        done, pending = asyncio.run(main())
        for done_task in done:
            print((f"[{current_time()}]得到执行结果 {done_task.result()}"))
    
    1. asyncio.gather 用法

    最常见的用法是: await asyncio.gather(*task_list) ,注意这里 task_list 前面有一个 *

    async def main():
        task_list = []
        for i in range(5):
            task = asyncio.create_task(func(i))
            task_list.append(task)
    
        results = await asyncio.gather(*task_list)
        for result in results:
            print((f"[{current_time()}]得到执行结果 {result}"))
    

3.5 多任务案例

  • 同步

    同时请求"百度", "阿里", "腾讯", "新浪"四个网站,假设响应时长均为2秒

    import time
    
    def run(url):
        print("开始向'%s'要数据……"%(url))
        # 向百度要数据,网络IO
        time.sleep(2)
        data = "'%s'的数据"%(url)
        return data
    
    if __name__ == "__main__":
        t1 = time.time()
        for url in ["百度", "阿里", "腾讯", "新浪"]:
            print(run(url))
        t2 = time.time()
        print("总耗时:%.2f"%(t2-t1))  # 大约8秒多
    
  • 异步

    同时请求"百度", "阿里", "腾讯", "新浪"四个网站,假设响应时长均为2秒

    • 使用asyncio.ensure_future创建多任务
    import time
    import asyncio
    
    async def run(url):
        print("开始向'%s'要数据……"%(url))
        await asyncio.sleep(2)
        data = "'%s'的数据"%(url)
        return data
    
    def call_back(future):
        print("call_back:", future.result())
    
        
    if __name__ == "__main__":
        tasks = []
        t1 = time.time()
        
        for url in ["百度", "阿里", "腾讯", "新浪"]:
            coroutine = run(url)
            task = asyncio.ensure_future(coroutine)
            task.add_done_callback(call_back)
            tasks.append(task)
            
        # 同时添加4个异步任务
        # asyncio.wait(tasks) 将多个任务的列表,又变成 被等待执行的 协程对象
        # <coroutine object wait at 0x7f80f43408c0>
        
        loop = asyncio.get_event_loop()
        loop.run_until_complete(asyncio.wait(tasks))
    
        t2 = time.time()
        print("总耗时:%.2f" % (t2 - t1))
    
    • 主入口函数,封装成异步函数
    import time
    import asyncio
    
    
    async def run(url):
        print("开始向'%s'要数据……" % (url))
        await asyncio.sleep(2)
        data = "'%s'的数据" % (url)
        return data
    
    
    def call_back(future):
        print("call_back:", future.result())
    
    
    async def main():
        tasks = []
        t1 = time.time()
    
        for url in ["百度", "阿里", "腾讯", "新浪"]:
            coroutine = run(url)
            task = asyncio.ensure_future(coroutine)
            task.add_done_callback(call_back)
            tasks.append(task)
    
        # 同时添加4个异步任务
        await asyncio.wait(tasks)
        t2 = time.time()
        print("总耗时:%.2f" % (t2 - t1))
    
    if __name__ == "__main__":
        # asyncio.run(main())
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
    
    • 使用loop.create_task创建多任务
    async def main():
        tasks = []
        t1 = time.time()
        for url in ["百度", "阿里", "腾讯", "新浪"]:
            coroutine = run(url)
            task = loop.create_task(coroutine)
            task.add_done_callback(call_back)
            tasks.append(task)
        # 同时添加4个异步任务
        await asyncio.wait(tasks)
        t2 = time.time()
        print("总耗时:%.2f" % (t2 - t1))
    
    if __name__ == "__main__":
        loop = asyncio.get_event_loop()  # 就必须 单独注册 loop事件循环 
        loop.run_until_complete(main())
    
    • 使用asyncio.create_task创建多任务 ***
    async def main():
        tasks = []
        t1 = time.time()
        for url in ["百度", "阿里", "腾讯", "新浪"]:
            coroutine = run(url)
            task = asyncio.create_task(coroutine)
            task.add_done_callback(call_back)
            tasks.append(task)
            
        # 同时添加4个异步任务
        await asyncio.wait(tasks)
        t2 = time.time()
        print("总耗时:%.2f" % (t2 - t1))
    

四 aiohttp 网络请求模块

4.1 安装与使用

# 1 安装
pip install aiohttp 

# 2 简单实例使用
aiohttp,是异步的网络请求,包含了客户端和服务器端,客户端和服务器端的简单实例代码


# 客户端
import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()


async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, "http://httpbin.org/headers")
        print(html)

asyncio.run(main())


"""输出结果:
{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "Python/3.7 aiohttp/3.6.2"
  }
}
"""

4.2 ClientSession() HTTP请求接口类

简单示范

首先是学习客户端,也就是用来发送http请求的用法。首先看一段代码,会在代码中讲述需要注意的地方:

import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://httpbin.org/get') as response:
            print(response.status)

asyncio.run(main())

代码解释:

class ClientSession:
    """First-class interface for making HTTP requests."""
    
# 在源码中
在网络请求中,一个请求就是一个会话
aiohttp使用的是ClientSession类来管理会话
该类的源码注释是使用HTTP请求接口的第一个类


上面的代码就是实例化一个ClientSession类,并命名为session
然后用session去发送请求


# 注意:  这里有一个坑
ClientSession.get() 协程的必需参数,只能是 str 类和 yarl.URL 的实例

当然这只是get请求,其他的请求都是支持的:

session.post('http://httpbin.org/post', data='data')

session.get('http://httpbin.org/get')

4.3 在URL中传递参数

通过params参数来指定要传递的参数:字典

import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        params = {'key1': 'value1', 'key2': 'value2'}
        async with session.get('http://httpbin.org/get', params=params) as response:
            print(response.url)
            
asyncio.run(main())

若是一个键对应多个值的参数,那么MultiDict就在这个时候起作用了

可以传递多个元祖列表,来作为参数

import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        params = [('key', 'value1'), ('key', 'value2')]

        async with session.get('http://httpbin.org/get', params=params) as response:
            expect = 'http://httpbin.org/get?key=value2&key=value1'
            # assert str(resp.url) == expect
            print(response.url)
            
asyncio.run(main())

4.4 读取响应内容

  • 通过status来获取响应状态码,text()来获取到响应内容,当然也可以指定编码格式
# 返回对象.text() 是个协程对象,需要await 阻塞挂起

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://httpbin.org/get') as response:
            print(response.status)
            print(await response.json())  # 响应内容 为json格式
            print(await response.text(encoding=utf-8))
            
"""输出结果:
200
<!doctype html>
<html lang="zh-CN">
<head>
......

"""


###  判断异步模块的接口方法,是需要需要 await挂起 的依据
异步模块的接口方法是用 async 定义的
  • 通过read()来获取二进制格式响应内容
await response.read()

4.5 请求的自定义

ClientResponse 客户端响应对象含有request_info(请求信息),主要是 url 和 headers 信息

raise_for_status 结构体上的信息会被复制给 ClientResponseError 实例

4.5.1 自定义Headers

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}

await session.post(url, headers=headers)

4.5.2 ssl验证失败的处理

import aiohttp
import asyncio
from aiohttp import TCPConnector


async def main():
    # 注意:是在ClientSession类 中添加参数    或者 使用 request中那个参数 verify=False
    async with aiohttp.ClientSession(connector=TCPConnector(ssl=False)) as session:
        pass
    
asyncio.run(main())

4.5.3 自定义cookie

ClientSession类,指定cookies参数

url = 'http://httpbin.org/cookies'
cookies = {'cookies_are': 'working'}

# 注意:是在ClientSession类 中添加参数
async with ClientSession(cookies=cookies) as session:
    async with session.get(url) as resp:
        assert await resp.json() == {"cookies": {"cookies_are": "working"}}

4.5.4 使用代理

ClientSession类实例化后的,session添加proxy参数

但它只支持http代理,不支持HTTPS代理

proxy = "http://127.0.0.1:10809

async with aiohttp.ClientSession(headers=headers) as session:
  async with session.get(url=login_url, proxy=proxy) as response:
    resu = await response.text()

五 aiofiles 文件读写模块

5.1 概述

# 前言
平常使用的file操作模式为同步,并且为线程阻塞
当程序I/O并发次数高的时候,CPU被阻塞,形成闲置

# 解决
# 1.用线程(Thread)方式来解决
硬盘缓存可以被多个线程访问,因此通过不同线程访问文件可以部分解决
但此方案涉及线程开启关闭的开销,而且不同线程间数据交互比较麻烦

from threading import Thread
for file in list_file:
     tr = Thread(target=file.write, args=(data,))
     tr.start()
    
    
# 2.使用已编写好的第三方插件-aiofiles,使用线程池实现异步
使用aio插件来开启文件的非阻塞异步模式

5.2 安装及使用

# 安装
pip install aiofiles

# 使用:和python原生open一致,而且可以支持异步迭代
  • 打开文件
import asyncio
import aiofiles

async def main():
    async with aiofiles.open('first.m3u8', mode='r') as f:
        contents = await f.read()
        print(contents)

if __name__ == '__main__':
    asyncio.run(main())
  • 迭代
import asyncio
import aiofiles

async def main():
    async with aiofiles.open('filename') as f:
        async for line in f:  
            print(line)

if __name__ == '__main__':
    asyncio.run(main())

六 协程的并发控制

6.1 信号量

semaphore 信号量:来控制开启协程的并发数

semaphore = asyncio.Semaphore(10) 

6.2 实例:协程抓取鬼吹灯小说

#!/usr/bin/python

import asyncio
import os
import aiofiles
import aiohttp
import requests
from bs4 import BeautifulSoup


def get_page_source(web):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36'
    }
    response = requests.get(web, headers=headers)
    response.encoding = 'utf-8'
    return response.text


def parse_page_source(html):
    book_list = []
    soup = BeautifulSoup(html, 'html.parser')
    a_list = soup.find_all('div', attrs={'class': 'mulu-list quanji'})
    for a in a_list:
        a_list = a.find_all('a')
        for href in a_list:
            chapter_url = href['href']
            book_list.append(chapter_url)
    return book_list


def get_book_name(book_page):
    book_number = book_page.split('/')[-1].split('.')[0]
    book_chapter_name = book_page.split('/')[-2]
    return book_number, book_chapter_name


async def aio_download_one(chapter_url, signal):
    number, c_name = get_book_name(chapter_url)
    for c in range(10):  # 若请求异常,循环10次请求,一旦成功,就函数返回了!
        try:
            async with signal:
                async with aiohttp.ClientSession() as session:
                    async with session.get(chapter_url) as resp:
                        page_source = await resp.text()
                        soup = BeautifulSoup(page_source, 'html.parser')
                        chapter_name = soup.find('h1').text
                        p_content = soup.find('div', attrs={'class': 'neirong'}).find_all('p')
                        content = [p.text + '\n' for p in p_content]
                        chapter_content = '\n'.join(content)
                        if not os.path.exists(f'{book_name}/{c_name}'):
                            os.makedirs(f'{book_name}/{c_name}')
                        async with aiofiles.open(f'{book_name}/{c_name}/{number}_{chapter_name}.txt', mode="w",
                                                 encoding='utf-8') as f:
                            await f.write(chapter_content)
                        print(chapter_url, "下载完毕!")
                        return ""
        except Exception as e:
            print(e)
            print(chapter_url, "下载失败!, 重新下载. ")
    return chapter_url


async def aio_download(url_list):
    tasks = []
    semaphore = asyncio.Semaphore(10)
    for h in url_list:
        tasks.append(asyncio.create_task(aio_download_one(h, semaphore)))
    await asyncio.wait(tasks)


if __name__ == '__main__':
    url = 'https://www.51shucheng.net/daomu/guichuideng'
    book_name = '鬼吹灯'
    if not os.path.exists(book_name):
        os.makedirs(book_name)
    source = get_page_source(url)
    href_list = parse_page_source(source)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(aio_download(href_list))
    loop.close()
posted @ 2024-01-08 23:06  Edmond辉仔  阅读(6)  评论(0编辑  收藏  举报