当前位置: 首页 > 科技观察

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

时间:2023-03-17 19:48:50 科技观察

使用python做网络开发的人应该都听说过gevent库。gevent是一个第三方python协程库,它是基于微线程库greenlet构建的,使用了epoll事件监听机制,使得gevent有很好的性能,比greenlet好用。根据gevent官方资料(网址:http://www.gevent.org),gevent有以下特点:基于libev或libuv的快速事件循环。基于greenlets的轻量级执行单元。重用Python标准库中的概念的API(例如,有事件和队列)。支持SSL的协作套接字通过线程池、dnspython或c-ares执行的协作DNS查询。Monkey-patchingutilitytoenablethird-partymodulestocooperatewithTCP/UDP/HTTPserversSubprocesssupport(viagevent.subprocess)Threadpool作者总结gevent的一般原理是当greenlet遇到需要等待的操作(多为IO操作),比如网络IO/sleepwaiting,那么会自动切换到其他greenlets,等上述操作完成后再在合适的时间切换回来继续执行。在这个过程中,仍然只有一个线程在执行,但是因为我们在等待一些IO操作的同时切换到其他操作,避免了无用的等待,为我们节省了很多时间,提高了效率。看到gevent的这么多优点,笔者也觉得有必要尝试一下,但一开始效果并不理想,速度提升也不大。后来仔细研究了gevent的用法,发现gevent的高效率还是有用的。条件,而重要的条件之一就是使用monkeypatch,也就是我们常说的monkeypatch。Monkeypatch是在不改变源代码的情况下对程序进行更改和优化,主要适用于动态语言。通过monkeypatch,gevent取代了标准库中的大部分阻塞式系统调用,如socket、ssl、threading和select,成为协同运行。接下来笔者还是用代码来演示一下monkeypatch的使用方法和条件。作者展示的程序是一个小的爬虫程序,程序代码量小,易于阅读和运行,也能更好地测试monkeypatch的改进程度。主要思路是从BoxOfficeMojo网站上抓取北美电影市场今年第二季度上映的电影,然后从每部电影的信息页中提取每部电影的电影评分,然后保存名称每部电影及其对应的评分在字典中,再次测试整个过程的时间。这里主要测试三种情况下程序的完成时间,分别是没有gevent的普通爬虫、有gevent但没有monkeypatch的爬虫、有gevent和monkeypatch的爬虫。先看不使用gevent的普通爬虫。首先导入所需的库。importtimeimportrequestsfromlxmlimportetree然后读取第二季度上映的电影页面。url=r'https://www.boxofficemojo.com/quarter/q2/2020/?grossesOption=totalGrosses'#二季度上映电影的URLheaders={'User-Agent':'Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/78.0.3904.108Safari/537.36'}#爬虫头rsp=requests.get(url,headersheaders=headers)#读取网页text=rsp.text#获取网页源代码html=etree.HTML(text)movie_relative_urls=html.xpath(r'//td[@class="a-text-leftmojo-field-type-releasemojo-cell-wide"]/a/@href')#获取每部电影信息页的相对地址movie_urls=[r'https://www.boxofficemojo.com'+uforuinmovie_relative_urls]#将每部电影的相对地址替换为绝对地址genres_dict={}#保存字典information上述代码中的变量url为第二季上映的电影网址,页面截图如图1所示。headers为爬虫模拟浏览器的头部信息。每部电影的信息页面是图1表格第一行Release列下的每部电影名称中包含的URL,点击每部电影名称进入其对应的页面。因为这个URL是相对地址,所以必须转换成绝对地址。图1二季度上映的电影页面接下来是每部电影信息页面的阅读。defspider(url):#这个函数主要用来读取每个电影页面中的电影评分信息rsp=requests.get(url,headersheaders=headers)#读取每个电影的网页text=rsp.text#获取页面代码html=etree.HTML(text)genre=html.xpath(r'//div/span[text()="Genres"]/following-sibling::span[1]/text()')[0]#Read电影评分信息title=html.xpath(r'//div/h1/text()')[0]#读取电影名称genres_dict[title]=genre#保存每部电影的名称和评分信息进入函数dictionary就是读取每个电影信息页的信息。它的功能类似于上面读取url页面的功能。很简单,也不多说了。在每个电影页面上,我们要阅读的每部电影的评级信息都在Genres行中。例如,在图2中的电影TheWretched中,它的Genres信息是Horror。图2.示例电影信息页面后跟时间测量。normal_start=time.time()#programstarttimeforuinmovie_urls:spider(u)normal_end=time.time()#programendtimenormal_elapse=normal_end-normal_start#programrunningtimeprint('Thenormalprocedurecosts%sseconds'%normal_elapse)我们测量time使用time.time()方法将结束时间减去开始时间,得到程序的运行时间。这里主要是多次测试spider函数的运行时间。结果这个过程用了59.6188秒。第二个爬虫是使用gevent但没有使用monkeypatch的爬虫。其完整代码如下。importtimefrommlxmlimportetreeimportgeventimportrequestsurl=r'https://www.boxofficemojo.com/quarter/q2/2020/?grossesOption=totalGrosses'headers={'User-Agent':'Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/78.0.3904.108Safari/537.36'}rsp=requests.get(url,headers=headers)text=rsp.texthtml=etree.HTML(text)movie_relative_urls=html.xpath(r'//td[@class="a-text-leftmojo-field-type-releasemojo-cell-wide"]/a/@href')movie_urls=[r'https://www.boxofficemojo.com'+uforuinmovie_relative_urls]genres_dict={}task_list=[]#用于存储协程的列表defspider(url):rsp=requests.get(url,headersheaders=headers)text=rsp.texthtml=etree.HTML(text)genre=html.xpath(r'//div/span[text()="Genres"]/following-sibling::span[1]/text()')[0]title=html.xpath(r'//div/h1/text()')[0]genres_dict[title]=genrerevent_start=time.time()foruinmovie_urls:task=gevent.spawn(spider,u)#生成协程task_list.append(task)#把协程放到这个list中gevent.joinall(task_list)#Run所有corou齿gevent_end=time.time()gevent_elapse=gevent_end-gevent_startprint('Thegeventspidercosts%sseconds'%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和monkeypatch的爬虫。这里我就不贴代码了,因为它的代码和第二个爬虫几乎一模一样。只有一点不同,就是多了一行代码fromgeventimportmonkey;monkey.patch_all(),注意这是一行代码,但是里面有两条语句,用分号放在一起。最重要的是,这行代码要放在所有代码的前面,切记!!!这个爬虫的运行结果是26.9184秒。作者将这里的三个爬虫分别放在三个文件中,分别命名为normal_spider.py、gevent_spider_no.py和gevent_spider.py,分别代表不使用gevent的普通爬虫、使用gevent但不使用monkeypatch的爬虫、使用gevent的爬虫和猴子补丁爬虫。这里要注意一点,monkeypatch目前不支持jupyternotebooks,所以这三个程序必须在命令行中使用,不能在notebooks中使用。最后将三个爬虫的结果总结如下。图3.三个爬虫的结果对比。可以看出,使用gevent而不使用monkeypatch的爬虫运行时间与普通爬虫几乎持平。使用monkeypatch后,运行时间不到之前程序的一半,速度提升了120%左右。仅仅一行代码就带来如此大的速度提升。可见monkeypatch还是很有效的。前两个爬虫的速度几乎是一样的。笔者认为原因在于这两个程序都是单线程的,本质上没有太大区别。同时,阅读的网页数量很少(只有18个网页),而且很难看。gevent的效果。从这个例子可以看出monkeypatch还是有很多提升的,但是gevent目前只对常用库有patch效果,尤其是官方标准库。其他第三方库的效果还不得而知,所以monkeypatch的使用还是要看情况。本文作者将代码放在了gitee代码网站,网址为https://gitee.com/leonmovie/speed-up-gevent-spider-with-monkey-patch,有需要的可以自行下载。