异步爬虫框架与协程浅析
经典原文使用协成完成异步爬虫原文链接
根据分享原文链接。
Python基于协程的实现,其实是利用了Python生成器的特性完成的,Python生成器的原理其实涉及到用户态绿色线程的实现,用户态绿色线程是指通过在用户态实现函数之间执行的跳转,正常的函数调用在底层执行时会创建堆栈,将函数执行的数据进行压栈,保存函数运行时的数据,在函数执行完成后,函数运行后的数据会被丢弃,不会保存,实现函数之间的来回切换主要就是为了当在函数A中执行时,在A没有执行完成时,切换到B函数执行,此时需要将A运行的数据进行压栈,保存A在运行时产生的数据,然后切换到B当B在执行一段时间后,再切换回A的时候,此时保存B函数运行的现场,恢复A上次运行的现场,此时继续执行A函数,这样就达到了在用户态实现了函数之间的跳转。
Python中yield的基本用法
代码基于Python3.5.2
#coding:utf-8
def a():
val_a = "val_a"
print("start a")
data = yield val_a
print("end a")
return data
def b():
val_b = "val_b"
print("start b")
res = yield from a()
print("end b")
print("res : ", res)
def start():
_b = b()
print("recv :", _b.send(None)) # 装饰器开始
try:
_b.send("data") # 函数a中接受的数据赋值给data,随即函数a执行完毕
except StopIteration:
pass
if __name__ == '__main__':
start()
执行时首先执行到_b.send(None),此时函数执行到a函数中
data = yield val_a
将val_a作为结果返回给了_b.send(None),结果返回完成后执行结果完成。
接着执行a函数打印print并返回结果,此时a函数执行完成返回data,函数返回后在函数b处
_b.send("data")
此时会继续执行a函数data = yield val_a该处代码,将”data”赋值给data变量
res = yield from a()
将函数a执行完成后的结果赋值给res,此时b函数继续执行打印函数,至此调用完成。
运行结果如下:
start b
start a
recv : val_a
end a
end b
res : data
在Python中,对于yield和yield from更详细的用法,请查阅相关资料
代码分析:
想要实现一个异步爬虫框架,
1.基于异步io必须依靠操作系统提供的io复用的支持,需要实现循环事件;
2.要想实现多个任务之间的执行,则需要将一个链接进行封装成一个事务;
3.在第2步中的事务需要在相应事件发生的时候执行相应的任务,则需要封装一个任务确保能够继续执行。
由以上三点,得出前三个类的实现。
事件循环函数,如果发生了注册的事件则调用相应的注册方法
def loop():
while not stopped:
events = selector.select()
for event_key, event_mask in events:
callback = event_key.data
callback()
事务的封装类,主要是在注册方法时调用,此时就调用相应的方法
class Future:
def __init__(self):
self.result = None
self._callback = []
def add_done_callback(self, fn):
self._callback.append(fn)
def set_result(self, result):
self.result = result
for fn in self._callback:
fn(self)
def __iter__(self):
yield self
return self.result
任务类,将要处理的流程封装好,等待事件注册发生时,继续执行coro的流程
class Task:
def __init__(self, coro):
self.coro = coro
f = Future()
f.set_result(None)
self.step(f)
def step(self, future):
try:
next_future = self.coro.send(future.result)
print("next_future: ", next_future)
except StopIteration as e:
print("step stop :", e.value)
return
next_future.add_done_callback(self.step)
得出这三个概念后,全部代码如下
#coding:utf-8
import socket
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
selector = DefaultSelector()
class Future:
def __init__(self):
self.result = None
self._callback = []
def add_done_callback(self, fn):
self._callback.append(fn) # 将fn添加到回调执行函数中
def set_result(self, result):
self.result = result # 重置Future.result的值
for fn in self._callback:
fn(self) # 执行回调函数,此时会执行Task.step方法
def __iter__(self):
yield self # 迭代返回
return self.result
def connect(sock, address):
f = Future() # 实例一个事物
sock.setblocking(False) # 设置连接为非阻塞
try:
sock.connect(address) # 连接远程
except BlockingIOError:
pass
def on_connected():
f.set_result(None) # 连接完成后需要将流程往下继续执行
selector.register(sock.fileno(), EVENT_WRITE, on_connected) # 注册写事件,如果连接建立后的回调函数
print("connect")
yield from f # 返回事物
selector.unregister(sock.fileno()) # 当函数执行完成后取消注册的事件
class Task:
def __init__(self, coro):
self.coro = coro # coro就是由yield from封装的主流程
f = Future() # 实例化一个事物
f.set_result(None) # 设置执行的值
self.step(f) # 调用step方法
def step(self, future):
try:
next_future = self.coro.send(future.result) # 继续执行主流程中的流程
print("next_future: ", next_future)
except StopIteration as e:
print("step stop :", e.value)
return
next_future.add_done_callback(self.step) # 将返回的对象添加回调函数,该例中,next_funture大多返回f实例
def read(sock):
f = Future() # 实例化一个事物
def on_readable():
f.set_result(sock.recv(1)) # 将连接接受的数据设置到事物中,作为返回值
selector.register(sock.fileno(), EVENT_READ, on_readable) # 注册连接的读事件和回调函数
chunk = yield f # 将接受的数据返回
selector.unregister(sock.fileno()) # 取消连接的事件
return chunk # 将数据返回
def read_all(sock):
response = []
chunk = yield from read(sock) # 接受单次返回的数据
while chunk: # 如果返回的数据有,则继续
response.append(chunk) # 加入到返回数据列表
chunk = yield from read(sock) # 继续获取连接数据
return b''.join(response) # 返回所有接受的数据
stopped = False # 循环执行的标志
class Fetcher:
def __init__(self, url): # 主流程
self.response = b'' # 响应结果
self.url = url # 访问链接
self.sock = None # 链接实例
def fetch(self):
sock = socket.socket() # 链接实例
yield from connect(sock, ('xkcd.com', 80)) # 连接远端
request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\n'.format(self.url) # 序列化请求
sock.send(request.encode("ascii")) # 发送数据出去
self.response = yield from read_all(sock) # 读取所有的数据
print("response: ", self.response)
def loop():
while not stopped:
events = selector.select() # 获取注册返回
for event_key, event_mask in events: #
callback = event_key.data # 获取注册事件的注册回调函数
callback() # 执行回调函数
if __name__ == "__main__":
fetcher = Fetcher('/353/') # 实例化一个主流程实例
Task(fetcher.fetch()) # 用任务包装主流程
loop() # 启动循环
流程解读:
首先,fetcher = Fetcher(‘/353/’) ,实例化一个主流程实例,然后调用fetcher.fetch()作为参数值传入Task中,此时Task实例化时,执行
def __init__(self, coro):
self.coro = coro # coro就是由yield from封装的主流程
f = Future() # 实例化一个事物
f.set_result(None) # 设置执行的值
self.step(f) # 调用step方法
def step(self, future):
try:
next_future = self.coro.send(future.result) # 继续执行主流程中的流程
print("next_future: ", next_future)
except StopIteration as e:
print("step stop :", e.value)
return
next_future.add_done_callback(self.step) # 将返回的对象添加回调函数,该例中,next_funture大多返回f实例
此时future.result=None,执行step方法,此时会执行
next_future = self.coro.send(future.result) # 继续执行主流程中的流程
此时会执行到
def connect(sock, address):
f = Future() # 实例一个事物
sock.setblocking(False) # 设置连接为非阻塞
try:
sock.connect(address) # 连接远程
except BlockingIOError:
pass
def on_connected():
f.set_result(None) # 连接完成后需要将流程往下继续执行
selector.register(sock.fileno(), EVENT_WRITE, on_connected) # 注册写事件,如果连接建立后的回调函数
print("connect")
yield from f # 返回事物
selector.unregister(sock.fileno()) # 当函数执行完成后取消注册的事件
连接远程端口,注册连接的读事件和回调函数,并返回f实例,此时
在step函数中继续执行
next_future.add_done_callback(self.step)
将step函数添加到f的回调函数中,
当远程连接成功后此时会调用loop中事件的回调函数,此时就执行
def on_connected():
f.set_result(None) # 连接完成后需要将流程往下继续执行
此时实例f的_callback中已经有了Task.step函数,此时就执行
def set_result(self, result):
self.result = result # 重置Future.result的值
for fn in self._callback:
fn(self) # 执行回调函数,此时会执行Task.stop方法
当调用fn(self)时,就执行以下代码
def step(self, future):
try:
next_future = self.coro.send(future.result) # 继续执行主流程中的流程
print("next_future: ", next_future)
except StopIteration as e:
print("step stop :", e.value)
return
next_future.add_done_callback(self.step) # 将返回的对象添加回调函数,该例中,next_funture大多返回f实例
此时继续执行coro主流程的代码,此时就执行
request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\n'.format(self.url) # 序列化请求
sock.send(request.encode("ascii")) # 发送数据出去
self.response = yield from read_all(sock) # 读取所有的数据
此时会继续执行yield from read_all(sock)中的函数,
def read_all(sock):
response = []
chunk = yield from read(sock) # 接受单次返回的数据
while chunk: # 如果返回的数据有,则继续
response.append(chunk) # 加入到返回数据列表
chunk = yield from read(sock) # 继续获取连接数据
return b''.join(response) # 返回所有接受的数据
第一次进去时,会执行
yield from read(sock)
执行read函数
def read(sock):
f = Future() # 实例化一个事物
def on_readable():
f.set_result(sock.recv(1)) # 将连接接受的数据设置到事物中,作为返回值
selector.register(sock.fileno(), EVENT_READ, on_readable) # 注册连接的读事件和回调函数
chunk = yield f # 将接受的数据返回
selector.unregister(sock.fileno()) # 取消连接的事件
return chunk # 将数据返回
此时,先实例化一个f,注册连接的读事件和回调函数,然后返回f,此时返回的f会继续执行
next_future.add_done_callback(self.step) # 将返回的对象添加回调函数,该例中,next_funture大多返回f实例
将self.step函数添加到实例f的回调执行函数中。
当读事件发生后,执行了相应的回调函数
def on_readable():
f.set_result(sock.recv(1)) # 将连接接受的数据设置到事物中,作为返回值
将sock.recv(1)的数据作为result,设置到f.result中,此时再执行
next_future = self.coro.send(future.result) # 继续执行主流程中的流程
此时coro主流程中中断的地方
chunk = yield f # 将接受的数据返回
此时,chunk就是f.result的值,此时继续往下执行
selector.unregister(sock.fileno()) # 取消连接的事件
return chunk # 将数据返回
当返回chunk时,此时
chunk = yield from read(sock) # 接受单次返回的数据
while chunk: # 如果返回的数据有,则继续
response.append(chunk) # 加入到返回数据列表
chunk = yield from read(sock) # 继续获取连接数据
接受的就是chunk数据,然后循环执行该循环,直到所有数据读取完毕
此时执行
self.response = yield from read_all(sock) # 读取所有的数据
所有响应数据返回给self.response此时,数据读取完毕。
一个简易的异步爬虫框架的原理分析完毕。