asynicio模块以及爬虫应用asynicio模块(高性能爬虫)

一、背景知识

爬虫的本质就是一个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. 1 #IO密集型程序应该用多线程,所以此时我们使用线程池
  2. 2 import requests
  3. 3 from threading import current_thread
  4. 4 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
  5. 5
  6. 6 def parse_page(res):
  7. 7 res=res.result()
  8. 8 print('%s 解析 %s' %(current_thread().getName(),len(res)))
  9. 9
  10. 10 def get_page(url):
  11. 11 print('%s 下载 %s' %(current_thread().getName(),url))
  12. 12 response=requests.get(url)
  13. 13 if response.status_code == 200:
  14. 14 return response.text
  15. 15
  16. 16 if __name__ == '__main__':
  17. 17 urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
  18. 18
  19. 19 pool=ThreadPoolExecutor(50)
  20. 20 # pool=ProcessPoolExecutor(50)
  21. 21 for url in urls:
  22. 22 pool.submit(get_page,url).add_done_callback(parse_page)
  23. 23
  24. 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. 1 import requests
  2. 2 import asyncio
  3. 3 import uuid
  4. 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. 5
  6. 6 def parse_page(res):
  7. 7 with open("%s.html"%uuid.uuid1(),"wb") as f:
  8. 8 f.write(res)
  9. 9
  10. 10 def get_pager(host,port=80,url="/",ssl=False,callback=parse_page):
  11. 11
  12. 12 #1、建立连接
  13. 13 if ssl:
  14. 14 port = 443
  15. 15 print("下载:https:%s:%s:%s"%(host,port,url))
  16. 16 recv,send = yield from asyncio.open_connection(host=host,port=port,ssl=ssl)
  17. 17
  18. 18 #2、封装请求头
  19. 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. 20 request_headers=request_headers.encode('utf-8')
  21. 21
  22. 22 #3、发送请求头
  23. 23 send.write(request_headers) #套接字不能发字符串,要发bytes
  24. 24 yield from send.drain() # 发送请求头 #遇到IO就切,并且保存状态
  25. 25 #4、接收响应头
  26. 26 # recv.read() # 接收全部的,但是不能区分响应头和响应体
  27. 27 # recv.readline() # 一次收一行,但是你也不确定一次收几行,所以搞个循环
  28. 28 while True:
  29. 29 line = yield from recv.readline()
  30. 30 if line == b'\r\n': # 最后一行是\r\n,就结束了
  31. 31 break
  32. 32 #5、接受响应体
  33. 33 text = yield from recv.read()
  34. 34 #6、调用回调函数,完成解析功能
  35. 35 #看一下效果,保存起来,吧返回的值给一个回调函数
  36. 36 callback(text)
  37. 37 #7、关闭连接
  38. 38 send.close()
  39. 39 # 三次握手建立好之后,一定是四次之后才断开连接
  40. 40 # 发送端决定接受数据的什么时候关闭,
  41. 41 # 没有recv.close()
  42. 42
  43. 43 if __name__ == '__main__':
  44. 44 tasks = [
  45. 45 get_pager(host='www.baidu.com', url='/s?wd=唐诗三百首', ssl=True),
  46. 46 get_pager(host='www.cnblogs.com', url='/haiyan123/p/7445542.html', ssl=True)
  47. 47 ]
  48. 48 loop = asyncio.get_event_loop()
  49. 49 loop.run_until_complete(asyncio.wait(tasks))
  50. 50 loop.close()

爬虫应用asyncio模块

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

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

asyncio+aiohttp

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

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

asyncio+requests模块的方法

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

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

gevent+requests

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

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

grequests

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

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

twisted

8、tornado

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

Tornado

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

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

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

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

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

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

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

  4. 如何为编程爱好者设计一款好玩的智能硬件(八)——LCD1602点阵字符型液晶显示模块驱动封装(中)

    六.温湿度传感器DHT11驱动封装(下):如何为编程爱好者设计一款好玩的智能硬件(六)——初尝试·把温湿度给收集了(下)! 七.点阵字符型液晶显示模块LCD1602驱动封装(上):如何为编程爱好者设计 ...

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

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

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

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

  7. Python爬虫之urllib模块2

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

  8. Python爬虫之urllib模块1

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

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

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

随机推荐

  1. Selenium 2自动化测试实战19(下载文件)

    一.下载文件 webDriver允许设置默认的文件下载路径,也就是说,文件会自动下载并且存放到设置的目录中.下面以火狐浏览器为例,执行文件的下载. #downfile.py # -*- coding: ...

  2. 阶段3 2.Spring_08.面向切面编程 AOP_3 spring基于XML的AOP-编写必要的代码

    新建项目 先改打包方式 导包,就先导入这俩包的坐标 aspectjweaver为了解析切入点表达式 新建业务层接口 定义三个方法 看返回和参数的区别.为了把这三类方法表现出来,并不局限于方法干什么事 ...

  3. delphi TDbGrid 右键 PopupMenu 菜单只在有数据的地方弹出

    最近用delphi做开发,用到了DbGrid控件,想在控件上点击鼠标右键弹出菜单 关联DbGrid的 Popupmenu 倒是可以实现,但是这样的效果是不管你在哪里单击鼠标右键 只要在DBGrid里面 ...

  4. java:LeakFilling(Springmvc)

    1.后台可以同时多个对象接收前端页面的值:(如图两个都打印了) 2.参数绑定的注解,通过该注解可以解决参数名称与controller中形参名称不一致的问题: @RequestParam(name=&q ...

  5. beego框架学习(二) -路由设置

    路由设置 什么是路由设置呢?前面介绍的 MVC 结构执行时,介绍过 beego 存在三种方式的路由:固定路由.正则路由.自动路由,接下来详细的讲解如何使用这三种路由. 基础路由 从beego1.2版本 ...

  6. CTF—WEB—sql注入之无过滤有回显最简单注入

    sql注入基础原理 一.Sql注入简介 Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手 ...

  7. Java锁机制ReentrantLock

    ReentrantLock 锁常用于保证程序的人为顺序执行. 写一个类模拟ReentrantLock类的功能 class MyLock{ private boolean lock = false; p ...

  8. 前端,后端,UI,UE,UX,区别到底在哪里?

    前端后端,到低区别在哪里? 其实后端是负责更为复杂的数据逻辑,表处理结构,如何实现一连串的数据提交,包括,数据验证,数据影响,数据计算,数据提取,,,等等. 那么前端负责的是什么呢?数据展示,数据验证 ...

  9. 常用邮件SMTP POP3服务器地址大全

    #阿里云邮箱(mail.aliyun.com): POP3服务器地址:pop3.aliyun.com(SSL加密端口:995:非加密端口:110) SMTP服务器地址:smtp.aliyun.com( ...

  10. Kick Start 2019 Round D

    X or What? 符号约定: $\xor$ 表示异或. popcount($x$) 表示非负整数 $x$ 的二进制表示里数字 1 出现的次数.例如,$13 = 1101_2$,则 popcount ...