一、背景知识

爬虫的本质就是一个socket客户端与服务端的通信过程,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行,那只能等待爬取一个结束后才能继续下一个,效率会非常低。

需要强调的是:对于单线程下串行N个任务,并不完全等同于低效,如果这N个任务都是纯计算的任务,那么该线程对cpu的利用率仍然会很高,之所以单线程下串行多个爬虫任务低效,是因为爬虫任务是明显的IO密集型程序。

二、同步、异步、回调机制

1、同步调用:即提交一个任务后就在原地等待任务结束,等到拿到任务的结果后再继续下一行代码,效率低下

  1. import requests
  2.  
  3. def parse_page(res):
  4. print('解析 %s' %(len(res)))
  5.  
  6. def get_page(url):
  7. print('下载 %s' %url)
  8. response=requests.get(url)
  9. if response.status_code == 200:
  10. return response.text
  11.  
  12. urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
  13. for url in urls:
  14. res=get_page(url) #调用一个任务,就在原地等待任务结束拿到结果后才继续往后执行
  15. parse_page(res)

2、一个简单的解决方案:多线程或多进程

  1. #在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),
    这样任何一个连接的阻塞都不会影响其他的连接。
  1. #IO密集型程序应该用多线程
  2. import requests
  3. from threading import Thread,current_thread
  4.  
  5. def parse_page(res):
  6. print('%s 解析 %s' %(current_thread().getName(),len(res)))
  7.  
  8. def get_page(url,callback=parse_page):
  9. print('%s 下载 %s' %(current_thread().getName(),url))
  10. response=requests.get(url)
  11. if response.status_code == 200:
  12. callback(response.text)
  13.  
  14. if __name__ == '__main__':
  15. urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
  16. for url in urls:
  17. t=Thread(target=get_page,args=(url,))
  18. t.start()

   该方案的问题是:

  1. 开启多进程或都线程的方式,我们是无法无限制地开启多进程或多线程的:在遇到要同时响应成百上千路的连接请求,
    则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。
    3、改进方案: 线程池或进程池+异步调用:提交一个任务后并不会等待任务结束,而是继续下一行代码
  1. #很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,
    并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。
    这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如webspheretomcat和各种数据库等。
  1. #IO密集型程序应该用多线程,所以此时我们使用线程池
  2. import requests
  3. from threading import current_thread
  4. from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
  5.  
  6. def parse_page(res):
  7. res=res.result()
  8. print('%s 解析 %s' %(current_thread().getName(),len(res)))
  9.  
  10. def get_page(url):
  11. print('%s 下载 %s' %(current_thread().getName(),url))
  12. response=requests.get(url)
  13. if response.status_code == 200:
  14. return response.text
  15.  
  16. if __name__ == '__main__':
  17. urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
  18.  
  19. pool=ThreadPoolExecutor(50)
  20. # pool=ProcessPoolExecutor(50)
  21. for url in urls:
  22. pool.submit(get_page,url).add_done_callback(parse_page)
  23.  
  24. pool.shutdown(wait=True)

进程池或线程池:异步调用+回调机制

    改进后方案其实也存在着问题:

  1. #“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,
    当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,
    并根据响应规模调整“池”的大小

对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。

总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

三、高性能

上述无论哪种解决方案其实没有解决一个性能相关的问题:IO阻塞,无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,程序的执行效率因此就降低了下来。

解决这一问题的关键在于,我们自己从应用程序级别检测IO阻塞然后切换到我们自己程序的其他任务执行,这样把我们程序的IO降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO比较少的程序,从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的

1、在python3.3之后新增了asyncio模块,可以帮我们检测IO(只能是网络IO),实现应用程序级别的切换

  1. import asyncio
  2. #当程序遇到IO的时候不阻塞了,让这个装饰器去检测有没有IO,当有IO的时候提醒一下,切到其他的地方去
  3. @asyncio.coroutine
  4. def task(task_id,seconds):
  5. print("%s is start"%task_id)
  6. yield from asyncio.sleep(seconds) #自动检测IO, #遇到IO就切,并且保存状态
  7. print("%s id end" %task_id)
  8.  
  9. tasks = [
  10. task(task_id="任务1",seconds=3),
  11. task(task_id="任务2",seconds=2),
  12. task(task_id="任务3",seconds=1),
  13. ]
  14. loop = asyncio.get_event_loop() #创建事件循环
  15. loop.run_until_complete(asyncio.wait(tasks)) #运行事件循环,直到任务完成
  16. loop.close() #一旦任务结束,就获取到任务的结果

2、但asyncio模块只能发tcp级别的请求,不能发http协议,因此,在我们需要发送http请求的时候,需要我们自定义http报头

  1. import requests
  2. import asyncio
  3. import uuid
  4. User_Agent='Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
  5.  
  6. def parse_page(res):
  7. with open("%s.html"%uuid.uuid1(),"wb") as f:
  8. f.write(res)
  9.  
  10. def get_pager(host,port=80,url="/",ssl=False,callback=parse_page):
  11.  
  12. #1、建立连接
  13. if ssl:
  14. port = 443
  15. print("下载:https:%s:%s:%s"%(host,port,url))
  16. recv,send = yield from asyncio.open_connection(host=host,port=port,ssl=ssl)
  17.  
  18. #2、封装请求头
  19. request_headers="""GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\n\r\n""" %(url,host,User_Agent)# http / 1.0省去了拼接太多的东西
  20. request_headers=request_headers.encode('utf-8')
  21.  
  22. #3、发送请求头
  23. send.write(request_headers) #套接字不能发字符串,要发bytes
  24. yield from send.drain() # 发送请求头 #遇到IO就切,并且保存状态
  25. #4、接收响应头
  26. # recv.read() # 接收全部的,但是不能区分响应头和响应体
  27. # recv.readline() # 一次收一行,但是你也不确定一次收几行,所以搞个循环
  28. while True:
  29. line = yield from recv.readline()
  30. if line == b'\r\n': # 最后一行是\r\n,就结束了
  31. break
  32. #5、接受响应体
  33. text = yield from recv.read()
  34. #6、调用回调函数,完成解析功能
  35. #看一下效果,保存起来,吧返回的值给一个回调函数
  36. callback(text)
  37. #7、关闭连接
  38. send.close()
  39. # 三次握手建立好之后,一定是四次之后才断开连接
  40. # 发送端决定接受数据的什么时候关闭,
  41. # 没有recv.close()
  42.  
  43. if __name__ == '__main__':
  44. tasks = [
  45. get_pager(host='www.baidu.com', url='/s?wd=唐诗三百首', ssl=True),
  46. get_pager(host='www.cnblogs.com', url='/haiyan123/p/7445542.html', ssl=True)
  47. ]
  48. loop = asyncio.get_event_loop()
  49. loop.run_until_complete(asyncio.wait(tasks))
  50. loop.close()

爬虫应用asyncio模块

3、自定义http报头多少有点麻烦,于是有了aiohttp模块,专门帮我们封装http报头,然后我们还需要用asyncio检测IO实现切换

  1. import aiohttp
  2. import asyncio
  3.  
  4. @asyncio.coroutine
  5. def get_page(url):
  6. print('GET:%s' %url)
  7. response=yield from aiohttp.request('GET',url)
  8.  
  9. data=yield from response.read()
  10.  
  11. print(url,data)
  12. response.close()
  13. return 1
  14.  
  15. tasks=[
  16. get_page('https://www.python.org/doc'),
  17. get_page('https://www.cnblogs.com/linhaifeng'),
  18. get_page('https://www.openstack.org')
  19. ]
  20.  
  21. loop=asyncio.get_event_loop()
  22. results=loop.run_until_complete(asyncio.gather(*tasks))
  23. loop.close()
  24.  
  25. print('=====>',results) #[1, 1, 1]

asyncio+aiohttp

4、此外,还可以将requests.get函数传给asyncio,就能够被检测了

  1. import requests
  2. import asyncio
  3.  
  4. @asyncio.coroutine
  5. def get_page(func,*args):
  6. print('GET:%s' %args[0])
  7. loog=asyncio.get_event_loop()
  8. furture=loop.run_in_executor(None,func,*args)
  9. response=yield from furture
  10.  
  11. print(response.url,len(response.text))
  12. return 1
  13.  
  14. tasks=[
  15. get_page(requests.get,'https://www.python.org/doc'),
  16. get_page(requests.get,'https://www.cnblogs.com/linhaifeng'),
  17. get_page(requests.get,'https://www.openstack.org')
  18. ]
  19.  
  20. loop=asyncio.get_event_loop()
  21. results=loop.run_until_complete(asyncio.gather(*tasks))
  22. loop.close()
  23.  
  24. print('=====>',results) #[1, 1, 1]

asyncio+requests模块的方法

5、还有之前在协程时介绍的gevent模块

  1. from gevent import monkey;monkey.patch_all()
  2. import gevent
  3. import requests
  4.  
  5. def get_page(url):
  6. print('GET:%s' %url)
  7. response=requests.get(url)
  8. print(url,len(response.text))
  9. return 1
  10.  
  11. # g1=gevent.spawn(get_page,'https://www.python.org/doc')
  12. # g2=gevent.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
  13. # g3=gevent.spawn(get_page,'https://www.openstack.org')
  14. # gevent.joinall([g1,g2,g3,])
  15. # print(g1.value,g2.value,g3.value) #拿到返回值
  16.  
  17. #协程池
  18. from gevent.pool import Pool
  19. pool=Pool(2)
  20. g1=pool.spawn(get_page,'https://www.python.org/doc')
  21. g2=pool.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
  22. g3=pool.spawn(get_page,'https://www.openstack.org')
  23. gevent.joinall([g1,g2,g3,])
  24. print(g1.value,g2.value,g3.value) #拿到返回值

gevent+requests

6、封装了gevent+requests模块的grequests模块

  1. #pip3 install grequests
  2.  
  3. import grequests
  4.  
  5. request_list=[
  6. grequests.get('https://wwww.xxxx.org/doc1'),
  7. grequests.get('https://www.cnblogs.com/linhaifeng'),
  8. grequests.get('https://www.openstack.org')
  9. ]
  10.  
  11. ##### 执行并获取响应列表 #####
  12. # response_list = grequests.map(request_list)
  13. # print(response_list)
  14.  
  15. ##### 执行并获取响应列表(处理异常) #####
  16. def exception_handler(request, exception):
  17. # print(request,exception)
  18. print("%s Request failed" %request.url)
  19.  
  20. response_list = grequests.map(request_list, exception_handler=exception_handler)
  21. print(response_list)

grequests

7、twisted:是一个网络框架,其中一个功能是发送异步请求,检测IO并自动切换

  1. from twisted.web.client import getPage,defer
  2. from twisted.internet import reactor
  3. #pip install pypiwin32
  4.  
  5. def all_done(res): #这里的res ,接收的是所有函D:\pywin32+twisted\Twisted-17.9.0-cp36-cp36m-win_amd64.whl数的返回值,
  6. '''等到所有的任务都结束了才触发这个函数'''
  7. print(res) # #打印结果[(回调函数是否抛出异常<True,False>,回调函数的返回值),(),()]
  8. reactor.stop()
  9.  
  10. def callback(res):
  11. print(len(res)) #obj
  12. return 1
  13.  
  14. urls = [
  15. 'http://www.baidu.com',
  16. 'http://www.bing.com',
  17. 'http://www.python.org',
  18. ]
  19. task = []
  20. for url in urls:
  21. obj = getPage(url.encode("utf-8"),) #请求url页面,要传bytes类型的
  22. obj.addCallback(callback) #吧结果给了回调函数
  23. task.append(obj)
  24. # defer.Deferred(task) #创建循环,开始检测IO
  25. defer.DeferredList(task).addBoth(all_done) #给所有任务绑定一个回调函数,等所有的任务都结束了以后关闭连接
  26. reactor.run()

twisted

8、tornado

  1. from tornado.httpclient import AsyncHTTPClient
  2. from tornado.httpclient import HTTPRequest
  3. from tornado import ioloop
  4.  
  5. def handle_response(response):
  6. """
  7. 处理返回值内容(需要维护计数器,来停止IO循环),调用 ioloop.IOLoop.current().stop()
  8. :param response:
  9. :return:
  10. """
  11. if response.error:
  12. print("Error:", response.error)
  13. else:
  14. print(response.body)
  15.  
  16. def func():
  17. url_list = [
  18. 'http://www.baidu.com',
  19. 'http://www.bing.com',
  20. ]
  21. for url in url_list:
  22. print(url)
  23. http_client = AsyncHTTPClient()
  24. http_client.fetch(HTTPRequest(url), handle_response)
  25.  
  26. ioloop.IOLoop.current().add_callback(func)
  27. ioloop.IOLoop.current().start()
  28.  
  29. #发现上例在所有任务都完毕后也不能正常结束,为了解决该问题,让我们来加上计数器
  30. from tornado.httpclient import AsyncHTTPClient
  31. from tornado.httpclient import HTTPRequest
  32. from tornado import ioloop
  33.  
  34. count=0
  35.  
  36. def handle_response(response):
  37. """
  38. 处理返回值内容(需要维护计数器,来停止IO循环),调用 ioloop.IOLoop.current().stop()
  39. :param response:
  40. :return:
  41. """
  42. if response.error:
  43. print("Error:", response.error)
  44. else:
  45. print(len(response.body))
  46.  
  47. global count
  48. count-=1 #完成一次回调,计数减1
  49. if count == 0:
  50. ioloop.IOLoop.current().stop()
  51.  
  52. def func():
  53. url_list = [
  54. 'http://www.baidu.com',
  55. 'http://www.bing.com',
  56. ]
  57.  
  58. global count
  59. for url in url_list:
  60. print(url)
  61. http_client = AsyncHTTPClient()
  62. http_client.fetch(HTTPRequest(url), handle_response)
  63. count+=1 #计数加1
  64.  
  65. ioloop.IOLoop.current().add_callback(func)
  66. ioloop.IOLoop.current().start()

Tornado

asynicio模块以及爬虫应用asynicio模块(高性能爬虫)的更多相关文章

  1. 八、asynicio模块以及爬虫应用asynicio模块(高性能爬虫)

    asynicio模块以及爬虫应用asynicio模块(高性能爬虫) 一.背景知识 爬虫的本质就是一个socket客户端与服务端的通信过程,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行, ...

  2. 高性能爬虫——asynicio模块

      一 背景知识 爬虫的本质就是一个socket客户端与服务端的通信过程,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行,那只能等待爬取一个结束后才能继续下一个,效率会非常低. 需要强调 ...

  3. python爬虫主要就是五个模块:爬虫启动入口模块,URL管理器存放已经爬虫的URL和待爬虫URL列表,html下载器,html解析器,html输出器 同时可以掌握到urllib2的使用、bs4(BeautifulSoup)页面解析器、re正则表达式、urlparse、python基础知识回顾(set集合操作)等相关内容。

    本次python爬虫百步百科,里面详细分析了爬虫的步骤,对每一步代码都有详细的注释说明,可通过本案例掌握python爬虫的特点: 1.爬虫调度入口(crawler_main.py) # coding: ...

  4. 【爬虫入门手记03】爬虫解析利器beautifulSoup模块的基本应用

    [爬虫入门手记03]爬虫解析利器beautifulSoup模块的基本应用 1.引言 网络爬虫最终的目的就是过滤选取网络信息,因此最重要的就是解析器了,其性能的优劣直接决定这网络爬虫的速度和效率.Bea ...

  5. Python爬虫之urllib模块2

    Python爬虫之urllib模块2 本文来自网友投稿 作者:PG-55,一个待毕业待就业的二流大学生. 看了一下上一节的反馈,有些同学认为这个没什么意义,也有的同学觉得太简单,关于Beautiful ...

  6. Python爬虫之urllib模块1

    Python爬虫之urllib模块1 本文来自网友投稿.作者PG,一个待毕业待就业二流大学生.玄魂工作室未对该文章内容做任何改变. 因为本人一直对推理悬疑比较感兴趣,所以这次爬取的网站也是平时看一些悬 ...

  7. Python爬虫与数据分析之模块:内置模块、开源模块、自定义模块

    专栏目录: Python爬虫与数据分析之python教学视频.python源码分享,python Python爬虫与数据分析之基础教程:Python的语法.字典.元组.列表 Python爬虫与数据分析 ...

  8. 04.Python网络爬虫之requests模块(1)

    引入 Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用. 警告:非专业使用其他 HTTP 库会导致危险的副作用,包括:安全缺陷症.冗余代码症.重新发明轮子症.啃文档 ...

  9. 洗礼灵魂,修炼python(53)--爬虫篇—urllib模块

    urllib 1.简介: urllib 模块是python的最基础的爬虫模块,其核心功能就是模仿web浏览器等客户端,去请求相应的资源,并返回一个类文件对象.urllib 支持各种 web 协议,例如 ...

随机推荐

  1. Ant使用指南

    ant 使用指南  参考:http://www.cnblogs.com/hoojo/archive/2013/06/14/java_ant_project_target_task_run.html 一 ...

  2. SpringBoot - 添加定时任务

    SpringBoot 添加定时任务 EXample1: import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spri ...

  3. linux学习记录.6.vscode调试c makefile

    参考 https://www.cnblogs.com/lidabo/p/5888997.html task有更新,不能使用文章的代码. 多文件 终端 touch main.c hw.c hw.h vs ...

  4. Eclipse安装lombok及常用注解

    转自:https://blog.csdn.net/ZJDWHD/article/details/77795023 lombok的官方网址:http://projectlombok.org/ https ...

  5. printf是在libc库中么?

    libc中果然有很多的函数,使用nm看了一下,里面竟然还有reboot函数,汗! 使用grep,可以看到各种 printf 也都在这里头. objdump是看函数的地址与函数名对应的,虽然也能证明pr ...

  6. jsonp实现下拉搜索

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  7. 省电优化之WakeLock

    https://blog.csdn.net/qfanmingyiq/article/details/54587664 boolean onStopJob (JobParameters params) ...

  8. 第二节,surf特征检测关键点,实现图片拼接

    初级的图像拼接为将两幅图像简单的粘贴在一起,仅仅是图像几何空间的转移和合成,与图像内容无关:高级图像拼接也叫做基于特征匹配的图像拼接,拼接时消去两幅图像相同的部分,实现拼接全景图. 实现步骤: 1.采 ...

  9. Trickbot增加的远程应用程序凭证抓取功能

    来源 https://blog.trendmicro.com/trendlabs-security-intelligence/trickbot-adds-remote-application-cred ...

  10. LwIP Application Developers Manual5---高层协议之DNS

    1.前言 lwIP提供一个基本的DNS客户端(1.3.0后引进),通过使用DNS(Domain Name System)协议来允许应用程序解决主机名到地址的转换. 在文件lwipopts.h里面定义L ...