Scrapy第五篇:斷點(diǎn)續(xù)爬 | 存入MySQL
五一前后瘋癲玩了一周(純玩耍真的)昼窗,然后又應(yīng)付本專業(yè)各種作業(yè)钝的、PPT缸夹?
本來想先解決IP這一塊燎字,結(jié)果被坑慘了,輾轉(zhuǎn)兩天先跳過勾笆,心累
好久不學(xué)習(xí)敌蚜,慚愧,不說了我們回歸正題窝爪。
不得不說scrapy真的是一個(gè)強(qiáng)大的框架弛车,配上輕靈簡(jiǎn)潔的mongodb齐媒,只需極少代碼便可應(yīng)付一個(gè)簡(jiǎn)單爬蟲。但如果幾十萬幾百萬的數(shù)據(jù)量纷跛,需要考慮的因素就很多了喻括,速度、存儲(chǔ)贫奠、代理Ip應(yīng)對(duì)反爬等等唬血。
還有一個(gè)重要的,比如某一天你好不容易寫完代碼唤崭,心想“這下好了拷恨,接下來全部丟給機(jī)器我真機(jī)智哈哈哈”,于是睡覺/玩耍/撩妹美滋滋谢肾,回來一看腕侄,不巧,斷網(wǎng)/IP被封/奇異bug芦疏,這時(shí)你的內(nèi)心是這樣的:
好氣呀冕杠,對(duì)不對(duì)?
所以你需要——斷點(diǎn)續(xù)爬酸茴。
好吧貌似又胡扯了(其實(shí)是喜歡這個(gè)表情拌汇,強(qiáng)行用上哈哈)
</br>
本次目標(biāo):
爬取輪子哥54萬關(guān)注用戶信息,存入MySQL弊决,實(shí)現(xiàn)斷點(diǎn)續(xù)爬
</br>
</br>
一、爬蟲思路
關(guān)于這個(gè)話題網(wǎng)上的教程也非常多魁淳,一般是設(shè)法獲取_xsrf參數(shù)然后模擬登陸,再進(jìn)行抓取飘诗。
比如下面這一篇寫得真的很不錯(cuò):爬蟲(3)--- 一起來爬知乎,作者是簡(jiǎn)書的whenif
爬取關(guān)注用戶其實(shí)更簡(jiǎn)單界逛,可以直接調(diào)用API
一般用開發(fā)者工具抓包昆稿,只能獲得以下字段
本篇將獲得以下字段:
大概30來項(xiàng),調(diào)用URL:
這么多字段哪里來息拜?好多天前了溉潭,抓包過程中無意發(fā)現(xiàn)的,easy(相信有盆友早就知道了)
其實(shí)這只是Api其中一部分字段少欺,全部字段比這還多喳瓣,為了防止查水表還是不貼上了,想要可以私信赞别。
</br>
</br>
二畏陕、Spiders邏輯
調(diào)用Api非常簡(jiǎn)單,但是想防止被ban仿滔,要做好偽裝惠毁。random.choice設(shè)置動(dòng)態(tài)U-A犹芹。原以為還需要到代理Ip,沒想到可以應(yīng)付得過去鞠绰。還是要注意爬蟲的友好性腰埂,下載延遲3s。就不贅述蜈膨。
需要注意的是header盡量模擬瀏覽器屿笼,'authorization'參數(shù)一定要帶上,如果不帶上無法返回?cái)?shù)據(jù)丈挟,同時(shí)這個(gè)參數(shù)是有有效期限的刁卜,失效返回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)}
接著就是提取數(shù)據(jù)的部分蛔趴,打開上面的URL,返回一堆Json格式數(shù)據(jù)
自定義一個(gè)Too類清洗headline(簽名)例朱、description(個(gè)人簡(jiǎn)介),為了方便調(diào)用放在settings中孝情;還有l(wèi)ocations(居住地址)和business(行業(yè)),不是所有用戶都填寫的洒嗤,需要判斷箫荡;還有educations(教育經(jīng)歷)和employments(職業(yè)經(jīng)歷),有各種可能,以前者來說:
1渔隶、有'school'羔挡、有'major'
2、有'school'间唉、無'major'
2绞灼、無'school'、有'major'
4呈野、無'school'低矮、無'major'
同時(shí)還可能有若干項(xiàng),比如某某用戶教育經(jīng)歷:
1被冒、xx小學(xué)
2军掂、xx初中
3、xx高中
4昨悼、xx大學(xué)
用in dict.keys() 方法判斷dict中某一屬性是否存在
一堆if/else,一點(diǎn)也不優(yōu)雅蝗锥,無可奈何
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等設(shè)為全局變量率触。
最后獲得這樣玛追,美麗的數(shù)據(jù),不容易
</br>
</br>
三、存儲(chǔ)入MySQL
1痊剖、首先下載安裝MySQL及服務(wù)
如果是5.7以后的windows系統(tǒng)韩玩,推薦以下教程,很詳細(xì):
windows安裝MySQL數(shù)據(jù)庫(非安裝版),設(shè)置編碼為utf8
我是之前某天安裝的陆馁,忘記密碼找颓,上網(wǎng)搜到一個(gè)可以跳過驗(yàn)證的教程,好不容易通過叮贩。
第二天電腦重啟击狮,無法連接了,搜了網(wǎng)上各種方法無果益老。糊里糊涂又安裝了個(gè)5.6.36版本,還是不行彪蓬。最后全部卸載并強(qiáng)力刪除,重新又安裝了一個(gè)5.7版本的捺萌。為此注冊(cè)了個(gè)oracle賬戶档冬,弄到一半發(fā)現(xiàn)原來已經(jīng)有賬戶了,重新郵箱驗(yàn)證找回密碼桃纯,登錄酷誓,下載,安裝态坦,終于...
不心累了盐数,心痛。伞梯。
你看玫氢,各種坑,可能是因?yàn)閣indows情況許多情況必需用管理員命令才能運(yùn)行谜诫。
迫切地想換linux系統(tǒng)Q俊!猜绣!
</br>
2、可視化工具
MySQL可視化工具很多敬特,例如:20個(gè)最佳MySQL GUI的可視化管理工具
也可參考某個(gè)社區(qū)網(wǎng)友推薦:最好用的mysql可視化工具是什么掰邢? - 開源中國(guó)社區(qū)
</br>
我試了好幾個(gè),覺得MySQL-Front 簡(jiǎn)潔易上手伟阔,SQL-yog功能超級(jí)強(qiáng)大辣之。
最喜歡還是Navicat,真的很好用,強(qiáng)力推薦哈哈皱炉。
比較不巧的是怀估,想要長(zhǎng)久服務(wù)得購(gòu)買,官方正版只有14天試用期,需要驗(yàn)證碼多搀。
傻乎乎去找驗(yàn)證碼歧蕉,結(jié)果發(fā)現(xiàn)網(wǎng)上的都是不能用的,套路啊康铭。最后還是找到了一個(gè)中文版navicat惯退,用山寨驗(yàn)證碼,驗(yàn)證山寨navicat从藤,成功催跪。雖然比較簡(jiǎn)陋,也和正式版一樣好用夷野,小小確幸懊蒸?
</br>
不熟悉MySQL語法也沒關(guān)系,利用Navicat可快速進(jìn)行一些設(shè)置:
引擎
</br>
字段
name等文本型
gender等數(shù)字型
</br>
主鍵
看到這把鑰匙了么悯搔?
主鍵設(shè)置為detailURL(用戶主頁地址)骑丸,每一個(gè)用戶唯一,機(jī)智如我哈哈
可以看到鳖孤,字符集盡可能選擇了utf8( utf8而不是utf-8)者娱,為什么呢?
MySQL默認(rèn)使用字符集latin1苏揣,不支持中文黄鳍。如果不設(shè)置成utf8,會(huì)出現(xiàn)下面這樣:
</br>
3平匈、MySQL基礎(chǔ)知識(shí)
mysql查詢:【圖文】mysql查詢數(shù)據(jù)相關(guān)操作_百度文庫
Python中操作mysql:python 操作MySQL數(shù)據(jù)庫 | 菜鳥教程
</br>
4框沟、自定義模塊存儲(chǔ)
這里參考了博文:小白進(jìn)階之Scrapy第一篇 | 靜覓 - Part 3
使用Mysql-connector操作mysql(當(dāng)然也可以用上面的MySQLdb方法)
但是覺得里面有些部分有些冗贅,而且我們已經(jīng)設(shè)置了主鍵增炭,改成下面這樣
<Mysql.py>
在這之前需要在settings中設(shè)置各種MySQL配置參數(shù)忍燥,這里不贅述
# --coding:utf-8 --
#導(dǎo)入settings中各種參數(shù)
from ZhiHu import settings
import mysql.connector
# 使用connector連接到數(shù)據(jù)庫
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)
# 初始化操作游標(biāo),buffer指的是使用客戶端的緩沖區(qū)隙姿,減少本機(jī)服務(wù)器的壓力
cursor=db.cursor(buffered=True)
print u'成功連接數(shù)據(jù)庫梅垄!'
class MySql(object):
@classmethod
def insert_db(cls,item):
'''數(shù)據(jù)插入數(shù)據(jù)庫,用到裝飾器'''
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形式來裝输玷,減少累贅更簡(jiǎn)潔
<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'主鍵重復(fù),不存入'
這樣一來利用主鍵欲鹏,實(shí)現(xiàn)了有效去重
</br>
</br>
四机久、斷點(diǎn)續(xù)爬
扯來扯去,終于到重點(diǎn)啦@_@
其實(shí)也是知乎上的問題:python爬蟲如何斷點(diǎn)繼續(xù)抓扰夂俊膘盖? - 知乎
非常贊同路人甲大神的建議胧弛,斷點(diǎn)續(xù)爬核心:****記錄下最后的狀態(tài)。
最初的想法是侠畔,手動(dòng)記錄到筆記本上结缚。可是每次這樣未免太繁瑣了践图,又容易丟掺冠,不如寫一個(gè)函數(shù)(中間件),連同抓取的字段一并記錄到數(shù)據(jù)庫码党,需要重新抓取時(shí)從數(shù)據(jù)庫調(diào)用德崭。
后面發(fā)現(xiàn),想法與這個(gè)答案不謀而和:
</br>
分析網(wǎng)頁最后幾頁發(fā)現(xiàn)其是不變的揖盘,只是頁碼增加眉厨,大概知道網(wǎng)站堆積數(shù)據(jù)的方式,那就將計(jì)就計(jì)兽狭?從最后一頁倒過來抓取憾股。
但是我們要注意到,輪子哥的關(guān)注量不斷增加箕慧,兩次抓取之間數(shù)據(jù)是有偏移的7颉!颠焦!
這么說斩熊,比如這一次某一用戶的位置在i=10這個(gè)位置,下一次爬蟲之前新增關(guān)注3伐庭。下一次爬蟲時(shí)粉渠,它會(huì)漂移到i=13這個(gè)位置。
需要考慮的有點(diǎn)多圾另,幾個(gè)字段
All_Num —— 目標(biāo)抓取量(zhihu.py中手動(dòng)輸入)
Save_Num—— 已經(jīng)抓取量(items字段 )
Last_Num —— 上一次爬蟲霸株,輪子哥關(guān)注量(items字段)
Now_Num —— 本次爬蟲,輪子哥關(guān)注量(爬蟲開始前集乔,requests方法檢測(cè)去件,檢測(cè)第一頁即offset=0時(shí))
Real_Num —— 記錄URL中的i(items字段,數(shù)據(jù)隨著用戶關(guān)注有偏移扰路,不規(guī)則)
Save_Num,Last_Num,Real_Num都需要調(diào)用/保存入數(shù)據(jù)庫尤溜。前面我們已經(jīng)設(shè)置好數(shù)據(jù)庫連接、游標(biāo)等幼衰,直接在Mysql.py再自定義一個(gè)NumberCheck類獲取靴跛,最后Zhihu.py中導(dǎo)入
(MySQL索引最后一條數(shù)據(jù):"SELECT 字段名 FROM 表名 ORDER BY 字段名 DESC LIMIT 1;"缀雳,DESC表示倒序)
</br>
class NumberCheck(object):
@classmethod
def find_db_real(cls):
'''用于每次斷點(diǎn)爬蟲前渡嚣,檢查數(shù)據(jù)庫中最新插入的一條數(shù)據(jù),
返回最后一條數(shù)據(jù)的序號(hào)'''
sql="SELECT Real_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"
cursor.execute(sql)
result=cursor.fetchall() #fetchall返回所有數(shù)據(jù)列表
for row in result:
db_num=row[0]
return db_num
@classmethod
def find_last(cls):
'''用于每次斷點(diǎn)爬蟲前,檢查數(shù)據(jù)庫中最新插入的一條數(shù)據(jù)识椰,
返回上次關(guān)注量'''
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):
'''用于每次斷點(diǎn)爬蟲前绝葡,檢查數(shù)據(jù)庫中最新插入的一條數(shù)據(jù),
返回總共抓取量'''
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中初始化各種參數(shù)腹鹉,第一次抓取和之后是不一樣的藏畅,詳情看注釋
# -*- 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):
'''初始化各種參數(shù)'''
name='ZhiHu'
allowed_domains=['zhihu.com']
L = ''
K = ''
All_Num=546049 # 目標(biāo)抓取量(要保證是該時(shí)間最新關(guān)注量)
Save_Num=NumberCheck.find_save() # 已經(jīng)抓取量
DB_Num=NumberCheck.find_db_real() # 上次爬蟲,數(shù)據(jù)庫的最后一條數(shù)據(jù)DB_Num
Last_Num=NumberCheck.find_last() # 獲得上一次爬蟲功咒,輪子哥關(guān)注量
#用requests檢測(cè)最新關(guān)注量
url='xxxxxx(和前面一樣)&offset=0'
response=requests.get(url, headers=settings['DEFAULT_REQUEST_HEADERS'])
parse=json.loads(response.text)
try:
# 獲得最新關(guān)注者數(shù)目Now_Num,注意檢驗(yàn)token是否過期
Now_Num=parse['paging']['totals']
# 因?yàn)殛P(guān)注者不時(shí)更新愉阎,需計(jì)算出真實(shí)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'目標(biāo)爬取:', All_Num
print u'已經(jīng)抓攘Ψ堋:', Save_Num
print u''
print u'目前關(guān)注:',Now_Num
print u'上次關(guān)注:',Last_Num
except KeyError:
print u'\n'
print u'Authorization過期榜旦,請(qǐng)停止程序,重新抓取并在settings中更新'
來看下結(jié)果:
分別是關(guān)注者為546361和546059的狀態(tài)(相差300多景殷,大家也可以猜想一下過了多長(zhǎng)時(shí)間)
找到網(wǎng)頁進(jìn)行比對(duì)
發(fā)現(xiàn)了沒有溅呢,順序一致,連上了
運(yùn)行過程大概是下面這樣(第一次和之后有些不一樣)
結(jié)果都不一定猿挚,有時(shí)立馬能續(xù)連咐旧,有的連續(xù)返回幾個(gè)mysql.connector.ItergrityError(之前已經(jīng)存過)才連上。不過一般來說绩蜻,數(shù)據(jù)越是中間斷開铣墨,變動(dòng)也會(huì)稍大;斷點(diǎn)時(shí)間差的越大辜羊,變動(dòng)也可能更大踏兜。
還好的是,我們用MySQL設(shè)置主鍵來實(shí)現(xiàn)去重八秃,可以減少變動(dòng)造成的影響:同時(shí)終于可以忽略scrapy本身去重的不完美碱妆。(注意self.Real_Num+=1設(shè)置的位置)
此間我們沒有考慮爬蟲過程中新增用戶關(guān)注造成的影響,沒有考慮取關(guān)昔驱,沒有考慮其它變化疹尾。
一個(gè)簡(jiǎn)陋的“偽動(dòng)態(tài)斷點(diǎn)續(xù)爬”,大概就是這樣骤肛。
</br>
</br>
五纳本、總結(jié)
這篇文寫的好累呀,還是總結(jié)下比較好
1腋颠、爬蟲思路:你所沒有發(fā)現(xiàn)的API繁成,原來可以抓許多字段
2、Spiders邏輯:主要是數(shù)據(jù)清洗淑玫,值得注意的in dict.keys()方法判斷屬性是否存在
3巾腕、MySQL存儲(chǔ):
1)下載安裝
2)可視化工具(Navicat)
3)基礎(chǔ)知識(shí)
4)自定義模塊進(jìn)行存儲(chǔ):設(shè)置主鍵去重面睛,裝飾器,python中操作MySQL兩種方式—— MySQLdb和mysql connector
4尊搬、斷點(diǎn)續(xù)爬:核心是記錄最后的狀態(tài)叁鉴,本篇中采用存入數(shù)據(jù)庫的方式,需要設(shè)置各種字段佛寿。對(duì)于動(dòng)態(tài)更新的項(xiàng)目幌墓,斷點(diǎn)續(xù)爬會(huì)有變動(dòng),有些麻煩冀泻。
完整代碼:github地址
</br>
對(duì)了想獲得山寨版常侣,哦不,經(jīng)濟(jì)適用版Navicat下載安裝包的童鞋可以關(guān)注我的微信公眾號(hào):
然后回復(fù):Navicat弹渔,可獲取百度云分享鏈接
</br>
么么噠袭祟,本篇就是這樣啦~