问题:我给你10个图片的url,你帮我去把10张图片下载。
以前的你
urls = ['http://www.xx1.png','http://www.xx1.png','http://www.xx10.png',] for url in urls: response = requests.get(url) with open(url+'.png','wb') as f: f.write(response.content)
上面这种形式可以实现任务,但是效率是非常低的,如果每一个url的io时间为2s,这样就要花费6s,这样不是高效的
下面有几种方案可以实现高性能
1.多线程:
缺点: 线程的利用率不高,每个线程访问一个url以后就闲置了。
""" 多线程 """ import requests import threading urls = [ 'http://www.baidu.com/', 'https://www.cnblogs.com/', 'https://www.cnblogs.com/news/', 'https://cn.bing.com/', 'https://stackoverflow.com/', ] def task(url): response = requests.get(url) print(response) for url in urls: t = threading.Thread(target=task,args=(url,)) t.start()
2.协程:
因为协程遇到io的时候可以进行切换(内部进行切换),提高效率
""" 协程+IO切换 pip3 install gevent gevent内部调用greenlet(实现了协程)。 """ from gevent import monkey; monkey.patch_all() import gevent import requests def func(url): response = requests.get(url) print(response) urls = [ 'http://www.baidu.com/', 'https://www.cnblogs.com/', 'https://www.cnblogs.com/news/', 'https://cn.bing.com/', 'https://stackoverflow.com/', ] spawn_list = [] for url in urls: #在这里没有发送请求,只是都放在一个list中。 spawn_list.append(gevent.spawn(func, url)) #发起请求;遇到io就进行切换 gevent.joinall(spawn_list)
3.基于事件循环异步非阻塞的模块(该模块内部利用了IO多路复用。)Twisted
""" 基于事件循环的异步非阻塞模块:Twisted """ from twisted.web.client import getPage, defer from twisted.internet import reactor def stop_loop(arg): reactor.stop() def get_response(contents): print(contents) deferred_list = [] url_list = [ 'http://www.baidu.com/', 'https://www.cnblogs.com/', 'https://www.cnblogs.com/news/', 'https://cn.bing.com/', 'https://stackoverflow.com/', ] for url in url_list: deferred = getPage(bytes(url, encoding='utf8')) deferred.addCallback(get_response) deferred_list.append(deferred) dlist = defer.DeferredList(deferred_list) dlist.addBoth(stop_loop) reactor.run()
问题:基于事件循环的异步非阻塞模块内部是如何实现?利用了IO多路复用。
- 爬虫本质上就是写socket
- 非阻塞的好处? 发请求的时候不再等待了。
内部实现的机制:IO多路复用。
import socket import select class ChunSheng(object): def __init__(self): self.socket_list = [] self.conn_list = [] self.conn_func_dict = {} # url_func[0] url; url_func--(url,func_url) def add_request(self, url_func): conn = socket.socket() conn.setblocking(False) try: conn.connect((url_func[0], 80)) except BlockingIOError as e: pass self.conn_func_dict[conn] = url_func[1] self.socket_list.append(conn) self.conn_list.append(conn) def run(self): """ 检测self.socket_list中的5个socket对象是否连接成功 :return: """ while True: # select.select # 第一个参数: 用于检测其中socket是否已经获取到响应内容 # 第二个参数: 用于检测其中socket是否已经连接成功 # 第一个返回值 r:具体是那一个socket获取到结果 # 第二个返回值 w:具体是那一个socket连接成功 r, w, e = select.select(self.socket_list, self.conn_list, [], 0.05) for sock in w: # [socket1,socket2] sock.send(b'GET / http1.1\r\nhost:xxxx.com\r\n\r\n') self.conn_list.remove(sock) for sock in r: data = sock.recv(8096) func = self.conn_func_dict[sock] func(data) sock.close() self.socket_list.remove(sock) if not self.socket_list: break
什么是异步?
起始就是回调,当一个任务完成后自动执行某个函数。
我们接触:
- 爬虫:下载完成后自动执行回调函数
- ajax:向后台发送请求,请求完成后执行回调函数。
- 什么是非阻塞?
其实就是不等待,socket中如何设置setblocing(False)那么socket就不再阻塞。
- IO多路复用的作用?
监听socket的状态:
- 是否连接成功
- 是否获取结果
IO多路复用的实现:
- select,只能监听1024个socket;内部会循环所有的socket去检测;
- poll,无个数限制,内部会循环所有的socket去检测;
- epoll,无个数限制,回调。
方案2和3效果差不多,只是切换的角度不一样。协程是内部进行切换的;而基于事件循环异步非阻塞的模块是外人(以上帝视角)进行调配的;