Scrapy之断点续爬(存入MySQL)

作者: Wakingup88688 | 来源:发表于2017-05-10 20:13 被阅读2211次

    Scrapy第五篇:断点续爬 | 存入MySQL

    五一前后疯癫玩了一周(纯玩耍真的),然后又应付本专业各种作业、PPT?
    本来想先解决IP这一块,结果被坑惨了,辗转两天先跳过,心累

    40918944

    好久不学习,惭愧,不说了我们回归正题。

    不得不说scrapy真的是一个强大的框架,配上轻灵简洁的mongodb,只需极少代码便可应付一个简单爬虫。但如果几十万几百万的数据量,需要考虑的因素就很多了,速度、存储、代理Ip应对反爬等等

    还有一个重要的,比如某一天你好不容易写完代码,心想“这下好了,接下来全部丢给机器我真机智哈哈哈”,于是睡觉/玩耍/撩妹美滋滋,回来一看,不巧,断网/IP被封/奇异bug,这时你的内心是这样的:

    好气呀,对不对?
    所以你需要——断点续爬
    好吧貌似又胡扯了(其实是喜欢这个表情,强行用上哈哈)
    </br>
    本次目标:
    爬取轮子哥54万关注用户信息,存入MySQL,实现断点续爬
    </br>
    </br>

    一、爬虫思路

    关于这个话题网上的教程也非常多,一般是设法获取_xsrf参数然后模拟登陆,再进行抓取。
    比如下面这一篇写得真的很不错:爬虫(3)--- 一起来爬知乎,作者是简书的whenif

    爬取关注用户其实更简单,可以直接调用API
    一般用开发者工具抓包,只能获得以下字段

    本篇将获得以下字段:

    大概30来项,调用URL:

    https://www.zhihu.com/api/v4/members/excited-vczh/followers?include=data%5B*%5D.locations%2Cemployments%2Cgender%2Ceducations%2Cbusiness%2Cvoteup_count%2Cthanked_Count%2Cfollower_count%2Cfollowing_count%2Ccover_url%2Cfollowing_topic_count%2Cfollowing_question_count%2Cfollowing_favlists_count%2Cfollowing_columns_count%2Cavatar_hue%2Canswer_count%2Carticles_count%2Cpins_count%2Cquestion_count%2Ccommercial_question_count%2Cfavorite_count%2Cfavorited_count%2Clogs_count%2Cmarked_answers_count%2Cmarked_answers_text%2Cmessage_thread_token%2Caccount_status%2Cis_active%2Cis_force_renamed%2Cis_bind_sina%2Csina_weibo_url%2Csina_weibo_name%2Cshow_sina_weibo%2Cis_blocking%2Cis_blocked%2Cis_following%2Cis_followed%2Cmutual_followees_count%2Cvote_to_count%2Cvote_from_count%2Cthank_to_count%2Cthank_from_count%2Cthanked_count%2Cdescription%2Chosted_live_count%2Cparticipated_live_count%2Callow_message%2Cindustry_category%2Corg_name%2Corg_homepage%2Cbadge%5B%3F(type%3Dbest_answerer)%5D.topics&limit=20&offset=i

    这么多字段哪里来?好多天前了,抓包过程中无意发现的,easy(相信有盆友早就知道了)

    其实这只是Api其中一部分字段,全部字段比这还多,为了防止查水表还是不贴上了,想要可以私信。

    </br>
    </br>

    二、Spiders逻辑

    调用Api非常简单,但是想防止被ban,要做好伪装。random.choice设置动态U-A。原以为还需要到代理Ip,没想到可以应付得过去。还是要注意爬虫的友好性,下载延迟3s。就不赘述。

    需要注意的是header尽量模拟浏览器,'authorization'参数一定要带上,如果不带上无法返回数据,同时这个参数是有有效期限的,失效返回401,需要重新抓取。

    DEFAULT_REQUEST_HEADERS = {
        'accept':'application/json, text/plain, */*',
        'Accept-Encoding':'gzip, deflate, sdch',
        'Accept-Language':'zh-CN,zh;q=0.8',
        'authorization':'xxx(自己抓)',
        'Connection':'keep-alive',
        'Host':'www.zhihu.com',
        'Referer':'https://www.zhihu.com/people/excited-vczh/followers',
        'x-udid':'AIDAV7HBLwqPTulyfqA9p0CFbRlOL10cidE=',
        'User-Agent':random.choice(USER_AGENTS)}
    

    接着就是提取数据的部分,打开上面的URL,返回一堆Json格式数据

    自定义一个Too类清洗headline(签名)、description(个人简介),为了方便调用放在settings中;还有locations(居住地址)和business(行业),不是所有用户都填写的,需要判断;还有educations(教育经历)和employments(职业经历),有各种可能,以前者来说:
    1、有'school'、有'major'
    2、有'school'、无'major'
    2、无'school'、有'major'
    4、无'school'、无'major'
    同时还可能有若干项,比如某某用户教育经历:
    1、xx小学
    2、xx初中
    3、xx高中
    4、xx大学

    in dict.keys() 方法判断dict中某一属性是否存在
    一堆if/else,一点也不优雅,无可奈何

     if len(i['educations'])== 0:
        item['educations']=''
    else:
        content=[]
        for n in i['educations']:
            S='school' in n.keys()
            M='major' in n.keys()
            if S:
                if M:
                   self.L=n['school']['name']+'/'+n['major']['name']
                else:
                   self.L=n['school']['name']
            else:
                self.L=n['major']['name']
            content.append(self.L)
        item['educations']=''
        for l in content:
            item['educations']+=l+'  '      
    

    employments同理,L等设为全局变量。
    最后获得这样,美丽的数据,不容易


    </br>
    </br>

    三、存储入MySQL

    1、首先下载安装MySQL及服务
    如果是5.7以后的windows系统,推荐以下教程,很详细:
    windows安装MySQL数据库(非安装版),设置编码为utf8

    我是之前某天安装的,忘记密码,上网搜到一个可以跳过验证的教程,好不容易通过。
    第二天电脑重启,无法连接了,搜了网上各种方法无果。糊里糊涂又安装了个5.6.36版本,还是不行。最后全部卸载并强力删除,重新又安装了一个5.7版本的。为此注册了个oracle账户,弄到一半发现原来已经有账户了,重新邮箱验证找回密码,登录,下载,安装,终于...

    不心累了,心痛。。

    你看,各种坑,可能是因为windows情况许多情况必需用管理员命令才能运行。
    迫切地想换linux系统!!!
    </br>
    2、可视化工具
    MySQL可视化工具很多,例如:20个最佳MySQL GUI的可视化管理工具
    也可参考某个社区网友推荐:最好用的mysql可视化工具是什么? - 开源中国社区
    </br>
    我试了好几个,觉得MySQL-Front 简洁易上手,SQL-yog功能超级强大。
    最喜欢还是Navicat,真的很好用,强力推荐哈哈。

    比较不巧的是,想要长久服务得购买,官方正版只有14天试用期,需要验证码。

    傻乎乎去找验证码,结果发现网上的都是不能用的,套路啊。最后还是找到了一个中文版navicat,用山寨验证码,验证山寨navicat,成功。虽然比较简陋,也和正式版一样好用,小小确幸?
    </br>
    不熟悉MySQL语法也没关系,利用Navicat可快速进行一些设置:
    引擎



    gender等数字型


    </br>
    主键

    看到这把钥匙了么?
    主键设置为detailURL(用户主页地址),每一个用户唯一,机智如我哈哈

    可以看到,字符集尽可能选择了utf8( utf8而不是utf-8),为什么呢?
    MySQL默认使用字符集latin1,不支持中文。如果不设置成utf8,会出现下面这样:


    </br>
    3、MySQL基础知识
    mysql查询:【图文】mysql查询数据相关操作_百度文库
    Python中操作mysql:python 操作MySQL数据库 | 菜鸟教程
    </br>
    4、自定义模块存储
    这里参考了博文:小白进阶之Scrapy第一篇 | 静觅 - Part 3
    使用Mysql-connector操作mysql(当然也可以用上面的MySQLdb方法)
    但是觉得里面有些部分有些冗赘,而且我们已经设置了主键,改成下面这样

    <Mysql.py>
    在这之前需要在settings中设置各种MySQL配置参数,这里不赘述
    # --coding:utf-8 --
    #导入settings中各种参数
    from ZhiHu import settings
    import mysql.connector

    # 使用connector连接到数据库
    db=mysql.connector.Connect(user=settings.MYSQL_USER, host=settings.MYSQL_HOST, port=settings.MYSQL_PORT,password=settings.MYSQL_PASSWORD, database=settings.MYSQL_DB_NAME)
    # 初始化操作游标,buffer指的是使用客户端的缓冲区,减少本机服务器的压力
    cursor=db.cursor(buffered=True)
    
    print u'成功连接数据库!'
    
    class MySql(object):
        @classmethod
        def insert_db(cls,item):
            '''数据插入数据库,用到装饰器'''
            sql="INSERT INTO zhihu(name,headline,description,detailURL,gender,user_type,is_active,locations,business,educations,employments,following_count,follower_count,mutual_followees_count,voteup_count,thanked_count,favorited_count,logs_count,following_question_count,following_topic_count,following_favlists_count,following_columns_count,question_count,answer_count,articles_count,pins_count,participated_live_count,hosted_live_count) VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
    
            values=(item['name'], item['headline'], item['description'], item['detailURL'], item['gender'],
                item['user_type'], item['is_active'], item['locations'], item['business'],
                item['educations'], item['employments'], item['following_count'], item['follower_count'],
                item['mutual_followees_count'], item['voteup_count'], item['thanked_count'], item['favorited_count'],
                item['logs_count'], item['following_question_count'], item['following_topic_count'],
                item['following_favlists_count'], item['following_columns_count'], item['articles_count'],
                item['question_count'], item['answer_count'], item['pins_count'], item['participated_live_count'],
                item['hosted_live_count'])
    
            cursor.execute(sql,values)
            db.commit()
    

    将dict改成set形式来装,减少累赘更简洁

    <pipelines.py>

    # -*- coding: utf-8 -*-
    from ZhiHu.items import ZhihuItem
    from Mysql import MySql
    import mysql.connector
    
    class ZhihuPipeline(object):
        def process_item(self,item,spider):
            if isinstance(item,ZhihuItem):
                try:
                    MySql.insert_db(item)
                    print u'成功!'
                except mysql.connector.IntegrityError as e:
                    print 'mysql.connector.IntegrityError'
                    print u'主键重复,不存入'
    

    这样一来利用主键,实现了有效去重

    </br>
    </br>

    四、断点续爬

    扯来扯去,终于到重点啦@_@
    其实也是知乎上的问题:python爬虫如何断点继续抓取? - 知乎
    非常赞同路人甲大神的建议,断点续爬核心:****记录下最后的状态

    最初的想法是,手动记录到笔记本上。可是每次这样未免太繁琐了,又容易丢,不如写一个函数(中间件),连同抓取的字段一并记录到数据库,需要重新抓取时从数据库调用

    后面发现,想法与这个答案不谋而和:



    </br>

    分析网页最后几页发现其是不变的,只是页码增加,大概知道网站堆积数据的方式,那就将计就计?从最后一页倒过来抓取

    但是我们要注意到,轮子哥的关注量不断增加,两次抓取之间数据是有偏移的!!!

    这么说,比如这一次某一用户的位置在i=10这个位置,下一次爬虫之前新增关注3。下一次爬虫时,它会漂移到i=13这个位置。

    需要考虑的有点多,几个字段

    All_Num —— 目标抓取量(zhihu.py中手动输入)
    Save_Num—— 已经抓取量(items字段 )
    Last_Num —— 上一次爬虫,轮子哥关注量(items字段)
    Now_Num —— 本次爬虫,轮子哥关注量(爬虫开始前,requests方法检测,检测第一页即offset=0时)
    Real_Num —— 记录URL中的i(items字段,数据随着用户关注有偏移,不规则)

    Save_Num,Last_Num,Real_Num都需要调用/保存入数据库。前面我们已经设置好数据库连接、游标等,直接在Mysql.py再自定义一个NumberCheck类获取,最后Zhihu.py中导入

    MySQL索引最后一条数据"SELECT 字段名 FROM 表名 ORDER BY 字段名 DESC LIMIT 1;",DESC表示倒序)

    </br>

    class NumberCheck(object):
        @classmethod
        def find_db_real(cls):
            '''用于每次断点爬虫前,检查数据库中最新插入的一条数据,
            返回最后一条数据的序号'''
            sql="SELECT Real_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"
            cursor.execute(sql)
            result=cursor.fetchall()  #fetchall返回所有数据列表
            for row in result:
                db_num=row[0]
                return db_num
    
        @classmethod
        def find_last(cls):
            '''用于每次断点爬虫前,检查数据库中最新插入的一条数据,
            返回上次关注量'''
            sql = "SELECT Last_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"
            cursor.execute(sql)
            result = cursor.fetchall()
            for row in result:
                last_num = row[0]
                return last_num
    
        @classmethod
        def find_save(cls):
            '''用于每次断点爬虫前,检查数据库中最新插入的一条数据,
            返回总共抓取量'''
            sql = "SELECT Save_Num FROM zhihu ORDER BY Save_Num  DESC LIMIT 1;"
            cursor.execute(sql)
            result = cursor.fetchall()
            for row in result:
                save_num = row[0]
                return save_num
    

    在Zhihu.py中初始化各种参数,第一次抓取和之后是不一样的,详情看注释

    # -*- coding:utf-8 -*-
    import scrapy
    from scrapy.http import Request
    from ZhiHu.items import ZhihuItem
    from ZhiHu.MysqlPipelines.Mysql import NumberCheck
    from scrapy.conf import settings
    from ZhiHu.settings import Tool
    import requests
    import json
    
    class Myspider(scrapy.Spider):
        '''初始化各种参数'''
        name='ZhiHu'
        allowed_domains=['zhihu.com']
        L = ''
        K = ''
    
        All_Num=546049 # 目标抓取量(要保证是该时间最新关注量)
        Save_Num=NumberCheck.find_save() # 已经抓取量
        DB_Num=NumberCheck.find_db_real()  # 上次爬虫,数据库的最后一条数据DB_Num
        Last_Num=NumberCheck.find_last()  # 获得上一次爬虫,轮子哥关注量
    
        #用requests检测最新关注量
        url='xxxxxx(和前面一样)&offset=0'
        response=requests.get(url, headers=settings['DEFAULT_REQUEST_HEADERS'])
        parse=json.loads(response.text)
        try:
            # 获得最新关注者数目Now_Num,注意检验token是否过期
            Now_Num=parse['paging']['totals']
            # 因为关注者不时更新,需计算出真实Real_Num,分两种情况讨论
            # 第一次DB_Num和Last_Num为None,第二次之后不为None
            if DB_Num is not None:
                if Last_Num is not None:
                    Real_Num = DB_Num+(Now_Num-Last_Num)-1
                    Save_Num = Save_Num
            else:
                Real_Num=All_Num
                Save_Num=0
            print u'目标爬取:', All_Num
            print u'已经抓取:', Save_Num
            print u''
            print u'目前关注:',Now_Num
            print u'上次关注:',Last_Num
        except KeyError:
            print u'\n'
            print u'Authorization过期,请停止程序,重新抓取并在settings中更新'
    

    来看下结果:

    分别是关注者为546361和546059的状态(相差300多,大家也可以猜想一下过了多长时间)

    找到网页进行比对

    发现了没有,顺序一致,连上了

    运行过程大概是下面这样(第一次和之后有些不一样)



    结果都不一定,有时立马能续连,有的连续返回几个mysql.connector.ItergrityError(之前已经存过)才连上。不过一般来说,数据越是中间断开,变动也会稍大;断点时间差的越大,变动也可能更大。

    还好的是,我们用MySQL设置主键来实现去重,可以减少变动造成的影响:同时终于可以忽略scrapy本身去重的不完美。(注意self.Real_Num+=1设置的位置)

    此间我们没有考虑爬虫过程中新增用户关注造成的影响,没有考虑取关,没有考虑其它变化。
    一个简陋的“伪动态断点续爬”,大概就是这样。

    </br>
    </br>

    五、总结

    这篇文写的好累呀,还是总结下比较好
    1、爬虫思路:你所没有发现的API,原来可以抓许多字段
    2、Spiders逻辑:主要是数据清洗,值得注意的in dict.keys()方法判断属性是否存在
    3、MySQL存储:
    1)下载安装
    2)可视化工具(Navicat)
    3)基础知识
    4)自定义模块进行存储:设置主键去重,装饰器,python中操作MySQL两种方式—— MySQLdb和mysql connector

    4、断点续爬:核心是记录最后的状态,本篇中采用存入数据库的方式,需要设置各种字段。对于动态更新的项目,断点续爬会有变动,有些麻烦。

    完整代码:github地址
    </br>

    对了想获得山寨版,哦不,经济适用版Navicat下载安装包的童鞋可以关注我的微信公众号:

    然后回复:Navicat,可获取百度云分享链接

    </br>
    么么哒,本篇就是这样啦~

    相关文章

      网友评论

      • 1e29ea3e5360:尝试运行代码,会出现错误,不知道怎么解决。
      • loopq:文章很有启发性,问一句当多线程爬虫遇到断点续爬的情况下,有什么好的建议?
      • d08846da24ce:scrapy做个抓取器还行,但它不是面向业务的
        d08846da24ce:任务生命周期管理,调度,代理,cookie池等等。抓取,分析只是最简单的一环
        Wakingup88688:还有分布式一定要的,正准备看:joy:
        Wakingup88688:您说的是京东淘宝亚马逊一类的电商大数据吧,各种反爬、存储、调度、速度、效率都应该要考虑,所以应该还是得有自己的爬虫框架,不过我还没学到,路漫漫:joy:
      • 梅花鹿数据rieuse:干货满满!很有帮助!
      • JaeGwen:很赞 提供了新思路
      • loading_miracle:写的好详细,很赞,学到不少思想
        Wakingup88688: @loading_miracle 谢谢,晚安啦~
      • 54d7ffabb63a:对了 还有公众号 还有github star
      • 54d7ffabb63a:知乎点赞 然后来简书 喜欢 然后学习
        54d7ffabb63a:一激动 字都打错了 是近距离。。。
        54d7ffabb63a:@Wakingup88688 仅处理 接触 大神 开心 对了我看你说过不熬夜的 :grin:
        Wakingup88688: @石洋洋 好巧呀,我也睡不着😅

      本文标题:Scrapy之断点续爬(存入MySQL)

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