asyncio协程

说明1
正常的程序都是从上到下依次执行的,如果遇到了要等待的地方,就会阻塞,等待相应的代码执行完毕后,再往下执行。

说明2
协程(Coroutine) 是一种特殊的函数,它可以在执行过程中暂停(挂起),并在稍后恢复执行。协程是异步编程的核心概念,允许程序在等待 I/O 操作(如网络请求、文件读写)时挂起当前任务,转而执行其他任务,从而提高程序的并发性能。

说人话
使用协程异步处理程序就和做家务差不多,在炖汤的时候,使用洗衣机洗衣服,再去洗碗,洗碗的时候抽出时间看看汤炖的咋样了,就会发现,事情都是同时进行的,而不使用异步的方法,就会发生一件很呆的事情:你炖汤的时候在那等着,等汤炖好再去洗衣服,洗完衣服再去洗碗。

使用协程重要的几个函数

  1. import asyncio
    首先要下载并导入asyncio这个库,这是协程的库

  2. async
    对函数进行异步标识,我们可以把一个个的函数类比为一项项家务,在函数前面使用async标识,那么通过await函数(家务)就是异步的,就可以并发的执行,一个函数在运行的时候,不用等待该函数(家务)做完,而是可以直接抽空去执行其他的函数。

  3. asyncio.create_task()
    上面对函数标识后还不够,还需要使用asyncio.create_task()将函数转换为task任务,这个后面具体的代码再解释

  4. await
    上面的async和asyncio.create_task()标识函数并把函数转换为task任务后,要想实现异步还需要将这个任务挂起来才能真正实现异步,具体操作看后面的案例。

  5. asyncio.gather()、asyncio.wait()
    与await一起实现将多个task任务一次挂起

  6. asyncio.run()
    运行统领所有任务的主函数,也就是你在哪个函数上面创建了异步任务就把相应的函数作为参数交给run()处理

举例

import asyncio
import aiohttp
import aiofiles
import time

async def wash_glass():
    print("开始洗碗辣!")
    #假设洗碗要花费2秒,这里await就是为了将等待的时间加入到事件循环中
    # ,当要等待的时候,将其挂起来,去执行其他的函数(家务)。
    await asyncio.sleep(2)
    print("洗完碗辣!")
async def wash_clothes():
    print("开始洗衣服辣!")
    await asyncio.sleep(4)#假设洗衣服要花费4秒
    print("洗完衣服辣!")

async def cooking():
    print("开始做菜辣!")
    await asyncio.sleep(5)#假设做菜要花费5秒
    print("做完菜辣!")

#main()函数作用是统领家务,在main()函数这个分支下,各种家务分支并行处理,当家务都做完后,再运行后续的代码
async def main():
    list_task=[]#存放task任务
    #使用create_task创建task
    list_task.append(asyncio.create_task(wash_clothes()))
    list_task.append(asyncio.create_task(wash_glass()))
    list_task.append(asyncio.create_task(cooking()))
    #最后将任务挂起来
    # await asyncio.gather(*list_task)#使用gather的方法将任务集中挂起
    await asyncio.wait(list_task)#使用wait的方法,将任务集中挂起
    print("家务都做完了")#所有的异步程序(家务)都完成后,才会执行这条语句


if __name__ == '__main__':
    start=time.time()
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())#不加可能会报错,因为与windows系统冲突
    asyncio.run(main())#执行统领家务的主函数
    end=time.time()
    print(f"总共耗时{end-start}秒")

配合这个图来理解吧:

小总结

  1. async主要用于标识函数
  2. await很重要,你觉得哪个任务耗时间,就要在前面使用await将其挂起
  3. create_task()用于创建任务
  4. gather()或者wait()与await配合使用,便于再main()函数,集中将多个任务挂起
  5. asyncio.run()运行主函数

使用异步方法爬取Microsoft Bing图片

说明:之前写了一个爬取Microsoft Bing图片的爬虫案例,这里我将其改为异步的方式,与之前的进行对比。

#main.py
from requests import get
import time
import os
from download_img import download
import asyncio
# import aiohttp
# import aiofiles
def download_html(search_len,search_name,url,headers,params):
    for i in range(int(search_len)):
        response=get(url,headers=headers,params=params)
        image_html=response.text
        with open(f'./{search_name}_html/{search_name}'+f'_{i}'+'.html','w',encoding='utf-8') as fp:
            fp.write(image_html)
        params['first']+=params['count']
        params['SFX']+=1
        params['count']=35
async def main(search_name=None,search_len=None):
    url='https://cn.bing.com/images/async'
    headers={
    'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36 Edg/134.0.0.0'
           
     
            }
    params={
    'q':search_name,
    'first': 13,
    'count': 12,#爬取图片的数量
    'cw': 437,
    'ch': 603,
    'relp': 12,
    'datsrc': 'I',
    'layout': 'ColumnBased_Landscape',
    'apc': 0,
    'imgbf': 'DfCtqwgAAACQAQAAAAAAAAAAAAAFAAAAsUuBIONfTNlAgBAASAEAQgAQIFEABiAGIBgAgNDAoQAIUAAIgUYIAAiAQCAAABIiIBgIIAAAACkCAAAAAAAAAA==',
    'mmasync': 2,
    'dgState': 'c*2_y*725s715_i*13_w*204',
    'IG': '32F3E4B3953D4FFCB5B4E5EDB527C8EC',
    'SFX': 2,#页数
    'iid': 'images.5306',
            }

    if not os.path.exists(f'./{search_name}_html'):
        os.mkdir(f'./{search_name}_html')
    download_html(search_len,search_name,url,headers,params)
    task_list=[]
    for html in os.listdir(f'./{search_name}_html'):
        task_list.append(asyncio.create_task(download(html,search_name)))
    await asyncio.gather(*task_list)
    
if __name__ == '__main__':
    search_name=input('请输入搜索内容:')
    search_len=input('请输入要爬取图片页数:')
    s=time.time()
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())#解决asyncio.run()报错
    asyncio.run(main(search_name,search_len))
    print(f"总共花费:{time.time()-s}秒")
#download_img.py
from lxml import etree
from requests import get
# import os
from os.path import exists
from os import mkdir
import re
import aiofiles
import asyncio
import aiohttp
async def download_url(url,headers,dir_name,p):
    async with aiohttp.ClientSession() as session:
        async with session.get(url,headers=headers) as response:
            response=await response.read()
        
    async with aiofiles.open(f'./{dir_name}_image_dir/'+p.search(url).group('name')+'.jpg','wb') as f:
        await f.write(response)
async def download(html_name,dir_name):
    headers={
        'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36 Edg/134.0.0.0',
    
    }
    # with open(f'./{dir_name}_html/{html_name}',encoding='utf-8') as f:
    #     text=f.read()
    async with aiofiles.open(f'./{dir_name}_html/{html_name}',encoding='utf-8') as f:
        text=await f.read()
    tree=etree.HTML(text)
    img_src_list=tree.xpath('//img/@data-src')
    p=re.compile(r'https.*/OIP-C\.(?P<name>.*)\?.*')
    if not exists(f'./{dir_name}_image_dir'):
        mkdir(f'./{dir_name}_image_dir')
    task_url=[]
    for url in img_src_list:
        task_url.append(asyncio.create_task(download_url(url,headers,dir_name,p)))
        await asyncio.gather(*task_url)  

asyncio.Queue(maxsize=)

你会发现加入队列后程序就会复杂一点,要考虑数据安全方面的问题,这里稍微解释一下

  • 队列常用函数
    1、queue=asyncio.Queue(maxsize=):
    创建队列通道(想象排队,先进先出),其中maxsize决定了,这个队列里面最多一次能存放几个数据
    2、await queue.put(data):
    提交数据data到队列中去
    3、await queue.get():
    获取数据,注意了,如果一个异步任务中有get(),最后尽量要将这个任务使用cancel()函数删除,并且如果get()获得的是None,那么消费者也会主动退出
    4、queue.task_done():一般出现在有获取get()的任务函数中,通知队列一个任务已经完成。它的主要用途是与 queue.join() 配合,用于等待队列中的所有任务完成。
    5、await queue.join()等待队列中所有的数据被处理,通常是放在async main()函数中的后几行,保证所有的需要队列中的异步任务处理完以后,再终结主程序

案例:这是再deepseek中生成的案例,讲的很好,我进行了稍加改进,让代码不再死循环

import asyncio
import random

# 生产者:餐馆做菜
async def restaurant(queue, num_dishes, num_customers):
    for dish_id in range(num_dishes):
        cooking_time = random.uniform(0.1, 0.5)  # 随机做菜时间
        await asyncio.sleep(cooking_time)
        dish = f"🍲菜品_{dish_id + 1}"
        await queue.put(dish)
        print(f"餐馆制作了 {dish}(队列长度:{queue.qsize()})")

    # 向队列发送终止信号(每个顾客一个None),当顾客中queue.get()为None时,收到的顾客任务会被踢出循环,也就不再是异步了
    for _ in range(num_customers):
        await queue.put(None)

# 消费者:顾客吃菜
async def customer(queue, name):
    while True:
        dish = await queue.get()
        if dish is None:  # 收到终止信号
            print(f"{name} 离开了餐馆")
            queue.task_done()#这个代码要加上,如果不加上,那么代码会陷入死循环
            return

        eating_time = random.uniform(0.5, 1.5)  # 随机用餐时间
        print(f"{name} 开始吃 {dish}")
        await asyncio.sleep(eating_time)
        print(f"{name} 吃完了 {dish}(耗时:{eating_time:.1f}s)")
        queue.task_done()

async def main():
    queue = asyncio.Queue(maxsize=3)  # 设置队列容量
    num_customers = 3
    num_dishes = 5

    # 创建消费者任务
    consumers = [
        asyncio.create_task(customer(queue, f"👤顾客_{i+1}"))
        for i in range(num_customers)
    ]

    # 创建生产者任务
    producer = asyncio.create_task(restaurant(queue, num_dishes, num_customers))

    # 等待所有任务完成
    await asyncio.gather(producer)
    await asyncio.gather(*consumers)
    await queue.join()  # 等待队列清空
    for i in consumers: #以防万一
        i.cancel()

if __name__ == "__main__":
    asyncio.run(main())
posted @ 2025-03-17 12:42  CodeCraftsMan  阅读(116)  评论(0)    收藏  举报