Python 协程实现生产者-消费者模型

本文介绍了Python协程的概念及其在爬虫中的应用,通过实例展示了如何使用asyncio库提升爬虫的并发性能。对比了同步和异步爬虫的运行时间,说明了协程在单线程上的任务调度优势,并提供了一个豆瓣电影推荐信息的异步爬虫例子,展示如何利用async/await和asyncio.create_task进行高效爬取。
🐛 从一个假设爬虫说起

先看一个简单的爬虫例子:

import time

def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    time.sleep(sleep_time)
    print('OK {}'.format(url))

def main(urls):
    for url in urls:
        crawl_page(url)

if __name__ == "__main__":
	s = time.time()
	main(['url_1', 'url_2', 'url_3', 'url_4'])
	print("Wall time:", time.time()-s, " s")

########## 输出 ##########

crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Wall time: 10 s

我们简化了爬虫函数 scrawl_page 为休眠数秒,休眠时间取决于 url 字符串最后的数字。

我们可以看到爬取 5 个页面一共用了 10 秒钟,这显然效率低下,于是我们自然想到并发化爬取操作。用 Python 协程只需简单改动:

import time
import asyncio

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    for url in urls:
        await crawl_page(url)

if __name__ == "__main__":
	s = time.time()
	asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
	print("Wall time:", time.time()-s, " s")

########## 输出 ##########

crawling url_1
OK url_1
crawling url_2
OK url_2
crawling url_3
OK url_3
crawling url_4
OK url_4
Wall time: 10 s  # 这。。。怎么还是10秒?

由于 python 3.7+ 优化了 async/await 语法,实现协程非常简单。

async 修饰词声明异步函数,于是,这里的 crawl_pagemain 都变成了异步函数。而调用异步函数,我们便可得到一个协程对象(coroutine object)。

await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。代码中 await asyncio.sleep(sleep_time) 会在这里休息若干秒,await crawl_page(url) 则会执行 crawl_page() 函数。

其次,我们可以通过 asyncio.create_task() 来创建任务,我们实现生产者-消费者模型时会用到。

最后,我们需要 asyncio.run 来触发运行。asyncio.run 这个函数是 Python 3.7 之后才有的特性,可以让 Python 的协程接口变得非常简单。

⌚️ 协程始于 Task

但是由于 await 是同步调用,因此,crawl_page(url) 在当前的调用结束之前,是不会触发下一次调用的。于是,这个代码效果就和上面完全一样了,相当于我们用异步接口写了个同步代码。。。

实现真正的异步需要用到协程中的一个重要概念,任务(Task)

import time
import asyncio

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    await asyncio.gather(*tasks)
    # for task in tasks: await task  # 上一句也可以换成这样

if __name__ == "__main__":
	s = time.time()
	asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
	print("Wall time:", time.time()-s, " s")


########## 输出 ##########

crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 4.01 s

有了协程对象后,便可以通过 asyncio.create_task() 来创建任务。任务创建后很快就会被调度执行,这样,我们的代码也不会阻塞在一个任务这里。所以,我们要 await asyncio.gather(*tasks) 来等所有任务都结束才行。

结果显示,运行总时长等于运行时间最长的爬虫。

协程就是在单个线程上执行多个任务(Task),所以 协程的开销 < 线程 < 进程,并且协程有一个巨大的优势,程序员可以通过 await 调用来掌握任务调度的主动权。

⏰ 解密协程运行时

理解下面两段代码很简单:

import time
import asyncio

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(1)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)
    print('worker_2 done')

async def main():
    print('before await')
    await worker_1()
    print('awaited worker_1')
    await worker_2()
    print('awaited worker_2')

if __name__ == "__main__":
	s = time.time()
	asyncio.run(main())
	print("Wall time:", time.time()-s, " s")

########## 输出 ##########

before await
worker_1 start
worker_1 done
awaited worker_1
worker_2 start
worker_2 done
awaited worker_2
Wall time: 3 s
import time
import asyncio

async def worker_1():
    print('worker_1 start')
    awai
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Skr.B

WUHOOO~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值