本文目录一览:
- python协程(4):asyncio
- [在Python中使用Asyncio系统(3-4)Task 和 Future](#在Python中使用Asyncio系统(3-4)Task 和 Future)
- 如何在scrapy框架下,用python实现爬虫自动跳转页面来抓去网页内容??
python协程(4):asyncio
asyncio
是官方提供的协程的类库,从Python 3.4开始支持该模块。
async
和await
是Python 3.5中引入的关键字。使用async
关键字可以将一个函数定义为协程函数,使用await
关键字可以在遇到IO的时候挂起当前协程(也就是任务),去执行其他协程。
await
后面可以跟可等待的对象(协程对象、Future对象、Task对象 - IO等待)。
注意:在Python 3.4中是通过asyncio
装饰器定义协程,在Python 3.8中已经移除了asyncio
装饰器。
事件循环可以看作是一个while循环,这个while循环在周期性地运行并执行一些协程(任务),在特定条件下终止循环。
loop = asyncio.get_event_loop()
:生成一个事件循环loop.run_until_complete(任务)
:将任务放到事件循环 Tasks用于并发调度协程,通过asyncio.create_task(协程对象)
的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用asyncio.create_task()
函数以外,还可以用低层级的loop.create_task()
或ensure_future()
函数。不建议手动实例化Task对象。 本质上是将协程对象封装成task对象,并将协程立即加入事件循环,同时追踪协程的状态。 注意:asyncio.create_task()
函数在Python 3.7中被加入。在Python 3.7之前,可以改用asyncio.ensure_future()
函数。 下面结合async
、await
、事件循环和Task看一个示例: 示例一: 注意:Python 3.7以后增加了asyncio.run(协程对象)
,效果等同于loop = asyncio.get_event_loop()
,loop.run_until_complete(协程对象)
示例二: 注意:asyncio.wait
源码内部会对列表中的每个协程执行ensure_future
从而封装为Task对象,所以在和wait
配合使用时task_list
的值为[func(), func()]
也是可以的。 示例三:
在Python中使用Asyncio系统(3-4)Task 和 Future
Task 和 Future
前面我们讨论了协程,以及如何在循环中运行它们才有用。现在我想简单谈谈Task和Future API。你将使用最多的是Task,因为你的大部分工作将涉及使用create_task()
函数运行协程,就像在第22页的“快速开始”中设置的那样。Future类实际上是Task的超类,它提供了与循环交互操作的所有功能。
可以这样简单地理解:Future表示某个活动的未来完成状态,并由循环管理。Task是完全相同的,但是具体的“activity”是一个协程——可能是你用async def
函数加上create_task()
创建的协程。
Future类表示与循环交互的某个东西的状态。这个描述太模糊了,不太有用,所以你可以将Future实例视为一个切换器,一个完成状态的切换器。当创建Future实例时,切换设置为“尚未完成”状态,但稍后它将是“完成”状态。事实上,Future实例有一个名为done()
的方法,它允许你检查状态,如示例3-15所示。
示例 3-15. 用done()方法检查完成状态
Future实例还可以执行以下操作:
- 设置一个result值(用
.set_result(value)
设置值并且使用.result()
获取值) - 使用
.cancel()
方法取消(并且会用.cancelled()
检查是否取消) - 增加一个Future完成时回调的函数
即使Task更常见,也不可能完全避免使用Future:例如,在执行器上运行函数将返回Future实例,而不是Task。让我们快速看一下示例3-16,了解一下直接使用Future实例是什么感觉。
示例 3-16. 与Future实例的交互
(L3)创建一个简单的main函数。我们运行这个函数,等上一会儿然后在Future f上设置一个结果。
(L5)设置一个结果。
(L8)手动创建一个Future实例。注意,这个实例(默认情况下)绑定到我们的循环,但它没有也不会被附加到任何协程(这就是Tasks的作用)。
(L9)在做任何事情之前,确认future还没有完成。
(L11)安排main()协程,传递future。请记住,main()协程所做的所有工作就是sleep,然后切换Future实例。(注意main()协程还不会开始运行:协程只在事件循环运行时才开始运行。)
(L13)在这里我们在Future实例上而不是Task实例上使用run_until_complete()。这和你以前见过的不一样。现在循环正在运行,main()协程将开始执行.
(L16)最终,当future的结果被设置时,它就完成了。完成后,可以访问结果。
当然,你不太可能以这里所示的方式直接使用Future;代码示例仅用于教育目的。你与asyncio的大部分联系都是通过Task实例进行的。
你可能想知道如果在Task实例上调用
set_result()
会发生什么。在Python 3.8之前可以这样做,但现在不允许这么做了。任务实例是协程对象的包装器,它们的结果值只能在内部设置为底层协程函数的结果,如示例3-17所示。 示例 3-17. 在task上调用set_result (L13)唯一的区别是我们创建的是Task实例而不是Future实例。当然,Task API要求我们提供一个协程;这里我们使用sleep()只是因为简单方便。 (L7)正在传入一个Task实例。它满足函数的类型签名(因为Task是Future的子类),但从Python 3.8开始,我们不再允许在Task上调用set_result()
:尝试这样做将引发RuntimeError。这个想法是,一个Task代表一个正在运行的协程,所以结果应该总是来自于task自身。 (L10, L24)但是,我们仍然可以cancel()
一个任务,它将在底层协程中引发CancelledError。
Create_task? Ensure_Future? 下定决心吧!
在第22页的“快速入门”中,我说过运行协程的方法是使用asyncio.create_task()
。在引入该函数之前,有必要获取一个循环实例并使用loop.create_task()
完成相同的任务。事实上,这也可以通过一个不同的模块级函数来实现:asyncio.ensure_future()
。一些开发人员推荐create_task()
,而其他人推荐ensure_future()
。
在我为这本书做研究的过程中,我确信API方法asyncio.ensure_future()
是引起对asyncio库广泛误解的罪魁祸首。API的大部分内容都非常清晰,但在学习过程中还存在一些严重的障碍,这就是其中之一。当你遇到ensure_future()
时,你的大脑会非常努力地将其集成到关于asyncio应该如何使用的心理模型中——但很可能会失败!
在Python 3.6 asyncio文档中,这个现在已经臭名昭著的解释突出了ensure_future()
的问题:
asyncio.ensure_future(coro_or_future, *, _loop =None)
安排执行一个协程对象:把它包装在future中。返回一个Task对象。如果参数是Future,则直接返回。
什么!? 当我第一次读到这篇文章时,我很困惑。下面希望是对ensure_future()
的更清楚的描述:
这个函数很好地说明了针对终端用户开发人员的asyncio API(高级API)和针对框架设计人员的asyncio API(低级API)之间的区别。让我们在示例3-18中仔细看看它是如何工作的。
示例 3-18. 仔细看看ensure_future()在做什么
(L3)一个简单的什么都不做的协程函数。我们只需要一些能组成协程的东西。
(L6)我们通过直接调用该函数来创建协程对象。你的代码很少会这样做,但我想在这里明确地表示,我们正在向每个create_task()
和ensure_future()
传递一个协程对象。
(L7)获取一个循环。
(L9)首先,我们使用loop.create_task()
在循环中调度协程,并返回一个新的Task实例。
(L10)验证类型。到目前为止,没有什么有趣的。
(L12)我们展示了asyncio.ensure_future()
可以被用来执行与create_task()
相同的动作:我们传入了一个协程,并返回了一个Task实例(并且协程已经被安排在循环中运行)!如果传入的是协程,那么loop.create_task()
和asyncio.ensure_future()
之间没有区别。
(L15)如果我们给ensure_future()
传递一个Task实例会发生什么呢?注意我们要传递的Task实例是已经在第4步通过loop.create_task()
创建好的。
(L16)返回的Task实例与传入的Task实例完全相同:它在被传递时没有被改变。
直接传递Future实例的意义何在?为什么用同一个函数做两件不同的事情?答案是,ensure_future()
的目的是让框架作者向最终用户开发者提供可以处理两种参数的API。不相信我?这是ex-BDFL自己说的:
ensure_future()
的要点是,如果你有一个可能是协程或Future(后者包括一个Task,因为它是Future的子类)的东西,并且你想能够调用一个只在Future上定义的方法(可能唯一有用的例子是cancel())。当它已经是Future(或Task)时,它什么也不做;当它是协程时,它将它包装在Task中。 如果您知道您有一个协程,并且希望它被调度,那么正确的API是create_task()
。唯一应该调用ensure_future()
的时候是当你提供一个API(像大多数asyncio自己的API),它接受协程或Future,你需要对它做一些事情,需要你有一个Future。 —Guido van Rossum 总而言之,asyncio.ensure_future()
是一个为框架设计者准备的辅助函数。这一点最容易通过与一种更常见的函数进行类比来解释,所以我们来做这个解释。如果你有几年的编程经验,你可能已经见过类似于例3-19中的listify()
函数的函数。示例3-19中listify()
的函数。 示例 3-19. 一个强制输入列表的工具函数 这个函数试图将参数转换为一个列表,不管输入的是什么。API和框架中经常使用这类函数将输入强制转换为已知类型,这将简化后续代码——在本例中,您知道参数(来自listify()
的输出)将始终是一个列表。 如果我将listify()
函数重命名为ensure_list()
,那么您应该开始看到与asyncio.ensure_future()
的类似之处:它总是试图将参数强制转换为Future(或子类)类型。这是一个实用函数,它使框架开发人员(而不是像你我这样的终端用户开发人员)的工作变得更容易。 实际上,asyncio
标准库模块本身使用ensure_future()
正是出于这个原因。当你下次查看API时,你会发现函数参数被描述为“可等待对象”,很可能内部使用ensure_future()
进行类型强制转换。例如,asyncio.gather()
函数就像下面的代码一样:aws
参数表示“可等待对象”,包括协程、task和future。在内部,gather()
使用ensure_future()
进行类型强制转换:task和future保持不变,而把协程强制转为task。 这里的关键是,作为终端用户应用程序开发人员,应该永远不需要使用asyncio.ensure_future()
。它更像是框架设计师的工具。如果你需要在事件循环上调度协程,只需直接使用asyncio.create_task()
来完成。 在接下来的几节中,我们将回到语言级别的特性,从异步上下文管理器开始。
如何在scrapy框架下,用python实现爬虫自动跳转页面来抓去网页内容??
Scrapy是一个用Python写的Crawler Framework,简单轻巧,并且非常方便。Scrapy使用Twisted这个异步网络库来处理网络通信,架构清晰,并且包含了各种中间件接口,可以灵活地完成各种需求。Scrapy整体架构如下图所示: 根据架构图介绍一下Scrapy中的各大组件及其功能:
- Scrapy引擎(Engine):负责控制数据流在系统的所有组建中流动,并在相应动作发生触发事件。
- 调度器(Scheduler):从引擎接收Request并将它们入队,以便之后引擎请求request时提供给引擎。
- 下载器(Downloader):负责获取页面数据并提供给引擎,而后提供给Spider。
- Spider:Scrapy用户编写用于分析Response并提取Item(即获取到的Item)或额外跟进的URL的类。每个Spider负责处理一个特定(或一些网站)。
- Item Pipeline:负责处理被Spider提取出来的Item。典型的处理有清理验证及持久化(例如存储到数据库中,这部分后面会介绍存储到MySQL中,其他的数据库类似)。
- 下载器中间件(Downloader middlewares):是在引擎即下载器之间的特定钩子(special hook),处理Downloader传递给引擎的Response。其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能(后面会介绍配置一些中间并激活,用以应对反爬虫)。
- Spider中间件(Spider middlewares):是在引擎及Spider之间的特定钩子(special hook),处理Spider的输入(response)和输出(Items即Requests)。其提供了一个简便的机制,通过插入自定义的代码来扩展Scrapy功能。