python爬虫从入门到放弃之十二:多协程
一个爬虫,每发起一个请求,都要等服务器返回响应后,才会执行下一步。而很多时候,由于网络不稳定,加上服务器自身也需要响应的时间,导致爬虫会浪费大量时间在等待上。这也是爬取大量数据时,爬虫的速度会比较慢的原因。
既然一个爬虫爬取大量数据要爬很久,那我们能不能让多个爬虫一起爬取?
-
同步、异步
同步:就是一个任务结束才能启动下一个
异步:在一个任务未完成时,就可以执行其他多个任务,彼此不受影响
显然,异步执行任务会比同步更加节省时间,因为它能减少不必要的等待。
-
多协程
一种非抢占式的异步方式,叫多协程(在此,多是多个的意思)
它的原理是:一个任务在执行过程中,如果遇到等待,就先去执行其他的任务,当等待结束,再回来继续之前的那个任务。在计算机的世界,这种任务来回切换得非常快速,看上去就像多个任务在被同时执行一样。
-
多协程的用法:gevent库
- 安装方法:
pip install gevent(Win)
pip3 install gevent(Mac)
爬取8个网站(包括百度、新浪、搜狐、腾讯、网易、爱奇艺、天猫、凤凰)
我们先用之前同步的爬虫方式爬取这8个网站,然后等下再和gevent异步爬取做一个对比。
- 同步的爬虫方式
import requests,time
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/',
'http://www.ifeng.com/']
for url in url_list:
r = requests.get(url)
print(url,r.status_code)
end = time.time()
print(end-start)
运行结果:
https://www.baidu.com/ 200
https://www.sina.com.cn/ 200
http://www.sohu.com/ 200
https://www.qq.com/ 200
https://www.163.com/ 200
http://www.iqiyi.com/ 200
https://www.tmall.com/ 200
http://www.ifeng.com/ 200
1.0101947784423828
- 异步的爬虫方式
from gevent import monkey
monkey.patch_all()
import gevent,time,requests
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/',
'http://www.ifeng.com/']
def crawler(url):
r = requests.get(url)
print(url,time.time()-start,r.status_code)
tasks_list = [gevent.spawn(crawler,url) for url in url_list]
gevent.joinall(tasks_list)
end = time.time()
print(end-start)
运行结果:
http://www.iqiyi.com/ 0.16013050079345703 200
http://www.ifeng.com/ 0.22873139381408691 200
https://www.qq.com/ 0.2317953109741211 200
https://www.baidu.com/ 0.26370882987976074 200
https://www.163.com/ 0.2657487392425537 200
https://www.sina.com.cn/ 0.287355899810791 200
https://www.tmall.com/ 0.3268725872039795 200
http://www.sohu.com/ 0.35901474952697754 200
0.35901474952697754
程序运行后,打印出了网址、每个请求运行的时间、状态码和爬取8个网站最终所用时间。
通过每个请求运行的时间,我们能知道:爬虫用了异步的方式抓取了8个网站,因为每个请求完成的时间并不是按着顺序来的。
且每个请求完成时间之间的间隔都非常短,可以看作这些请求几乎是“同时”发起的。
通过对比同步和异步爬取最终所花的时间,用多协程异步的爬取方式,确实比同步的爬虫方式速度更快。
其实,我们案例爬取的数据量还比较小,不能直接体现出更大的速度差异。如果爬的是大量的数据,运用多协程会有更显著的速度优势。
现在,我们一行行来看刚刚用了gevent的代码。
gevent.monkey模块,能将程序转换成可异步的程序,monkey.patch_all(),它的作用其实就像你的电脑有时会弹出“是否要用补丁修补漏洞或更新”一样。它能给程序打上补丁,让程序变成是异步模式,而不是同步模式。它也叫“猴子补丁”。
我们要在导入其他库和模块前,先把monkey模块导入进来,并运行monkey.patch_all()。这样,才能先给程序打上补丁。
由于gevent只能处理gevent的任务对象,不能直接调用普通函数,所以需要借助gevent.spawn()来创建任务对象。gevent.spawn()的参数需为要调用的函数名及该函数的参数。比如,gevent.spawn(crawler,url)就是创建一个执行crawler函数的任务,参数为crawler函数名和它自身的参数url。
最后,调用gevent库里的joinall方法,能启动执行所有的任务。gevent.joinall(tasks_list)就是执行tasks_list这个任务列表里的所有任务,开始爬取。
-
那么问题来了?
如果我们要爬的不是8个网站,而是1000个网站,我们可以怎么做?
用我们刚刚的gevent语法,我们可以用gevent.spawn()创建1000个爬取任务,再用gevent.joinall()执行这1000个任务。
但这种方法会有问题:执行1000个任务,就是一下子发起1000次请求,这样子的恶意请求,会拖垮网站的服务器。
那我们能不能只创建成5个任务,但每个任务爬取200个网站?
这么做也还是会有问题的。就算我们用gevent.spawn()创建了5个分别执行爬取200个网站的任务,这5个任务之间是异步执行的,但是每个任务(爬取200个网站)内部是同步的。
这意味着:如果有一个任务在执行的过程中,它要爬取的一个网站一直在等待响应,哪怕其他任务都完成了200个网站的爬取,它也还是不能完成200个网站的爬取。
这时我们可以从实际生活的案例中得到启发。想想银行是怎么在一天内办理1000个客户的业务的。
银行会开设办理业务的多个窗口,让客户取号排队,由银行的叫号系统分配客户到不同的窗口去办理业务。
在gevent库中,也有一个模块可以实现这种功能——queue模块。
-
queue模块
queue翻译成中文是队列的意思。我们可以用queue模块来存储任务,让任务都变成一条整齐的队列,就像银行窗口的排号做法。因为queue其实是一种有序的数据结构,可以用来存取数据。
这样,协程就可以从队列里把任务提取出来执行,直到队列空了,任务也就处理完了。就像银行窗口的工作人员会根据排号系统里的排号,处理客人的业务,如果已经没有新的排号,就意味着客户的业务都已办理完毕。
代码实现:
from gevent import monkey
monkey.patch_all()
import gevent,time,requests
from gevent.queue import Queue
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/',
'http://www.ifeng.com/']
work = Queue()
for url in url_list:
work.put_nowait(url)
def crawler():
while not work.empty():
url = work.get_nowait()
r = requests.get(url)
print(url,work.qsize(),r.status_code)
tasks_list = [gevent.spawn(crawler) for x in range(2)]
gevent.joinall(tasks_list)
end = time.time()
print(end-start)
以上代码
work = Queue()
实例化一个队列,可以传入参数,如Queue(10)表示队列只能存储10个成员。
work.put_nowait(url)
表示:添加队列的成员
while not work.empty():
表示:当这个队列不为空时
url = work.get_nowait()
表示:取出队列的成员
work.qsize()
表示:队列的长度
tasks_list = [gevent.spawn(crawler) for x in range(2)]
相当于创建2个爬虫执行crawler()函数
gevent.joinall(tasks_list)
启动所有任务
-
queue对象的方法
put_nowait() # 往队列里存储数据
get_nowait() # 从队列里取出数据
empty() # 判断队列是否为空
full() # 判断队列是否为满
qsize() # 判断队列还剩多少数量
实例展示:
时光网TOP100链接:http://www.mtime.com/top/tv/top100/
使用多协程和队列,爬取时光网电视剧TOP100的数据(剧名、导演、主演和简介),并用csv模块将数据存储下来。
范例代码:
from gevent import monkey
monkey.patch_all()
from gevent.queue import Queue
from lxml import etree
import requests,csv,gevent
work = Queue()
work.put_nowait('http://www.mtime.com/top/tv/top100/')
for n in range(2,11):
url = 'http://www.mtime.com/top/tv/top100/index-%d.html'%n
work.put_nowait(url)
def info(list_name):
if list_name==[]:
return ''
else:
return list_name[0]
def crawler():
while not work.empty():
url = work.get_nowait()
headers = {'User-Agent': 'Mozilla/5.0'}
r = requests.get(url,headers = headers)
html = etree.HTML(r.text)
tvs = html.xpath('//ul[@id="asyncRatingRegion"]/li')
for tv in tvs:
title = tv.xpath('./div[2]/a/@title')
director = tv.xpath('./div[3]/p[1]/a/text()')
actor1 = tv.xpath('./div[3]/p[2]/a[1]/text()')
actor2 = tv.xpath('./div[3]/p[2]/a[2]/text()')
introduction = tv.xpath('./div[3]/p[3]/text()')
title = info(title)
director = info(director)
actors = info(actor1)+' '+info(actor2)
introduction = info(introduction)
print(title,director,actors,introduction)
writer.writerow([title,director,actors,introduction])
f = open('tv.csv','a',newline = '',encoding = 'utf-8')
writer = csv.writer(f)
writer.writerow(['剧名','导演','主演','简介'])
tasks_list = [gevent.spawn(crawler) for x in range(5)]
gevent.joinall(tasks_list)
f.close()
>>>
阅读更多文章请点击以下链接:
python爬虫从入门到放弃之一:认识爬虫
python爬虫从入门到放弃之二:HTML基础
python爬虫从入门到放弃之三:爬虫的基本流程
python爬虫从入门到放弃之四:Requests库基础
python爬虫从入门到放弃之五:Requests库高级用法
python爬虫从入门到放弃之六:BeautifulSoup库
python爬虫从入门到放弃之七:正则表达式
python爬虫从入门到放弃之八:Xpath
python爬虫从入门到放弃之九:Json解析
python爬虫从入门到放弃之十:selenium库
python爬虫从入门到放弃之十一:定时发送邮件
python爬虫从入门到放弃之十二:多协程
python爬虫从入门到放弃之十三:Scrapy概念和流程
python爬虫从入门到放弃之十四:Scrapy入门使用