资讯专栏INFORMATION COLUMN

如何实现一个Python爬虫框架

feng409 / 3559人阅读

摘要:这篇文章的题目有点大,但这并不是说我自觉对爬虫这块有多大见解,我只不过是想将自己的一些经验付诸于笔,对于如何写一个爬虫框架,我想一步一步地结合具体代码来讲述如何从零开始编写一个自己的爬虫框架年到如今,我花精力比较多的一个开源项目算是了,这是

这篇文章的题目有点大,但这并不是说我自觉对Python爬虫这块有多大见解,我只不过是想将自己的一些经验付诸于笔,对于如何写一个爬虫框架,我想一步一步地结合具体代码来讲述如何从零开始编写一个自己的爬虫框架

2018年到如今,我花精力比较多的一个开源项目算是Ruia了,这是一个基于Python3.6+的异步爬虫框架,当时也获得一些推荐,比如Github Trending Python语言榜单第二,目前Ruia还在开发中,Star数目不过700+,如果各位有兴趣,欢迎一起开发,来波star我也不会拒绝哈~

什么是爬虫框架

说这个之前,得先说说什么是框架

是实现业界标准的组件规范:比如众所周知的MVC开发规范

提供规范所要求之基础功能的软件产品:比如Django框架就是MVC的开发框架,但它还提供了其他基础功能帮助我们快速开发,比如中间件、认证系统等

框架的关注点在于规范二字,好,我们要写的Python爬虫框架规范是什么?

很简单,爬虫框架就是对爬虫流程规范的实现,不清楚的朋友可以看上一篇文章谈谈对Python爬虫的理解,下面总结一下爬虫流程:

请求&响应

解析

持久化

这三个流程有没有可能以一种优雅的形式串联起来,Ruia目前是这样实现的,请看代码示例:

可以看到,Item & Field类结合一起实现了字段的解析提取,Spider类结合Request * Response类实现了对爬虫程序整体的控制,从而可以如同流水线一般编写爬虫,最后返回的item可以根据使用者自身的需求进行持久化,这几行代码,我们就实现了获取目标网页请求、字段解析提取、持久化这三个流程

实现了基本流程规范之后,我们继而就可以考虑一些基础功能,让使用者编写爬虫可以更加轻松,比如:中间件(Ruia里面的Middleware)、提供一些hook让用户编写爬虫更方便(比如ruia-motor)

这些想明白之后,接下来就可以愉快地编写自己心目中的爬虫框架了

如何踏出第一步

首先,我对Ruia爬虫框架的定位很清楚,基于asyncio & aiohttp的一个轻量的、异步爬虫框架,怎么实现呢,我觉得以下几点需要遵守:

轻量级,专注于抓取、解析和良好的API接口

插件化,各个模块耦合程度尽量低,目的是容易编写自定义插件

速度,异步无阻塞框架,需要对速度有一定追求

什么是爬虫框架如今我们已经很清楚了,现在急需要做的就是将流程规范利用Python语言实现出来,怎么实现,分为哪几个模块,可以看如下图示:

同时让我们结合上面一节的Ruia代码来从业务逻辑角度看看这几个模块到底是什么意思:

Request:请求

Response:响应

Item & Field:解析提取

Spider:爬虫程序的控制中心,将请求、响应、解析、存储结合起来

这四个部分我们可以简单地使用五个类来实现,在开始讲解之前,请先克隆Ruia框架到本地:

# 请确保本地Python环境是3.6+
git clone https://github.com/howie6879/ruia.git
# 安装pipenv
pip install pipenv 
# 安装依赖包
pipenv install --dev

然后用PyCharm打开Ruia项目:

选择刚刚pipenv配置好的python解释器:

此时可以完整地看到项目代码:

好,环境以及源码准备完毕,接下来将结合代码讲述一个爬虫框架的编写流程

Request & Response

Request类的目的是对aiohttp加一层封装进行模拟请求,功能如下:

封装GET、POST两种请求方式

增加回调机制

自定义重试次数、休眠时间、超时、重试解决方案、请求是否成功验证等功能

将返回的一系列数据封装成Response类返回

接下来就简单了,不过就是实现上述需求,首先,需要实现一个函数来抓取目标url,比如命名为fetch:

import asyncio
import aiohttp
import async_timeout

from typing import Coroutine


class Request:
    # Default config
    REQUEST_CONFIG = {
        "RETRIES": 3,
        "DELAY": 0,
        "TIMEOUT": 10,
        "RETRY_FUNC": Coroutine,
        "VALID": Coroutine
    }

    METHOD = ["GET", "POST"]

    def __init__(self, url, method="GET", request_config=None, request_session=None):
        self.url = url
        self.method = method.upper()
        self.request_config = request_config or self.REQUEST_CONFIG
        self.request_session = request_session

    @property
    def current_request_session(self):
        if self.request_session is None:
            self.request_session = aiohttp.ClientSession()
            self.close_request_session = True
        return self.request_session

    async def fetch(self):
        """Fetch all the information by using aiohttp"""
        if self.request_config.get("DELAY", 0) > 0:
            await asyncio.sleep(self.request_config["DELAY"])

        timeout = self.request_config.get("TIMEOUT", 10)
        async with async_timeout.timeout(timeout):
            resp = await self._make_request()
        try:
            resp_data = await resp.text()
        except UnicodeDecodeError:
            resp_data = await resp.read()
        resp_dict = dict(
            rl=self.url,
            method=self.method,
            encoding=resp.get_encoding(),
            html=resp_data,
            cookies=resp.cookies,
            headers=resp.headers,
            status=resp.status,
            history=resp.history
        )
        await self.request_session.close()
        return type("Response", (), resp_dict)


    async def _make_request(self):
        if self.method == "GET":
            request_func = self.current_request_session.get(self.url)
        else:
            request_func = self.current_request_session.post(self.url)
        resp = await request_func
        return resp

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    resp = loop.run_until_complete(Request("https://docs.python-ruia.org/").fetch())
    print(resp.status)

实际运行一下,会输出请求状态200,就这样简单封装一下,我们已经有了自己的请求类Request,接下来只需要再完善一下重试机制以及将返回的属性封装一下就基本完成了:

# 重试函数
async def _retry(self):
    if self.retry_times > 0:
        retry_times = self.request_config.get("RETRIES", 3) - self.retry_times + 1
        self.retry_times -= 1
        retry_func = self.request_config.get("RETRY_FUNC")
        if retry_func and iscoroutinefunction(retry_func):
            request_ins = await retry_func(weakref.proxy(self))
            if isinstance(request_ins, Request):
                return await request_ins.fetch()
        return await self.fetch()

最终代码见ruia/request.py即可,接下来就可以利用Request来实际请求一个目标网页,如下:

这段代码请求了目标网页https://docs.python-ruia.org/并返回了Response对象,其中Response提供属性介绍如下:

Field & Item

实现了对目标网页的请求,接下来就是对目标网页进行字段提取,我觉得ORM的思想很适合用在这里,我们只需要定义一个Item类,类里面每个属性都可以用Field类来定义,然后只需要传入url或者html,执行过后Item类里面 定义的属性会自动被提取出来变成目标字段值

可能说起来比较拗口,下面直接演示一下可能你就明白这样写的好,假设你的需求是获取HackerNews网页的titleurl,可以这样实现:

import asyncio

from ruia import AttrField, TextField, Item


class HackerNewsItem(Item):
    target_item = TextField(css_select="tr.athing")
    title = TextField(css_select="a.storylink")
    url = AttrField(css_select="a.storylink", attr="href")

async def main():
    async for item in HackerNewsItem.get_items(url="https://news.ycombinator.com/"):
        print(item.title, item.url)

if __name__ == "__main__":
     items = asyncio.run(main())

从输出结果可以看到,titleurl属性已经被赋与实际的目标值,这样写起来是不是很简洁清晰也很明了呢?

来看看怎么实现,Field类的目的是提供多种方式让开发者提取网页字段,比如:

XPath

CSS Selector

RE

所以我们只需要根据需求,定义父类然后再利用不同的提取方式实现子类即可,代码如下:

class BaseField(object):
    """
    BaseField class
    """

    def __init__(self, default: str = "", many: bool = False):
        """
        Init BaseField class
        url: http://lxml.de/index.html
        :param default: default value
        :param many: if there are many fields in one page
        """
        self.default = default
        self.many = many

    def extract(self, *args, **kwargs):
        raise NotImplementedError("extract is not implemented.")


class _LxmlElementField(BaseField):
    pass


class AttrField(_LxmlElementField):
    """
    This field is used to get  attribute.
    """
      pass


class HtmlField(_LxmlElementField):
    """
    This field is used to get raw html data.
    """
    pass


class TextField(_LxmlElementField):
    """
    This field is used to get text.
    """
      pass


class RegexField(BaseField):
    """
    This field is used to get raw html code by regular expression.
    RegexField uses standard library `re` inner, that is to say it has a better performance than _LxmlElementField.
    """
    pass

核心类就是上面的代码,具体实现请看ruia/field.py

接下来继续说Item部分,这部分实际上是对ORM那块的实现,用到的知识点是元类,因为我们需要控制类的创建行为:

class ItemMeta(type):
    """
    Metaclass for an item
    """

    def __new__(cls, name, bases, attrs):
        __fields = dict({(field_name, attrs.pop(field_name))
                         for field_name, object in list(attrs.items())
                         if isinstance(object, BaseField)})
        attrs["__fields"] = __fields
        new_class = type.__new__(cls, name, bases, attrs)
        return new_class


class Item(metaclass=ItemMeta):
    """
    Item class for each item
    """

    def __init__(self):
        self.ignore_item = False
        self.results = {}

这一层弄明白接下来就很简单了,还记得上一篇文章《谈谈对Python爬虫的理解》里面说的四个类型的目标网页么:

单页面单目标

单页面多目标

多页面单目标

多页面多目标

本质来说就是要获取网页的单目标以及多目标(多页面可以放在Spider那块实现),Item类只需要定义两个方法就能实现:

get_item():单目标

get_items():多目标,需要定义好target_item

具体实现见:ruia/item.py

Spider

Ruia框架中,为什么要有Spider,有以下原因:

真实世界爬虫是多个页面的(或深度或广度),利用Spider可以对这些进行 有效的管理

制定一套爬虫程序的编写标准,可以让开发者容易理解、交流,能迅速产出高质量爬虫程序

自由地定制插件

接下来说说代码实现,Ruia框架的API写法我有参考Scrapy,各个函数之间的联结也是使用回调,但是你也可以直接使用await,可以直接看代码示例:

from ruia import AttrField, TextField, Item, Spider


class HackerNewsItem(Item):
    target_item = TextField(css_select="tr.athing")
    title = TextField(css_select="a.storylink")
    url = AttrField(css_select="a.storylink", attr="href")


class HackerNewsSpider(Spider):
    start_urls = [f"https://news.ycombinator.com/news?p={index}" for index in range(1, 3)]

    async def parse(self, response):
        async for item in HackerNewsItem.get_items(html=response.html):
            yield item


if __name__ == "__main__":
    HackerNewsSpider.start()

使用起来还是挺简洁的,输出如下:

[2019:03:14 10:29:04] INFO  Spider  Spider started!
[2019:03:14 10:29:04] INFO  Spider  Worker started: 4380434912
[2019:03:14 10:29:04] INFO  Spider  Worker started: 4380435048
[2019:03:14 10:29:04] INFO  Request 
[2019:03:14 10:29:04] INFO  Request 
[2019:03:14 10:29:08] INFO  Spider  Stopping spider: Ruia
[2019:03:14 10:29:08] INFO  Spider  Total requests: 2
[2019:03:14 10:29:08] INFO  Spider  Time usage: 0:00:03.426335
[2019:03:14 10:29:08] INFO  Spider  Spider finished!

Spider的核心部分在于对请求URL的请求控制,目前采用的是生产消费者模式来处理,具体函数如下:

详细代码,见ruia/spider.py

更多

至此,爬虫框架的核心部分已经实现完毕,基础功能同样一个不落地实现了,接下来要做的就是:

实现更多优雅地功能

实现更多的插件,让生态丰富起来

修BUG

项目地址点击阅读原文或者在github搜索ruia,如果你有兴趣,请参与进来吧!

如果觉得写得不错,点个好看来个star呗~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/43363.html

相关文章

  • 零基础如何爬虫技术

    摘要:楚江数据是专业的互联网数据技术服务,现整理出零基础如何学爬虫技术以供学习,。本文来源知乎作者路人甲链接楚江数据提供网站数据采集和爬虫软件定制开发服务,服务范围涵盖社交网络电子商务分类信息学术研究等。 楚江数据是专业的互联网数据技术服务,现整理出零基础如何学爬虫技术以供学习,http://www.chujiangdata.com。 第一:Python爬虫学习系列教程(来源于某博主:htt...

    KunMinX 评论0 收藏0
  • 爬虫 - 收藏集 - 掘金

    摘要:在这之前,还是有必要对一些概念超轻量级反爬虫方案后端掘金前言爬虫和反爬虫日益成为每家公司的标配系统。 爬虫修炼之道——从网页中提取结构化数据并保存(以爬取糗百文本板块所有糗事为例) - 后端 - 掘金欢迎大家关注我的专题:爬虫修炼之道 上篇 爬虫修炼之道——编写一个爬取多页面的网络爬虫主要讲解了如何使用python编写一个可以下载多页面的爬虫,如何将相对URL转为绝对URL,如何限速,...

    1fe1se 评论0 收藏0
  • 精通Python网络爬虫(0):网络爬虫学习路线

    摘要:以上是如果你想精通网络爬虫的学习研究路线,按照这些步骤学习下去,可以让你的爬虫技术得到非常大的提升。 作者:韦玮 转载请注明出处 随着大数据时代的到来,人们对数据资源的需求越来越多,而爬虫是一种很好的自动采集数据的手段。 那么,如何才能精通Python网络爬虫呢?学习Python网络爬虫的路线应该如何进行呢?在此为大家具体进行介绍。 1、选择一款合适的编程语言 事实上,Python、P...

    spacewander 评论0 收藏0
  • Python爬虫学习路线

    摘要:以下这些项目,你拿来学习学习练练手。当你每个步骤都能做到很优秀的时候,你应该考虑如何组合这四个步骤,使你的爬虫达到效率最高,也就是所谓的爬虫策略问题,爬虫策略学习不是一朝一夕的事情,建议多看看一些比较优秀的爬虫的设计方案,比如说。 (一)如何学习Python 学习Python大致可以分为以下几个阶段: 1.刚上手的时候肯定是先过一遍Python最基本的知识,比如说:变量、数据结构、语法...

    liaoyg8023 评论0 收藏0
  • Python爬虫之Scrapy学习(基础篇)

    摘要:下载器下载器负责获取页面数据并提供给引擎,而后提供给。下载器中间件下载器中间件是在引擎及下载器之间的特定钩子,处理传递给引擎的。一旦页面下载完毕,下载器生成一个该页面的,并将其通过下载中间件返回方向发送给引擎。 作者:xiaoyu微信公众号:Python数据科学知乎:Python数据分析师 在爬虫的路上,学习scrapy是一个必不可少的环节。也许有好多朋友此时此刻也正在接触并学习sc...

    pkhope 评论0 收藏0

发表评论

0条评论

feng409

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<