构建简单的类Flask的爬虫框架

Flask作为一个在Python领域较为出名的web框架,其页面构建采用了一种Python语法糖——修饰器,刚开始看到的时候,觉得Django简直是反Python之禅之大成!然后就火急火燎研究了一下修饰器的相关知识,瞬间觉得平时随手写的爬虫可以更加DRY(don't repeat yourself),开坑之后发现,这里面的坑还真深,所以容我写一篇博客来装逼。代码比较长,所以放在了Github上。

铺垫

首先我们需要想一个经典的爬虫应用,然后再开始实现,然后瞬间就想到了各种爬虫入门都使用的妹子图例子(我为什么会瞬间想到?我明明很单纯的)。这个例子很简单,先打开一个页面,然后解析出所有图片的链接,最后利用链接保存图片。这个例子网上很多,随便从简书找了一个,可以看到,这个例子也进行了函数的抽提以达到简化代码结构和复用的功效。但是这些重复的过程可能你在写下一个爬虫的时候还是会再写一次(如果你又一次引用了请别戳破,我只是觉得很少人会这样做,包括我),所以如果能够提取成一个库,那么这些工作就可以一劳永逸了。

开始构想

修饰器,其实际作用是将一个函数作为参数传入某个函数进行修饰,然后返回新的函数,此时再调用该函数,就是新的被修饰过的函数了。但是这样的理解不太适合于我们进行设计,打个不知道是否合适的比方,修饰器所需要传入的函数其实是大白胸前的那张卡,如果没有这张卡,大白就是不完整的,无法运行,但是这张卡插入了,大白就完整了,而且这张卡还决定了大白的属性。如此这般抽象到我们的想法当中,妹子图类似的爬虫里面,请求页面,保存图片这些操作都是一样的,就像大白充气的身体。而唯一不同的就是解析页面这个部分,可能这个网站的妹子图的链接在一个class的img标签内,但是另一个网站的妹子图在另一个class的img标签内,而这个解析的过程抽象出的函数,就是修饰器需要修饰的方法,即大白需要插入的卡,可能是红卡,可能是绿卡。来一张脑图

爬虫工作的基本流程

绿色的框框表示每次都是一样的操作,可以抽提为修饰器,而黄色的部分则是每次都不一样而需要修改的部分。

如果还不能理解,类比Flask框架,接收用户请求这部分可以看作我们这里请求页面这部分,而给用户返回结果的部分相当与我们保存图片这部分,中间唯一需要我们写的生成页面的部分就是我们这里的解析图片链接的部分,如果还不能理解,咳咳,直接上代码吧!

码代码

仔细想了想,我还是决定使用自顶向下的方法来讲一下这个代码,假使我们已经创建了一个我们理想中的爬虫框架,我们将其命名为spidry,其具有修饰器saveimages(类似Flask里面的app.route这样的东西)。

那么爬虫写出来如下:

# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: test.py
@time: 2016/11/28
"""
from spidry import saveimages
from spidry import response as resp
import os


@saveimages(feature='json', sleep=3)# 使用修饰器修饰解析方法
def bilibili():
    # 解析方法,生成包含需要保存图片url和路径的字典列表
    iconlist = [{'url': icon['icon'],
                 'path': 'icon/'+icon['title']+'.gif'}
                for icon in resp.json['fix']]
    return iconlist


if __name__ == '__main__':
    if not os.path.exists("icon"):
        os.makedirs("icon")
    # 调用被修饰的方法!
    bilibili("http://www.bilibili.com/index/index-icon.json")
    print("done!")

如果写过Flask应用的童鞋应该对这样的语法应用不会很陌生,这里的response对象就是我们这个框架自动根据请求页面生成的请求返回对象,已经自动根据参数解析,类似Flask里面的session对象之类的。为了体现我等当代青年的高尚追求,这里我们用了一个其他的例子,下载B站右上角的动图,这段url会返回一个json,里面记录了所有动图的名称和地址,网站显示时使用一段js代码随机抽出一个显示,这里我们全部下载。feature参数指定我们需要如何解析返回的数据,这里设置为json,sleep参数为每下载一张图片暂停的时间,更多的参数我们在代码实现中自然会看到,这里暂且不提。
运行之:

fetch:http://www.bilibili.com/index/index-icon.json
save:icon/羽生结弦.gif
save:icon/僵尸.gif
save:icon/困.gif
save:icon/南瓜灯.gif
...此处省略..
save:icon/233333.gif
done!

然后在icon文件夹下就出现了所有的鬼畜小动图!
看到这里,是不是觉得这个框架会让爬虫变得非常简单,写起来就是那么自然、体贴、干爽、透气,独有的速效凹道和完美的吸收轨迹,让你再也不用为每个月的那几天感到焦虑和不安,再加上贴心的护翼设计,量多也不用当心。对不起,我调皮了(鸡汁地盗了一段话)。

修饰器类这样实现

当然啦,最重要还是如何实现修饰器,关于修饰器的基础知识,这里不再造轮子,大家可以去这里看这篇文章,我认为是讲得比较清楚也比较全的一篇。直接上代码:

# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: saveimages.py
@time: 2016/11/29
"""
from bs4 import BeautifulSoup
from functools import wraps
from .spidry import response
import requests
import time


class saveimages:
    """
    修饰器类
    """

    def __init__(self,
                 feature='html',
                 method='get',
                 sleep=0,
                 log=True,
                 **kwargs):
        """
        构造方法,初始化各种参数
        :param feature: 解析请求数据的方法,暂时分为html的soup和json
        :param method: 请求图片的方法
        :param sleep: 保存图片时每张图片的请求时间间隔
        :param log: 是否打印log
        :param kwargs: 其他的关键词参数,与requests库的参数相关
        """
        self.feature = feature
        self.method = method
        self.sleep = sleep
        self.log = log
        self.kwargs = kwargs

    def __call__(self, fn):
        """
        类被作为修饰器调用时调用方法
        :param fn:传入的图片链接解析函数
        :return:
        """

        @wraps(fn)
        def wrapper(url, method='get', **kwargs):
            """
            修饰后的函数的实现
            :param url: 需要请求的页面地址
            :param method: 请求的方法
            :param kwargs: 其他请求参数
            """
            self._fetchpage(url, method, **kwargs)
            imglist = fn()  # 调用原始方法,获得图片列表
            for img in imglist:  # 循环保存图片
                self.saveimage(img)

        return wrapper

    def _fetchpage(self, url, method, **kwargs):
        """
        请求页面并解析为相应的解析对象
        :param url:请求页面的url
        :param method:请求方法
        :param kwargs:其他请求尝试
        """
        if self.log:
            print('fetch:' + url)
        response.r = requests.request(method, url, **kwargs)
        response.text = response.r.text
        if self.feature.lower() == 'html':  # 将结果解析为soup
            response.soup = BeautifulSoup(response.text, 'lxml')
            response.json = None
        elif self.feature.lower() == 'json':  # 将结果解析为json
            response.json = response.r.json()
            response.soup = None

    def saveimage(self, img):
        """
        保存图片函数
        :param img: 包含图片url和保存路径的字典
        """
        url = img['url']
        path = img['path']
        r = requests.request(self.method, url, **self.kwargs)
        with open(path, 'wb') as img:
            img.write(r.content)
        if self.log:
            print('save:' + path)
        time.sleep(self.sleep)

这个没什么好说的,几乎就是修饰器的内容,但是这里值得一提的是这里的respone对象,也是我们最终爬虫代码时调用的那个对象,这个对象实现起来其实也并不简单。

不简单的全局对象

前面说到了,这个response对象并不简单,我们在使用Flask的时候,你可能会引入session或者request对象,大致使用如下:

from flask import session, request
name = session['name']
name = request.name

那么这个看似是一个全局变量的东西是如何定位到每次的的请求对象的?我们的这个response对象又该如何实现呢?

第一想法,使用全局对象,但是有一个问题,就是使用起来非常的麻烦,每次均需要声明其为globe,且其是静态的!然而Flask的对象并不是这样,这又是为什么呢?所以还是找了一圈资料,如果想深究,建议直接看Flask的源码,简单点的,建议看这篇博客,讲得不是很清楚,但是没有啥讲得更清楚的貌似!这里总结一下,flask的这个对象其实是使用了werkzeug库的LocalStack类,该类是标准库中threading包中的local类的一种封装,至于local类的使用,可以看看这篇博客或者直接去看文档,其实际是一个线程唯一类,在同一线程中能够共享一些动态对象。这里我们也进行一些封装,实现如下:

# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: spidry.py
@time: 2016/11/29
"""
from threading import local
from requests.models import Response
from bs4 import BeautifulSoup


class Spidry(local):
    def __init__(self):
        self.soup = BeautifulSoup(features='lxml')
        self.json = {}
        self.r = Response()
        self.text = ''


response = Spidry()

可以看到,我们是否进行封装是无所谓的,只需要实例化local类的对象,我们就可以往里面塞各种对象同时进行共享,但是这里我还是进行了封装同时还假实例化了各个变量对应的类,只是为了在后面引用时能够获得代码提示而已,就这么简单却人性化,你来打我呀!

更多的扩展

虽然至此,我们最初的想法是已经成功了,但是如果只是能够进行图片保存,那么是否有点无趣了呢?其次,对于异常的处理,更多的参数设置,都还有待完善!其实这只是一种思路,我们还可以再继续添加将数据保存至数据库或文件,自动翻页等等修饰器,其次,还可以对现有的修饰器进行细分使其通用性更强,比如分开打开页面与保存图片的装饰器,以修饰器嵌套的方式来实现,这样代码的复用性将会更高!
整个坑从想法、搜集资料、编写各种模块的测试Demo到正式开坑进行编写最后写下这篇博客,在一边上课一边各种作业轰炸的夹缝中折腾了大约2周,总之收获很多,希望自己以后还能有各种各样类似的奇思妙想来继续折腾吧!至于开源的仓库,万一脑充血了,我可能就会更新,也欢迎pull request!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容