美文网首页
构建简单的类Flask的爬虫框架

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

作者: yangmqglobe | 来源:发表于2017-10-25 20:11 被阅读44次

    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!

    相关文章

      网友评论

          本文标题:构建简单的类Flask的爬虫框架

          本文链接:https://www.haomeiwen.com/subject/yvjcpxtx.html