前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开发一个爬虫框架——Tinepeas

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

作者头像
青南
发布2020-05-14 10:40:41
8000
发布2020-05-14 10:40:41
举报
文章被收录于专栏:未闻Code未闻Code

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

这是 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 的代码:

代码语言:javascript
复制
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比纯数据类要多一些东西:

代码语言:javascript
复制
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 类初始化以后的对象。

代码语言:javascript
复制
@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),返回请求对象。

原理解读

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

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

代码语言:javascript
复制
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, 优先请求)?那么问题来了,如果当前排队的请求,他们也要优先处理怎么办?你怎么知道哪些请求需要优先?

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

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 未闻Code 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 运行爬虫
  • 组件化与输入输出
  • 数据对象
    • 请求 Request
      • 响应 Response
      • 调度器 Scheduler
        • 输入与输出
          • 原理解读
          相关产品与服务
          消息队列 TDMQ
          消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档