Scrapy之断点续爬(存入MySQL)

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可快速进行一些设置:
引擎


</br>
字段
name等文本型


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>
么么哒,本篇就是这样啦~

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

推荐阅读更多精彩内容