Scrapy第五篇:断点续爬 | 存入MySQL
五一前后疯癫玩了一周(纯玩耍真的),然后又应付本专业各种作业、PPT?
本来想先解决IP这一块,结果被坑惨了,辗转两天先跳过,心累
好久不学习,惭愧,不说了我们回归正题。
不得不说scrapy真的是一个强大的框架,配上轻灵简洁的mongodb,只需极少代码便可应付一个简单爬虫。但如果几十万几百万的数据量,需要考虑的因素就很多了,速度、存储、代理Ip应对反爬等等。
还有一个重要的,比如某一天你好不容易写完代码,心想“这下好了,接下来全部丢给机器我真机智哈哈哈”,于是睡觉/玩耍/撩妹美滋滋,回来一看,不巧,断网/IP被封/奇异bug,这时你的内心是这样的:
好气呀,对不对?
所以你需要——断点续爬。
好吧貌似又胡扯了(其实是喜欢这个表情,强行用上哈哈)
</br>
本次目标:
爬取轮子哥54万关注用户信息,存入MySQL,实现断点续爬
</br>
</br>
一、爬虫思路
关于这个话题网上的教程也非常多,一般是设法获取_xsrf参数然后模拟登陆,再进行抓取。
比如下面这一篇写得真的很不错:爬虫(3)--- 一起来爬知乎,作者是简书的whenif
爬取关注用户其实更简单,可以直接调用API
一般用开发者工具抓包,只能获得以下字段
本篇将获得以下字段:
大概30来项,调用URL:
这么多字段哪里来?好多天前了,抓包过程中无意发现的,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>
么么哒,本篇就是这样啦~
网友评论