Python 3 爬虫-min.png

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

Python 3 爬虫-min.png

生成器可以作为协程(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)

Python 3 爬虫-min.png

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

Python 3 爬虫-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 张图片的那个线程

专题系列