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字符串的encode与decode研究心得乱码问题解决方法
Mar 23 Python
python求pi的方法
Oct 08 Python
python网络编程之文件下载实例分析
May 20 Python
python编写弹球游戏的实现代码
Mar 12 Python
Python实现合并同一个文件夹下所有PDF文件的方法示例
Apr 28 Python
Python基于property实现类的特性操作示例
Jun 15 Python
Python Web编程之WSGI协议简介
Jul 18 Python
python 判断参数为Nonetype类型或空的实例
Oct 30 Python
Python学习笔记之函数的定义和作用域实例详解
Aug 13 Python
Python魔术方法专题
Jun 19 Python
Python切片列表字符串如何实现切换
Aug 06 Python
python爬虫---requests库的用法详解
Sep 28 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.ini实现Mysql导入数据库文件最大限制的修改方法
2007/12/11 PHP
PHP通用分页类page.php[仿google分页]
2008/08/31 PHP
php 将bmp图片转为jpg等其他任意格式的图片
2009/06/21 PHP
浅谈PDO的rowCount函数
2015/06/18 PHP
PHP实现的json类实例
2015/07/28 PHP
PHP使用http_build_query()构造URL字符串的方法
2016/04/02 PHP
js 鼠标拖动对象 可让任何div实现拖动效果
2009/11/09 Javascript
在JavaScript中获取请求的URL参数
2010/12/22 Javascript
jQuery简单实现banner图片切换
2014/01/02 Javascript
Jquery实现鼠标移动放大图片功能实例
2015/03/25 Javascript
jQuery实现在下拉列表选择时获取json数据的方法
2015/04/16 Javascript
详解AngularJS中的作用域
2015/06/17 Javascript
jQuery 常用代码集锦(必看篇)
2016/05/16 Javascript
canvas学习之API整理笔记(二)
2016/12/29 Javascript
Angular4 中常用的指令入门总结
2017/06/12 Javascript
微信小程序picker组件下拉框选择input输入框的实例
2017/09/20 Javascript
Vue组件之极简的地址选择器的实现
2018/05/31 Javascript
详解vue-video-player使用心得(兼容m3u8)
2019/08/23 Javascript
在layui tab控件中载入外部html页面的方法
2019/09/04 Javascript
Vue过滤器,生命周期函数和vue-resource简单介绍
2021/01/12 Vue.js
基于python 处理中文路径的终极解决方法
2018/04/12 Python
selenium + python 获取table数据的示例讲解
2018/10/13 Python
简单了解python PEP的一些知识
2019/07/13 Python
解决python多线程报错:AttributeError: Can't pickle local object问题
2020/04/08 Python
python3中datetime库,time库以及pandas中的时间函数区别与详解
2020/04/16 Python
CSS3 圆角效果
2009/07/15 HTML / CSS
Html5实现二维码扫描并解析
2016/01/20 HTML / CSS
GWT (Google Web Toolkit)有哪些主要的原件组成?
2015/06/08 面试题
求职信的要素有哪些呢
2013/12/26 职场文书
银行营业厅大堂经理岗位职责
2014/01/06 职场文书
《小小竹排画中游》教学反思
2014/02/26 职场文书
培训讲师岗位职责
2014/04/13 职场文书
中小学校园安全广播稿
2014/09/29 职场文书
五年级小学生评语
2014/12/26 职场文书
15个值得收藏的JavaScript函数
2021/09/15 Javascript
2022漫威和DC电影上映作品
2022/04/05 欧美动漫