建立爬虫大军之多协程
前言
如果我们想要爬取的是成千上万条的数据,那么就会遇到一个问题:因为程序是一行一行地依次执行,要等待很久,才能拿到想要的数据。
既然一个爬虫爬取大量数据要爬很久,那我们能不能让多个爬虫一起爬取?这样无疑能提高爬取的效率,就像一个人干不完的活儿,组个团队一起干,活儿一下被干完了。
那具体怎么用Python实现这事儿呢?
同步与异步
当需要爬取多个URL时,爬虫每发起一个请求,都要等服务器返回响应后,才能执行下一步。而由于网页请求和获得响应过程比较耗费时间,这导致爬虫会浪费大量时间在等待上。
图中所示的就是同步爬虫的运行模式。
而与之对应的异步爬虫则是并发或并行的,各个任务可以独立运行,一个任务的运行不受另一个任务影响。(可以理解成同一时间有多个事情在做,但有先后顺序)
我们可以采取异步的爬虫方式,让多个爬虫在执行任务时保持相对独立,彼此不受干扰,这样就可以免去等待时间,提高爬虫的效率和速度。
什么是多协程
刚才提到:
异步爬虫是并发或并行的
这两者有什么区别呢?
计算机利用单个核心在多个任务之间来回切换,称为并发;利用CPU的多核同时执行多个任务,称为并行。
并发 | 并行 |
---|---|
一个人合理利用时间,做多件事情,例如:同时炒菜和煲汤 | 多个人做多件事情,例如:一个人备菜,一个人炒菜 |
宏观上同时发送 | 宏观上同时发生 |
微观上交替运行 | 微观上同时发生 |
本文将讲述实现并发的一种方式——多协程,主要提高IO并发。
它的原理是:对于IO密集任务,如果遇到等待,可以随时中断,先去执行其他的任务,当等待结束,再回来继续之前的那个任务(可以自由切换),大大减少IO的等待时间。
这整个过程看似是多线程,像多个任务在被同时执行一样,但实际上协程只有一个线程执行,属于单线程高并发。
多协程的优势
- 执行效率极高,资源占用低。因为子程序切换(任务函数)不是线程切换,由程序自身控制,没有切换线程的开销,充分利用了IO等待时间。与多线程相比,线程的数量越多,协程性能的优势越明显。
- 不需要多线程的锁机制。因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁。
- 开发者可以指定切换的时机,同时调度多个协程(非抢占式并发)。
怎么用多协程
使用gevent
库可以实现多协程。gevent
是一个基于greenlet
实现的网络库,通过greenlet
实现协程。
基本思想是:一个greenlet
可以认为是一个协程,当一个greenlet
遇到IO操作的时候,就会自动切换到其他的greenlet
,待IO操作完成,在适当的时候切换回来继续执行。gevent
可以保证总有greenlet
在运行,而不是等待IO操作。
同时,当我们用多协程来爬虫,需要创建大量任务时,我们可以借助queue
模块,让“任务排个队”。
gevent库
导入gevent
库前,得先安装它。
window电脑:pip install gevent
mac电脑:pip3 install gevent
先上个没有queue模块的代码,可以看一下注释:
# 从gevent库里导入monkey模块
from gevent import monkey
# monkey.patch_all()能把程序变成协作式运行
monkey.patch_all()
# 导入gevent库来实现多协程,导入time模块来记录爬取所需时间,导入requests模块实现爬取10个网站。
import gevent,time,requests
# 记录程序开始时间
start = time.time()
# 把10个网站封装成列表
url_list = ['https://www.baidu.com/',
'https://www.sina.com.cn/',
'http://www.sohu.com/',
'https://www.qq.com/',
'https://www.163.com/',
'http://www.iqiyi.com/',
'https://www.tmall.com/',
'https://www.zhihu.com/',
'https://www.ipe.org.cn/',
'https://www.youku.com/']
#定义一个函数
def crawler(url):
#用requests.get()爬取网站
r = requests.get(url)
# 打印网址、请求运行时间、状态码
print(url,time.time()-start,r.status_code)
# 创建空的任务列表
tasks_list = [ ]
# 遍历url_list
for url in url_list:
# 用gevent.spawn()创建任务,参数为crawler函数名和它自身的参数url。
task = gevent.spawn(crawler,url)
# 往任务列表添加任务
tasks_list.append(task)
# 执行任务列表里的所有任务,就是让爬虫开始爬取网站
gevent.joinall(tasks_list)
# 记录程序结束时间
end = time.time()
#打印程序最终所需时间
print(end-start)
因为requests库是阻塞式的,只有将requests库阻塞式更改为非阻塞,异步操作才能实现
而gevent库中的猴子补丁(monkey patch)能够修改标准库里面大部分的阻塞式系统调用。这样在不改变原有代码的情况下,将应用的阻塞式方法,变成协程式的(异步)
这里需要注意一点,在导入其他库和模块前,要先把monkey模块导入进来,并运行monkey.patch_all(),这样,才能先给程序打上补丁,使得程序实现异步
掘友们可以自行对比使用多协程和不使用之间的速度差异,这里我不再展示。
小结一下使用gevent实现异步爬虫的关键步骤:
- 定义爬虫函数
- 用
gevent.spawn()
创建任务,gevent.spawn()
的参数是:要调用的函数名 及 该函数的参数。 - 用
gevent.joinall()
执行任务
还有一个问题,当我们要爬的不是10个网站,而是1000个网站,我们应该怎么做?
是用gevent.spawn()
创建1000个爬取任务,再用gevent.joinall()
执行这1000个任务吗?
这种方法会有问题:执行1000个任务,就是一下子发起1000次请求,这样的恶意请求,会拖垮网站的服务器。
既然这种直接创建1000个任务的方式不可取,那能不能创建成5个任务,但每个任务爬取200个网站?
这么做也还是会有问题的。就算我们用gevent.spawn()
创建了5个分别执行爬取200个网站的任务,这5个任务之间是异步执行的,但是每个任务(爬取200个网站)内部是同步的。
那我们应该如何做呢?
queue模块
这就引出了另一个主角——queue,我们可以用queue模块来存储任务,让任务排成队。
协程可以从队列里把任务一一提取出来执行,队列空了,任务也就处理完了。
把上面的代码拆成4部分来演示:
第1部分是导入模块。
# 从gevent库里导入monkey模块
from gevent import monkey
# monkey.patch_all()使得程序实现异步
monkey.patch_all()
# 导入gevent、time、requests
import gevent,time,requests
# 因为gevent库里就带有queue,直接导入`queue`模块即可
from gevent.queue import Queue
第2部分,是如何创建队列,以及怎么把任务存储进队列里。
#记录程序开始时间
start = time.time()
url_list = ['https://www.baidu.com/',
'https://www.sina.com.cn/',
'http://www.sohu.com/',
'https://www.qq.com/',
'https://www.163.com/',
'http://www.iqiyi.com/',
'https://www.tmall.com/',
'https://www.zhihu.com/',
'https://www.ipe.org.cn/',
'https://www.youku.com/']
# 创建队列对象
work = Queue()
# 遍历url_list
for url in url_list:
# 创建了queue对象后,用put_nowait()函数可以把网址都放进队列里
work.put_nowait(url)
用Queue()能创建queue对象,如果不加参数,相当于创建了一个不限任何存储数量的空队列。 如果传入参数,比如Queue(10),则表示这个队列只能存储10个任务。
第3部分,是定义爬取函数,和从队列里提取出刚刚存储进去的网址。
def crawler():
# 当队列不是空的时候,就执行下面的程序
while not work.empty():
url = work.get_nowait() # 用get_nowait()函数可以把队列里的网址都取出
r = requests.get(url) # 用requests.get()函数抓取网址
print(url,work.qsize(),r.status_code)# 打印网址、队列长度、抓取请求的状态码
代码块中涉及到queue对象的三个方法:
empty
方法,用来判断队列是否为空的;get_nowait
方法,用来从队列里提取数据的;qsize
方法,用来判断队列里还剩多少数量的。
附一张queue对象的常用方法,点个收藏。在需要时,可以来查表。
方法 | 作用 |
---|---|
put_nowait() | 往队列里存储数据 |
get_nowait | 从队列里提取数据 |
empty() | 判断队列是否为空 |
full() | 判断队列是否为满 |
qsize() | 判断队列还剩多少数量 |
小结一下queue模块的基操:
Queue()
创建队列put_nowait()
存储数据get_nowait()
提取数据
第4部分就是让爬虫用多协程执行任务,爬取队列里的10个网站的代码
# 创建空的任务列表
tasks_list = [ ]
# 这里创建了3个爬虫
for x in range(3):
# 用gevent.spawn()函数创建执行crawler()函数的任务
task = gevent.spawn(crawler)
# 往任务列表添加任务
tasks_list.append(task)
# 用gevent.joinall方法,执行任务列表里的所有任务,让爬虫开始爬取
gevent.joinall(tasks_list)
end = time.time()
print(end-start)
用一张图解释这个过程:
这里创建三个可以异步爬取的爬虫。它们会从队列里取走网址,执行爬取任务。一旦一个网址被一只爬虫取走,其他爬虫就取不到了,另一只爬虫就会取走下一个网址。直至所有网址都被取走,队列为空时,爬虫停止工作。
END
多协程,其实只占用了CPU的一个核运行,没有充分利用到其他核,如果要充分发挥CPU利用率可以结合多进程+协程,后续我会再出一篇文章,围绕其讲述。
本人水平有限,烦请掘友们多多指教。
转载自:https://juejin.cn/post/7128567431928217631