|
|
51CTO旗下网站
|
|
移动端

如何用一行代码让gevent爬虫提速100%

用python做网络开发的人估计都听说过gevent这个库,gevent是一个第三方的python协程库,其是在微线程库greenlet的基础上构建而成,并且使用了epoll事件监听机制,这让gevent具有很好的性能并且比greenlet更好用。

作者:Mort来源:Python中文社区|2020-07-20 09:20

 

用python做网络开发的人估计都听说过gevent这个库,gevent是一个第三方的python协程库,其是在微线程库greenlet的基础上构建而成,并且使用了epoll事件监听机制,这让gevent具有很好的性能并且比greenlet更好用。根据gevent官方的资料(网址:http://www.gevent.org),gevent具有以下特点:

  1.  基于libev或libuv的快速事件循环。
  2.  基于greenlet的轻量级执行单元。
  3.  重复使用Python标准库中的概念的API(例如,有event和 queues)。
  4.  具有SSL支持的协作套接字
  5.  通过线程池,dnspython或c-ares执行的合作DNS查询。
  6.  猴子修补实用程序,使第三方模块能够合作
  7.  TCP / UDP / HTTP服务器
  8.  子流程支持(通过gevent.subprocess)
  9.  线程池

笔者总结一下,gevent大致原理就是当一个greenlet遇到需要等待的操作时(多为IO操作),比如网络IO/睡眠等待,这时就会自动切换到其他的greenlet,等上述操作完成后,再在适当的时候切换回来继续执行。在这个过程中其实仍然只有一个线程在执行,但因为我们在等待某些IO操作时,切换到了其他操作,避免了无用的等待,这就为我们大大节省了时间,提高了效率。

笔者也是在看了gevent这么多的优点之后,感觉有必要上手试一试,但起初效果非常不理想,速度提升并不大,后来在仔细研究了gevent的用法之后,发现gevent的高效率是有条件的,而其中一个重要条件就是monkey patch的使用,也就是我们常说的猴子补丁。

monkey patch就是在不改变源代码的情况下,对程序进行更改和优化,其主要适用于动态语言。通过monkey patch,gevent替换了标准库里面大部分的阻塞式系统调用,比如socket、ssl、threading和select等,而变为协作式运行。下面笔者还是通过代码来演示一下monkey patch的用法以及使用条件。笔者展示的这个程序是一个小型的爬虫程序,程序代码量少,便于阅读和运行,同时也能较好地测试出monkey patch的提升程度。主要思路是从Box Office Mojo网站抓取北美电影市场今年第二季度上映的电影,然后从每部电影的信息页面提取出每部电影的电影分级,然后把每部电影的名称和其对应分级保存在一个字典当中,再测试一下整个过程的时间。在这里,我们主要测试三种情况下的程序完成时间,分别是普通不使用gevent的爬虫,使用gevent但不用monkey patch的爬虫,以及使用gevent和monkey patch的爬虫。

首先看普通不使用gevent的爬虫。

先导入需要的库。

  1. import time  
  2. import requests  
  3. from lxml import etree 

然后读取第二季度上映电影的页面。

  1. url = r'https://www.boxofficemojo.com/quarter/q2/2020/?grossesOption=totalGrosses' #第二季度上映电影的网址  
  2. headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'} #爬虫头部  
  3. rsp = requests.get(url, headersheaders=headers) #读取网页  
  4. text = rsp.text #获取网页源码  
  5. html = etree.HTML(text)  
  6. movie_relative_urls =  html.xpath(r'//td[@class="a-text-left mojo-field-type-release mojo-cell-wide"]/a/@href') #获取每部电影的信息页面的相对地址  
  7. movie_urls = [r'https://www.boxofficemojo.com'+u for u in movie_relative_urls] #把每部电影的相对地址换成绝对地址  
  8. genres_dict = {} #用于保存信息的字典 

上述代码中变量url就是第二季度上映电影的网页地址,其页面截图如图1所示。headers是爬虫模拟浏览器的头部信息,每部电影的信息页面就是图1中表格头一行列名Release下面每部电影名称所包含的网址,点击每部电影名称就可进入其对应页面。因为这个网址是相对地址,所以要转换成绝对地址。

图1. 第二季度上映电影的页面

接下来是每部电影的信息页面的读取。

  1. def spider(url): #这个函数主要用于读取每部电影页面中的电影分级信息  
  2.     rsp = requests.get(url, headersheaders=headers) #读取每部电影的网页  
  3.     text = rsp.text #获取页面代码  
  4.     html = etree.HTML(text)  
  5.     genre = html.xpath(r'//div/span[text()="Genres"]/following-sibling::span[1]/text()')[0] #读取电影分级信息  
  6.     title = html.xpath(r'//div/h1/text()')[0] #读取电影名称  
  7. genres_dict[title] = genre #把每部电影的名称和分级信息存入字典 

这个函数就是为了读取每部电影信息页面的信息,其功能和上面读取url页面的功能类似,都非常简单,没有过多可说的。在每部电影页面中,我们要读取的每部电影的分级信息就在Genres这一行,比如图2中电影The Wretched,其Genres信息就是Horror。

图2. 示例电影信息页面

接下来是时间测算。

  1. normal_start = time.time() #程序开始时间  
  2. for u in movie_urls:  
  3.     spider(u)  
  4. normal_end = time.time() #程序结束时间  
  5. normal_elapse = normal_end - normal_start #程序运行时间  
  6. print('The normal procedure costs %s seconds' % normal_elapse) 

我们测算时间用time.time()方法,用结束时间减去开始时间就是程序运行时间,这里我们主要测试spider这个函数多次运行的时间。结果显示,该过程耗时59.6188秒。

第二个爬虫是使用gevent但不用monkey patch的爬虫。其完整代码如下。

  1. import time  
  2. from lxml import etree  
  3. import gevent  
  4. import requests  
  5. url = r'https://www.boxofficemojo.com/quarter/q2/2020/?grossesOption=totalGrosses'  
  6. headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'}  
  7. rsp = requests.get(url, headersheaders=headers)  
  8. text = rsp.text  
  9. html = etree.HTML(text)  
  10. movie_relative_urls =  html.xpath(r'//td[@class="a-text-left mojo-field-type-release mojo-cell-wide"]/a/@href')  
  11. movie_urls = [r'https://www.boxofficemojo.com'+u for u in movie_relative_urls]  
  12. genres_dict = {}  
  13. task_list = [] #用于存放协程的列表  
  14. def spider(url): 
  15.     rsp = requests.get(url, headersheaders=headers)  
  16.     text = rsp.text  
  17.     html = etree.HTML(text)  
  18.     genre = html.xpath(r'//div/span[text()="Genres"]/following-sibling::span[1]/text()')[0]  
  19.     title = html.xpath(r'//div/h1/text()')[0]  
  20.     genres_dict[title] = genre    
  21. gevent_start = time.time()  
  22. for u in movie_urls:  
  23.     task = gevent.spawn(spider, u) #生成协程  
  24.     task_list.append(task) #把协程放入这个列表    
  25. gevent.joinall(task_list) #运行所有协程  
  26. gevent_end = time.time() 
  27. gevent_elapse = gevent_end - gevent_start  
  28. print('The gevent spider costs %s seconds' % gevent_elapse) 

这里绝大部分代码和前面爬虫代码相同,但多了一个task_list变量,其是用于存放协程的列表,我们从gevent_start = time.time()这行开始看,因为前面的代码都和之前的爬虫相同。task = gevent.spawn(spider, u)是生成gevent中生成协程的方法,task_list.append(task)是把每个协程放入这个列表中,而gevent.joinall(task_list)就是运行所有协程。上面这些过程和我们运行多线程的方式非常相似。运行结果是59.1744秒。

最后一个爬虫就是同时使用gevent和monkey patch的爬虫,在这里笔者不再粘贴代码,因为其代码和第二个爬虫几乎一模一样,只有一个区别,就是多了一行代码from gevent import monkey; monkey.patch_all(),注意这是一行代码,不过包含两个语句,用分号放在了一起。最重要的是,这行代码要放在所有代码的前面,切记!!!

这个爬虫的运行结果是26.9184秒。

笔者把这里三个爬虫分别放在三个文件中,分别命名为normal_spider.py、gevent_spider_no.py和gevent_spider.py,分别表示普通不用gevent的爬虫、使用gevent但不用monkey patch的爬虫、使用gevent和monkey patch的爬虫。这里有一点要注意,monkey patch暂不支持jupyter notebook,所以这三个程序要在命令行中使用,不能在notebook中使用。

最后把三种爬虫的结果总结如下。

图3. 三种爬虫的结果对比

可以看出使用了gevent但不用monkey patch的爬虫和普通爬虫的运行时间几乎完全相等,而在用了monkey patch以后,运行时间只有前面程序的一半不到,速度提升了大约120%,仅仅一行代码就带来如此大的速度提升,可见monkey patch的作用还是很大的。而对于前两个爬虫的速度几乎完全一样,笔者认为原因在于这两个程序都是单线程运行,本质上没有太大区别,同时网页读取数量较小(只有18个网页),也很难看出gevent的效果。

从本例中可以看出monkey patch还是有不小提升的,但gevent目前只对常见库尤其是官方标准库有patch作用,其他第三方库的效果还不得而知,所以对monkey patch的使用还是要视情况而定。本文的代码笔者放在gitee代码网站上,网址是https://gitee.com/leonmovie/speed-up-gevent-spider-with-monkey-patch,如有需要可以自行下载。

【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

大数据安全运维实战

大数据安全运维实战

CDH+Ambari
共20章 | 大数据陈浩

86人订阅学习

实操案例:Jenkins持续交付和持续部署

实操案例:Jenkins持续交付和持续部署

微服务架构下的自动化部署
共18章 | freshman411

176人订阅学习

思科交换网络安全指南

思科交换网络安全指南

安全才能无忧
共5章 | 思科小牛

108人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微