爬虫中一个很重要的性能就是爬取速度
假设我们要同时爬取好几个网站的内容,在一个线程中用requests模块实现的都是串行:给一个网站发送连接后,等待连接成功,这里是一个阻塞点,然后等待网站发送数据后来,我才能接收完数据发送另一个请求,这又是一个阻塞点,这样速度就会很慢
我们能够通过创建多进程或是多线程来解决上面的问题,但也会带来资源浪费的问题
除此之外,有很多的框架可以帮我们解决这个问题,比如gevent,Twsited,Tornado等,它们都是在一个线程中实现了异步IO,达到提高爬取速度的效果
那异步IO是什么呢?
首先我们爬取网站,发送的都是http请求,http请求基于Tcp,两者都是通过socket发送请求,只是发送数据的格式不同
tcp可直接发送字符串的字节形式,http要求是请求头加请求体字符串格式的字节形式
知道了怎么发送请求的了,也就知道了为何阻塞。
那就是socket默认在connect和recev阻塞,但可通过
sk = socket.socket()
sk.setblocking(False)
来变为非阻塞,但会相应报错(类似于队列通信),所以要用异常处理
没有了阻塞,我们就能够不等待而连续发送请求了,那么连接成功和数据到达是我们又是怎么知道,然后处理呢?
这里就引入了***IO多路复用,select模块***,它能帮助我们去监听何时连接成功了,何时数据到达了,
while True: #事件循环
rlist,wlist,elist=select.select(self.conn,self.connections,self.conn,0.05)
监听的对象不一定是socket对象,但要有fileno方法,并返回一个文件描述符
这样,可以得出一个结论:
异步IO等于非阻塞socket+IO多路复用 ,有数据时自动执行回调函数
有人认为异步IO就是非阻塞IO,这也没有问题,比如在这里的IO多路复用只是监视作用,对IO阻塞什么的没有影响
下面贴上一个自定义异步IO源码:
'''socket客户端'''
import select
import socket
import time
class HttpRequest:
def __init__(self,sk,host,callback):
self.socket = sk
self.host = host
self.callback=callback
def fileno(self):
return self.socket.fileno()
class AsyncRequest:
def __init__(self):
self.conn = []
self.connections = [] #用于检测是否已经连接成功
def add_request(self, host,callback):
"""创建一个要请求"""
try:
sk = socket.socket()
sk.setblocking(False)
sk.connect((host, 80)) # 原本的阻塞点
print("连接成功了")
except BlockingIOError as e:
pass
request=HttpRequest(sk,host,callback)
self.conn.append(request)
self.connections.append(request)
def run(self):
while True: #事件循环
rlist,wlist,elist=select.select(self.conn,self.connections,self.conn,0.05)
for w in wlist:
print(w.host,'连接成功')
#只要能循环得到,表示服务器端和客户端链接成功
tpl="GET / HTTP/1.0\r\nHost: %s\r\n\r\n" % (w.host,)
w.socket.send((bytes(tpl,encoding='utf-8')))
self.connections.remove(w)
for r in rlist:
#r是Httprequest对象
recv_data=bytes() #空字节变量
while True:
try:
chunk=r.socket.recv(8096)
recv_data+=chunk
except Exception as e:
break
print(r.host,"有数据返回",recv_data)
'''recv_data是httpresponse,包含请求头和请求体,可在回调函数中做相应处理'''
r.callback(recv_data)
r.socket.close()
self.conn.remove(r)
if len(self.conn)==0:
break
def callback1(data):
print('保存到文件',data)
def callback2(data):
print('保存到数据库',data)
url_list = [
{'host': 'www.baidu.com',
'callback': callback1},
{'host': 'cn.bing.com',
'callback': callback2},
]
req = AsyncRequest()
for item in url_list:
print(item)
req.add_request(item['host'],item['callback'])
req.run()