spider 08-min.jpg

asyncio 模块于Python 3.4添加到标准库中,它在单线程中使用事件循环来驱动协程从而实现并发。对事件循环来说,调用回调与在暂停的协程上调用 .send() 方法效果差不多。各个暂停的协程是要消耗内存,但是比线程消耗的内存数量级小。而且,协程能避免可怕的"回调地狱"。使用 asyncio 包时,我们编写的协程被包装成Task对象(相当于调用方),并且在我们编写的协程中,会通过调用 await 或 yield from 来使用由 asyncio 模块或第三方库(如aiohttp)所提供的协程(即委派生成器),而生成器最终把职责委托给Future对象,这种处理方式相当于架起了管道,让 asyncio 事件循环(通过我们编写的协程)驱动执行低层异步 I/O 操作的库函数

spider 07-min.jpg

生成器可以作为协程(coroutine)使用,称为 "基于生成器的协程"。协程和生成器类似,都是定义体中包含 yield 关键字的函数。但它们也有本质区别,生成器用于 生成 供迭代的数据,next()方法只允许调用方从生成器中获取数据; 而协程与迭代无关,协程是数据的消费者,调用方会把数据推送给协程。PEP 342给生成器增加了 send() 方法,允许调用方和协程之间双向交换数据。PEP 380允许生成器中可以return返回值,并新增了 yield from 语法结构,打开了调用方和子协程的双向通道。PEP 492新增了async和await关键字,实现了 "原生协程",以便于跟生成器进行区分。协程不等于异步编程,所以将在下一篇博客中介绍 asyncio 模块,它提供了事件循环,利用Coroutines、Tasks、Futures一起才能实现异步I/O(底层基于selectors模块,请回头查看本爬虫系列的第一篇博客I/O models中的I/O multiplexing)

spider 06-min.jpg

Python中内置的序列,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代的,但它们不是迭代器。迭代器可以被 next() 函数调用,并不断返回下一个值。Python从可迭代的对象中获取迭代器。迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间,实现高效处理大量数据。在Python 3中,生成器有广泛的用途,所有生成器都是迭代器,因为生成器完全实现了迭代器接口。迭代器用于从集合中取出元素,而生成器用于"凭空"生成元素 。PEP 342 给生成器增加了 send() 方法,实现了"基于生成器的协程"。PEP 380允许生成器中可以return返回值,并新增了 yield from 语法结构,打开了调用方和子生成器的双向通道

spider 05-min.png

I/O密集型最适合使用多线程,当然包括网络I/O。我们要下载多张图片,每次去下载一张图片,就是发起一次HTTP请求(使用TCP协议),客户端首先通过socket.socket()创建一个套接字,然后调用connect()方法经过三次握手与服务端建立TCP连接,这个过程是阻塞的。建立连接后,客户端将请求(要访问图片资源)发送给服务端,然后服务端返回响应,客户端用recv()方法每次接收一定数量的字节,客户端在每个响应报文(一张图片有多个数据包)到达操作系统内核时,是阻塞的。网络I/O对于CPU来说是无比漫长的,如果是依序下载,CPU就要一直阻塞到第1张图片的字节全部下载完成后,才能下载第2张,这些等待的时间(对CPU来说,比它处理数据的时间多出无数倍)就白白浪费了。为了合理利用CPU资源,可以使用多线程,每个线程去下载一张图片,当下载第1张图片的任务阻塞时,CPU切换到第2个线程,它开始下载第2张图片,依次类推,当第1张图片有响应报文到达时,等其它线程阻塞后,CPU又会切换到下载第1张图片的那个线程