Python并发爬虫常用实现方法解析


Posted in Python onNovember 19, 2020

在进行单个爬虫抓取的时候,我们不可能按照一次抓取一个url的方式进行网页抓取,这样效率低,也浪费了cpu的资源。目前python上面进行并发抓取的实现方式主要有以下几种:进程,线程,协程。进程不在的讨论范围之内,一般来说,进程是用来开启多个spider,比如我们开启了4进程,同时派发4个spider进行网络抓取,每个spider同时抓取4个url。

所以,我们今天讨论的是,在单个爬虫的情况下,尽可能的在同一个时间并发抓取,并且抓取的效率要高。

一.顺序抓取

顺序抓取是最最常见的抓取方式,一般初学爬虫的朋友就是利用这种方式,下面是一个测试代码,顺序抓取8个url,我们可以来测试一下抓取完成需要多少时间:

HEADERS = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',         
  'Accept-Language': 'zh-CN,zh;q=0.8',                            
  'Accept-Encoding': 'gzip, deflate',}                            
URLS = ['http://www.cnblogs.com/moodlxs/p/3248890.html',                   
    'https://www.zhihu.com/topic/19804387/newest',                    
    'http://blog.csdn.net/yueguanghaidao/article/details/24281751',            
    'https://my.oschina.net/visualgui823/blog/36987',                   
    'http://blog.chinaunix.net/uid-9162199-id-4738168.html',               
    'http://www.tuicool.com/articles/u67Bz26',                      
    'http://rfyiamcool.blog.51cto.com/1030776/1538367/',                 
    'http://itindex.net/detail/26512-flask-tornado-gevent']                
                                               
#url为随机获取的一批url                                        
                                               
def func():                                          
  """                                            
  顺序抓取                                           
  """                                            
  import requests                                      
  import time                                        
  urls = URLS                                        
  headers = HEADERS                                     
  headers['user-agent'] = "Mozilla/5.0+(Windows+NT+6.2;+WOW64)+AppleWebKit/537" \      
              ".36+(KHTML,+like+Gecko)+Chrome/45.0.2454.101+Safari/537.36"   
  print(u'顺序抓取')                                      
  starttime= time.time()                                  
  for url in urls:                                     
    try:                                         
      r = requests.get(url, allow_redirects=False, timeout=2.0, headers=headers)    
    except:                                        
      pass                                       
    else:                                         
      print(r.status_code, r.url)                            
  endtime=time.time()                                    
  print(endtime-starttime)                                 
                                               
func()

我们直接采用内建的time.time()来计时,较为粗略,但可以反映大概的情况。下面是顺序抓取的结果计时:

Python并发爬虫常用实现方法解析

可以从图片中看到,显示的顺序与urls的顺序是一模一样的,总共耗时为7.763269901275635秒,一共8个url,平均抓取一个大概需要0.97秒。总体来看,还可以接受。

二.多线程抓取

线程是python内的一种较为不错的并发方式,我们也给出相应的代码,并且为每个url创建了一个线程,一共8线程并发抓取,下面的代码:

下面是我们运行8线程的测试代码:

HEADERS = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',               
  'Accept-Language': 'zh-CN,zh;q=0.8',                                  
  'Accept-Encoding': 'gzip, deflate',}                                  
URLS = ['http://www.cnblogs.com/moodlxs/p/3248890.html',                          
    'https://www.zhihu.com/topic/19804387/newest',                           
    'http://blog.csdn.net/yueguanghaidao/article/details/24281751',                  
    'https://my.oschina.net/visualgui823/blog/36987',                         
    'http://blog.chinaunix.net/uid-9162199-id-4738168.html',                      
    'http://www.tuicool.com/articles/u67Bz26',                             
    'http://rfyiamcool.blog.51cto.com/1030776/1538367/',                        
    'http://itindex.net/detail/26512-flask-tornado-gevent']                      
                                                      
def thread():                                               
  from threading import Thread                                      
  import requests                                            
  import time                                              
  urls = URLS                                              
  headers = HEADERS                                           
  headers['user-agent'] = "Mozilla/5.0+(Windows+NT+6.2;+WOW64)+AppleWebKit/537.36+" \          
              "(KHTML,+like+Gecko)+Chrome/45.0.2454.101+Safari/537.36"            
  def get(url):                                             
    try:                                                
      r = requests.get(url, allow_redirects=False, timeout=2.0, headers=headers)           
    except:                                              
      pass                                              
    else:                                               
      print(r.status_code, r.url)                                  
                                                      
  print(u'多线程抓取')                                            
  ts = [Thread(target=get, args=(url,)) for url in urls]                         
  starttime= time.time()                                         
  for t in ts:                                              
    t.start()                                             
  for t in ts:                                              
    t.join()                                              
  endtime=time.time()                                          
  print(endtime-starttime)                                        
thread()

多线程抓住的时间如下:

Python并发爬虫常用实现方法解析

可以看到相较于顺序抓取,8线程的抓取效率明显上升了3倍多,全部完成只消耗了2.154秒。可以看到显示的结果已经不是urls的顺序了,说明每个url各自完成的时间都是不一样的。线程就是在一个进程中不断的切换,让每个线程各自运行一会,这对于网络io来说,性能是非常高的。但是线程之间的切换是挺浪费资源的。

三.gevent并发抓取

gevent是一种轻量级的协程,可用它来代替线程,而且,他是在一个线程中运行,机器资源的损耗比线程低很多。如果遇到了网络io阻塞,会马上切换到另一个程序中去运行,不断的轮询,来降低抓取的时间
下面是测试代码:

HEADERS = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',
  'Accept-Language': 'zh-CN,zh;q=0.8',
  'Accept-Encoding': 'gzip, deflate',}

URLS = ['http://www.cnblogs.com/moodlxs/p/3248890.html',
    'https://www.zhihu.com/topic/19804387/newest',
    'http://blog.csdn.net/yueguanghaidao/article/details/24281751',
    'https://my.oschina.net/visualgui823/blog/36987',
    'http://blog.chinaunix.net/uid-9162199-id-4738168.html',
    'http://www.tuicool.com/articles/u67Bz26',
    'http://rfyiamcool.blog.51cto.com/1030776/1538367/',
    'http://itindex.net/detail/26512-flask-tornado-gevent']

def main():
  """
  gevent并发抓取
  """
  import requests
  import gevent
  import time

  headers = HEADERS
  headers['user-agent'] = "Mozilla/5.0+(Windows+NT+6.2;+WOW64)+AppleWebKit/537.36+" \
              "(KHTML,+like+Gecko)+Chrome/45.0.2454.101+Safari/537.36"
  urls = URLS
  def get(url):
    try:
      r = requests.get(url, allow_redirects=False, timeout=2.0, headers=headers)
    except:
      pass
    else:
      print(r.status_code, r.url)

  print(u'基于gevent的并发抓取')
  starttime= time.time()
  g = [gevent.spawn(get, url) for url in urls]
  gevent.joinall(g)
  endtime=time.time()
  print(endtime - starttime)
main()

协程的抓取时间如下:

Python并发爬虫常用实现方法解析

正常情况下,gevent的并发抓取与多线程的消耗时间差不了多少,但是可能是我网络的原因,或者机器的性能的原因,时间有点长......,请各位小主在自己电脑进行跑一下看运行时间

四.基于tornado的coroutine并发抓取

tornado中的coroutine是python中真正意义上的协程,与python3中的asyncio几乎是完全一样的,而且两者之间的future是可以相互转换的,tornado中有与asyncio相兼容的接口。
下面是利用tornado中的coroutine进行并发抓取的代码:

利用coroutine编写并发略显复杂,但这是推荐的写法,如果你使用的是python3,强烈建议你使用coroutine来编写并发抓取。

下面是测试代码:

HEADERS = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9',
  'Accept-Language': 'zh-CN,zh;q=0.8',
  'Accept-Encoding': 'gzip, deflate',}

URLS = ['http://www.cnblogs.com/moodlxs/p/3248890.html',
    'https://www.zhihu.com/topic/19804387/newest',
    'http://blog.csdn.net/yueguanghaidao/article/details/24281751',
    'https://my.oschina.net/visualgui823/blog/36987',
    'http://blog.chinaunix.net/uid-9162199-id-4738168.html',
    'http://www.tuicool.com/articles/u67Bz26',
    'http://rfyiamcool.blog.51cto.com/1030776/1538367/',
    'http://itindex.net/detail/26512-flask-tornado-gevent']
import time
from tornado.gen import coroutine
from tornado.ioloop import IOLoop
from tornado.httpclient import AsyncHTTPClient, HTTPError
from tornado.httpclient import HTTPRequest

#urls与前面相同
class MyClass(object):

  def __init__(self):
    #AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
    self.http = AsyncHTTPClient()

  @coroutine
  def get(self, url):
    #tornado会自动在请求首部带上host首部
    request = HTTPRequest(url=url,
              method='GET',
              headers=HEADERS,
              connect_timeout=2.0,
              request_timeout=2.0,
              follow_redirects=False,
              max_redirects=False,
              user_agent="Mozilla/5.0+(Windows+NT+6.2;+WOW64)+AppleWebKit/537.36+\
              (KHTML,+like+Gecko)+Chrome/45.0.2454.101+Safari/537.36",)
    yield self.http.fetch(request, callback=self.find, raise_error=False)

  def find(self, response):
    if response.error:
      print(response.error)
    print(response.code, response.effective_url, response.request_time)


class Download(object):

  def __init__(self):
    self.a = MyClass()
    self.urls = URLS

  @coroutine
  def d(self):
    print(u'基于tornado的并发抓取')
    starttime = time.time()
    yield [self.a.get(url) for url in self.urls]
    endtime=time.time()
    print(endtime-starttime)

if __name__ == '__main__':
  dd = Download()
  loop = IOLoop.current()
  loop.run_sync(dd.d)

抓取的时间如下:

Python并发爬虫常用实现方法解析

可以看到总共花费了128087秒,而这所花费的时间恰恰就是最后一个url抓取所需要的时间,tornado中自带了查看每个请求的相应时间。我们可以从图中看到,最后一个url抓取总共花了1.28087秒,相较于其他时间大大的增加,这也是导致我们消耗时间过长的原因。那可以推断出,前面的并发抓取,也在这个url上花费了较多的时间。

总结:

以上测试其实非常的不严谨,因为我们选取的url的数量太少了,完全不能反映每一种抓取方式的优劣。如果有一万个不同的url同时抓取,那么记下总抓取时间,是可以得出一个较为客观的结果的。

并且,已经有人测试过,多线程抓取的效率是远不如gevent的。所以,如果你使用的是python2,那么我推荐你使用gevent进行并发抓取;如果你使用的是python3,我推荐你使用tornado的http客户端结合coroutine进行并发抓取。从上面的结果来看,tornado的coroutine是高于gevent的轻量级的协程的。但具体结果怎样,我没测试过。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python避免死锁方法实例分析
Jun 04 Python
Python中利用Scipy包的SIFT方法进行图片识别的实例教程
Jun 03 Python
Python中列表和元组的使用方法和区别详解
Dec 30 Python
Python中模块与包有相同名字的处理方法
May 05 Python
Python对字符串实现去重操作的方法示例
Aug 11 Python
浅谈PyQt5 的帮助文档查找方法,可以查看每个类的方法
Jun 25 Python
python自动化UI工具发送QQ消息的实例
Aug 27 Python
在Python中画图(基于Jupyter notebook的魔法函数)
Oct 28 Python
python使用 cx_Oracle 模块进行查询操作示例
Nov 28 Python
python实现实时视频流播放代码实例
Jan 11 Python
python跨文件使用全局变量的实现
Nov 17 Python
使用bandit对目标python代码进行安全函数扫描的案例分析
Jan 27 Python
python实现文件分片上传的接口自动化
Nov 19 #Python
Python类class参数self原理解析
Nov 19 #Python
Python爬虫如何破解JS加密的Cookie
Nov 19 #Python
python制作一个简单的gui 数据库查询界面
Nov 19 #Python
解决python3中os.popen()出错的问题
Nov 19 #Python
Python中return函数返回值实例用法
Nov 19 #Python
python 三种方法实现对Excel表格的读写
Nov 19 #Python
You might like
php 无限级分类学习参考之对ecshop无限级分类的解析 带详细注释
2010/03/23 PHP
php获取apk包信息的方法
2014/08/15 PHP
如何让thinkphp在模型中自动完成session赋值小教程
2014/09/05 PHP
在html文件中也可以执行php语句的方法
2015/04/09 PHP
十个PHP高级应用技巧果断收藏
2015/09/25 PHP
php操纵mysqli数据库的实现方法
2016/09/18 PHP
$.ajax json数据传递方法
2008/11/19 Javascript
读jQuery之七 判断点击了鼠标哪个键的代码
2011/06/21 Javascript
让低版本浏览器支持input的placeholder属性(js方法)
2013/04/03 Javascript
Jquery焦点与失去焦点示例应用
2014/06/10 Javascript
jQuery中on()方法用法实例
2015/01/19 Javascript
ajax读取数据后使用jqchart显示图表的方法
2015/06/10 Javascript
nodejs导出excel的方法
2015/06/30 NodeJs
jquery日历插件datepicker用法分析
2016/01/22 Javascript
JS如何设置cookie有效期为当天24点并弹出欢迎登陆界面
2016/08/04 Javascript
Node.js中防止错误导致的进程阻塞的方法
2016/08/11 Javascript
使用jquery给新生的th绑定hover事件的实例
2017/02/10 Javascript
jQuery插件HighCharts绘制2D金字塔图效果示例【附demo源码下载】
2017/03/09 Javascript
创建简单的node服务器实例(分享)
2017/06/23 Javascript
JS数组操作之增删改查的简单实现
2017/08/21 Javascript
requireJS模块化实现返回顶部功能的方法详解
2017/10/16 Javascript
Angular2进阶之如何避免Dom误区
2018/04/02 Javascript
在JavaScript中如何访问暂未存在的嵌套对象
2019/06/18 Javascript
详解Django+Uwsgi+Nginx 实现生产环境部署
2018/11/06 Python
Python基于内置函数type创建新类型
2020/10/22 Python
使用Python实现音频双通道分离
2020/12/25 Python
Python 求向量的余弦值操作
2021/03/04 Python
碧欧泉Biotherm加拿大官方网站:法国高端护肤品牌
2019/10/18 全球购物
大学毕业生通用自我评价
2014/01/05 职场文书
社区助残日活动总结
2014/08/29 职场文书
小学生2014国庆节演讲稿:祖国在我心中
2014/09/21 职场文书
警告通知
2015/04/25 职场文书
社区服务理念口号
2015/12/25 职场文书
创业计划书之都市休闲农庄
2019/12/28 职场文书
海贼王十大潜力果实,路飞仅排第十,第一可毁世界(震震果实)
2022/03/18 日漫
MySQL实战记录之如何快速定位慢SQL
2022/03/23 MySQL