问题:我给你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
host:xxxx.com
')
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效果差不多,只是切换的角度不一样。协程是内部进行切换的;而基于事件循环异步非阻塞的模块是外人(以上帝视角)进行调配的;