专栏首页未闻Code从零开发一个爬虫框架——Tinepeas

从零开发一个爬虫框架——Tinepeas

经常写爬虫的同学,肯定对下面这张图片很熟悉:

这是 Scrapy 的数据流图。

Scrapy 是一个非常优秀的爬虫框架,为了向 Scrapy 致敬,也为了让大家更好地理解 Scrapy 的工作原理,我们自己模仿 Scrapy 的数据流,写一个爬虫框架。

这个框架我命名为 Tinepeas,中文名叫做豌豆尖。以表达我对杭州吃不到豌豆尖的遗憾之情和对豌豆尖的想念。

Tinepeas 的数据流如下图所示:

基于 Scrapy 的数据流,我们省略了 Downloader Middleware、Spider Middleware 和 Pipeline,仅保留核心的爬虫定义、调度和网络请求。

Scrapy 基于 Twisted实现异步请求,而Tinepeas使用Asyncio 和 aiohttp 实现异步请求。

运行爬虫

我们先来看一下爬虫代码并运行,看看效果如何:

请求1000个页面,总共耗时不到10秒。

爬虫代码本身的写法,与 Scrapy 如出一辙。Tinepeas 会去调度开发者写的代码,并运行。

组件化与输入输出

Tinepeas 的核心思想是组件化。也就是单独实现各个不同的组件。每个组件之间定义好输入和输出。大家可以看到在数据流图中的DownloaderSpiderScheduler都是组件。他们之间的数据通过Core来进行沟通。而各个组件之间交流的数据,就是Request对象和Response对象。

只要定义好输入和输出,各个组件可以分别由不同的人开发,甚至组件也可以进行替换。例如本文我们使用aiohttp来请求网络。但是只要保持输入和输入对应的 API 不变,你完全可以使用 Pyppeteer 来替换。

数据对象

在数据流图中的RequestResponse都是数据对象。他们的作用,本质上与字典没有什么区别,都是用来存放数据的。只不过,使用类来组织,可以避免发生忘记字典里面有哪些 Key 的尴尬。而且通过 PyCharm 这种集成开发环境来开发的时候,还可以自动补全。

请求 Request

我们来看一下 Request 的代码:

from dataclasses import dataclass
from typing import Callable


@dataclass
class Request:
    url: str
    headers: dict = None
    callback: Callable = None
    method: str = 'get'
    meta: dict = None
    dont_filter: bool = False
    encoding: str = 'utf-8'

    def __repr__(self):
        return f'url: {self.url}, callback: {self.callback}'

正如上文所说,本质上,Request类的作用与字典没有什么区别,就是存放数据而已。在这个类里面,我们定义了请求的url(网址)headers(请求头)callback(回调函数)method(请求方式)meta(元数据存放)dont_filter(不要过滤)encoding(编码方式)

其中,dont_filter涉及到的去重功能,我们在下一篇文章中通过中间件来实现。

至于下面的__repr__,是方便在调试的时候,看到请求对象的内容。对主要功能没有什么影响。

响应 Response

响应类Response比纯数据类要多一些东西:

import json
from .request import Request
from .selector import Selector
from dataclasses import dataclass


@dataclass
class Response:
    body: str = ''
    status: int = -1
    request: Request = None
    _selector: Selector = None

    def json(self):
        return json.loads(self.body)

    @property
    def selector(self):
        if not self._selector:
            self._selector = Selector(self.body)
        return self._selector

    def xpath(self, xpath_str):
        return self.selector.xpath(xpath_str)

    @property
    def url(self):
        return self.request.url

    @property
    def meta(self):
        return self.request.meta

我们先来看下图中方框框住的部分:

其中的body对应请求 URL 以后返回的内容,如果返回的是 JSON 字符串,那么可以调用response.json()方法直接对 JSON 字符串进行解析。

request对应的是请求对象,也就是上面一小节的 Request 类初始化以后的对象。

@property
def url(self):
    return self.request.url

@property
def meta(self):
    return self.request.meta

通过@property装饰器,让这urlmeta变成属性,这样就可以直接使用response.url来获取请求的 URL,而不需要写为response.request.url

xpath方法和selector属性,我们在第二篇文章中再来详细讲解。

有了RequestResponse这两个数据类所初始化的对象进行数据传递,我们就可以开始沟通各个不同的组件了。

调度器 Scheduler

输入与输出

调度器提供两个 API:def schedule(self, request),接收请求参数;def get(self),返回请求对象。

原理解读

调度器的作用看起来非常鸡肋:把请求先放进去,然后再取出来。然后传给下载器下载。调度器一日游到底有什么作用呢?

我们来考虑最常见的情况,把调度器想象成一个列表:

scheduler = []
scheduler.append(请求1)
scheduler.append(请求2)
scheduler.append(请求3)
scheduler.append(请求4)

request = scheduler.pop(0)
print('把 request 传给下载器')
request = scheduler.pop(0)
print('把 request 传给下载器')
request = scheduler.pop(0)
print('把 request 传给下载器')
request = scheduler.pop(0)
print('把 request 传给下载器')

这就是一个非常常见的先入先出队列。我直接用列表就可以了,为什么还要写个类来处理它?

但我们考虑另一种情况,假如你一次性可以处理100个请求,现在你还有500个请求在排队。这个时候,你需要优先发起一个请求,应该怎么办?

你觉得是不是可以这样写:schedule.insert(0, 优先请求)?那么问题来了,如果当前排队的请求,他们也要优先处理怎么办?你怎么知道哪些请求需要优先?

显然,我们可以通过给每一个请求设定一个优先级评分,然后对评分进行排序来实现优先发起高优先级的请求。

那么,当涉及到优先级评分的时候,你觉得直接使用列表仍然是最好的选择吗?这个时候显然用一个最大堆会更好,插入以后自动排序。不用每次都做全排序,复杂度大大降低。

关于更多调度器的问题,我们还是留在后面的文章来讲。今天我们就把他作为一个先入先出队列:

本文分享自微信公众号 - 未闻Code(itskingname),作者:kingname

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一日一技:在Python中实现阿拉伯数字加上中文数字

    在Python 3里面,中文是可以作为变量名的,而运算符又可以重载,基于这两个特性,我们可以实现阿拉伯数字与中文数字的四则运算。

    青南
  • 一日一技:在什么情况下使用@property比较好?

    我在2016年的时候,写过一篇文章,介绍@property装饰器。4年过去了,本来以为这个装饰器使用起来应该是很自然的事情,但还是有同学不知道在什么场景下可以...

    青南
  • 一日一技:在Python类里面初始化自己

    这里的 __init__叫做 构造函数。它负责在类初始化为实例的时候,初始化必要的数据。如下图所示:

    青南
  • AFNetworking框架分析(二)——AFURLSessionManager(上)

    AFURLSessionManager 这个类是AFN框架的核心类,基本上通过它来实现了大部分核心功能。负责请求的建立、管理、销毁、安全、请求重定向、请求重启等...

    我只不过是出来写写代码
  • 教程 | 如何使用DeepFake实现视频换脸

    机器之心
  • 教程 | 如何使用DeepFake实现视频换脸

    朱晓霞
  • Backtrader量化平台教程-跟踪止损单(十)

    AD:(本人录制的backtrader视频课程,大家多多支持哦~ https://edu.csdn.net/course/detail/9040)

    钱塘小甲子
  • 用Python做一个游戏辅助脚本,完整编程思路分享!

    简述:本文将以4399小游戏《 宠物连连看经典版2 》作为测试案例,通过识别小图标,模拟鼠标点击,快速完成配对。对于有兴趣学习游戏脚本的同学有一定的帮助。

    python学习教程
  • PyQt5 技术篇-如何彻底删除控件?布局移除控件方法。

    正常调用removeWidget()方法删除控件,但是删不干净,需要调用sip.delete()再删一下才能彻底清除。

    小蓝枣
  • 收藏| Scrapy框架各组件详细设置

    大家好,关于Requests爬虫我们已经讲了很多。今天我们就说一下Scrapy框架各组件的详细设置方便之后更新Scrapy爬虫实战案例。

    刘早起

扫码关注云+社区

领取腾讯云代金券