Scrapy之断点续爬

浏览: 2362

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

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


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

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

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


所以你需要——断点续爬。
好吧貌似又胡扯了(其实是喜欢这个表情,强行用上哈哈)

本次目标:
爬取轮子哥54万关注用户信息,存入MySQL,实现断点续爬

一、爬虫思路

关于这个话题网上的教程也非常多,一般是设法获取_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其中一部分字段,全部字段比这还多,为了防止查水表还是不贴上了,想要可以私信。

二、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等设为全局变量。
最后获得这样,美丽的数据,不容易


三、存储入MySQL

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

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

不心累了,心痛。。


你看,各种坑,可能是因为windows情况许多情况必需用管理员命令才能运行。
迫切地想换linux系统!!!

2、可视化工具
MySQL可视化工具很多,例如:20个最佳MySQL GUI的可视化管理工具
也可参考某个社区网友推荐:最好用的mysql可视化工具是什么? - 开源中国社区

我试了好几个,觉得MySQL-Front 简洁易上手,SQL-yog功能超级强大。
最喜欢还是Navicat,真的很好用,强力推荐哈哈。

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

傻乎乎去找验证码,结果发现网上的都是不能用的,套路啊。最后还是找到了一个中文版navicat,用山寨验证码,验证山寨navicat,成功。虽然比较简陋,也和正式版一样好用,小小确幸?

不熟悉MySQL语法也没关系,利用Navicat可快速进行一些设置:
引擎

字段

name等文本型


gender等数字型

主键


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

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


3、MySQL基础知识
mysql查询:【图文】mysql查询数据相关操作_百度文库
Python中操作mysql:python 操作MySQL数据库 | 菜鸟教程

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'主键重复,不存入'

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

四、断点续爬

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

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

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


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

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

这么说,比如这一次某一用户的位置在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表示倒序)

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设置的位置)

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

五、总结

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

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

完整代码:github地址

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

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


么么哒,本篇就是这样啦~

推荐 2
本文由 waking_up88688 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册