异步爬虫框架与协程浅析

异步爬虫框架与协程浅析

经典原文使用协成完成异步爬虫原文链接

根据分享原文链接。

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此时,数据读取完毕。
一个简易的异步爬虫框架的原理分析完毕。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值