Scrapy之斷點續爬
Scrapy第五篇:斷點續爬 | 存入MySQL
不得不說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=+str(i) n
這麼多欄位哪裡來?好多天前了,抓包過程中無意發現的,easy(相信有盆友早就知道了)
其實這只是Api其中一部分欄位,全部欄位比這還多,為了防止查水表還是不貼上了,想要可以私信。
二、Spiders邏輯
調用Api非常簡單,但是想防止被ban,要做好偽裝。random.choice設置動態U-A。原以為還需要到代理Ip,沒想到可以應付得過去。還是要注意爬蟲的友好性,下載延遲3s。就不贅述。
需要注意的是header盡量模擬瀏覽器,authorization參數一定要帶上,如果不帶上無法返回數據,同時這個參數是有有效期限的,失效返回401,需要重新抓取。
DEFAULT_REQUEST_HEADERS = {n accept:application/json, text/plain, */*,n Accept-Encoding:gzip, deflate, sdch,n Accept-Language:zh-CN,zh;q=0.8,n authorization:xxx(自己抓),n Connection:keep-alive,n Host:www.zhihu.com,n Referer:https://www.zhihu.com/people/excited-vczh/followers,n x-udid:AIDAV7HBLwqPTulyfqA9p0CFbRlOL10cidE=,n User-Agent:random.choice(USER_AGENTS)n}n
接著就是提取數據的部分,打開上面的URL,返回一堆Json格式數據
自定義一個Too類清洗headline(簽名)、description(個人簡介),為了方便調用放在settings中還有locations(居住地址)和business(行業),不是所有用戶都填寫的,需要判斷
還有educations(教育經歷)和employments(職業經歷),有各種可能,以前者來說:
1、有school、有major 2、有school、無major
3、無school、有major 4、無school、無major
同時還可能有若干項,比如某某用戶教育經歷:
xx小學 xx初中 xx高中 xx 大學
用 in dict.keys() 方法判斷dict中某一屬性是否存在。一堆if/else,一點也不優雅,無可奈何
if len(i[educations])== 0:n item[educations]=nelse:n content=[]n for n in i[educations]:n S=school in n.keys()n M=major in n.keys()n if S:n if M:n self.L=n[school][name]+/+n[major][name]n else:n self.L=n[school][name]n else:n self.L=n[major][name]n content.append(self.L)n item[educations]=n for l in content:n item[educations]+=l+ n
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等數字型
- 主鍵
可以看到,字符集儘可能選擇了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 -*-n#導入settings中各種參數nfrom ZhiHu import settingsnimport mysql.connectornn# 使用connector連接到資料庫ndb=mysql.connector.Connect(user=settings.MYSQL_USER, host=settings.MYSQL_HOST, port=settings.MYSQL_PORT,password=settings.MYSQL_PASSWORD, database=settings.MYSQL_DB_NAME)n# 初始化操作游標,buffer指的是使用客戶端的緩衝區,減少本機伺服器的壓力ncursor=db.cursor(buffered=True)nnprint u成功連接資料庫!nnclass MySql(object):n @classmethodn def insert_db(cls,item):n 數據插入資料庫,用到裝飾器n 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)"nn values=(item[name], item[headline], item[description], item[detailURL], item[gender],n item[user_type], item[is_active], item[locations], item[business],n item[educations], item[employments], item[following_count], item[follower_count],n item[mutual_followees_count], item[voteup_count], item[thanked_count], item[favorited_count],n item[logs_count], item[following_question_count], item[following_topic_count],n item[following_favlists_count], item[following_columns_count], item[articles_count],n item[question_count], item[answer_count], item[pins_count], item[participated_live_count],n item[hosted_live_count])nn cursor.execute(sql,values)n db.commit()n
將dict改成set形式來裝,減少累贅更簡潔
<pipelines.py>
# -*- coding: utf-8 -*-nfrom ZhiHu.items import ZhihuItemnfrom Mysql import MySqlnimport mysql.connectornnclass ZhihuPipeline(object):n def process_item(self,item,spider):n if isinstance(item,ZhihuItem):n try:n MySql.insert_db(item)n print u成功!n except mysql.connector.IntegrityError as e:n print mysql.connector.IntegrityErrorn print u主鍵重複,不存入n
這樣一來利用主鍵,實現了有效去重
四、斷點續爬
扯來扯去,終於到重點啦哈哈
其實也是知乎上的問題: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):n @classmethodn def find_db_real(cls):n 用於每次斷點爬蟲前,檢查資料庫中最新插入的一條數據,n 返回最後一條數據的序號n sql="SELECT Real_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"n cursor.execute(sql)n result=cursor.fetchall() #fetchall返回所有數據列表n for row in result:n db_num=row[0]n return db_numnn @classmethodn def find_last(cls):n 用於每次斷點爬蟲前,檢查資料庫中最新插入的一條數據,n 返回上次關注量n sql = "SELECT Last_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"n cursor.execute(sql)n result = cursor.fetchall()n for row in result:n last_num = row[0]n return last_numnn @classmethodn def find_save(cls):n 用於每次斷點爬蟲前,檢查資料庫中最新插入的一條數據,n 返回總共抓取量n sql = "SELECT Save_Num FROM zhihu ORDER BY Save_Num DESC LIMIT 1;"n cursor.execute(sql)n result = cursor.fetchall()n for row in result:n save_num = row[0]n return save_numn
在Zhihu.py中初始化各種參數,第一次抓取和之後是不一樣的,詳情看注釋
from ZhiHu.MysqlPipelines.Mysql import NumberChecknfrom scrapy.conf import settingsnimport requestsnnclass Myspider(scrapy.Spider):n 初始化各種參數n name=ZhiHun allowed_domains=[zhihu.com]n L = n K = nn All_Num=546049 # 目標抓取量(要保證是該時間最新關注量)n Save_Num=NumberCheck.find_save() # 已經抓取量n DB_Num=NumberCheck.find_db_real() # 上次爬蟲,資料庫的最後一條數據DB_Numn Last_Num=NumberCheck.find_last() # 獲得上一次爬蟲,輪子哥關注量nn #用requests檢測最新關注量n url=xxxxxx(和前面一樣)&offset=0n response=requests.get(url, headers=settings[DEFAULT_REQUEST_HEADERS])n parse=json.loads(response.text)n try:n # 獲得最新關注者數目Now_Num,注意檢驗token是否過期n Now_Num=parse[paging][totals]n # 因為關注者不時更新,需計算出真實Real_Num,分兩種情況討論n # 第一次DB_Num和Last_Num為None,第二次之後不為Nonen if DB_Num is not None:n if Last_Num is not None:n Real_Num = DB_Num+(Now_Num-Last_Num)-1n Save_Num = Save_Numn else:n Real_Num=All_Numn Save_Num=0n print u目標爬取:, All_Numn print u已經抓取:, Save_Numn print un print u目前關註:,Now_Numn print u上次關註:,Last_Numn except KeyError:n print unn print uAuthorization過期,請停止程序,重新抓取並在settings中更新n
來看下結果:
分別是關注者為546361和546059的狀態(相差300多,大家也可以猜想一下過了多長時間),找到網頁進行比對
發現了沒有,順序一致,連上了運行過程大概是下面這樣(第一次和之後有些不一樣)
結果都不一定,有時立馬能續連,有的連續返回幾個mysql.connector.ItergrityError(之前已經存過)才連上。不過一般來說,數據越是中間斷開,變動也會稍大;斷點時間差的越大,變動也可能更大。還好的是,我們用MySQL設置主鍵來實現去重,可以減少變動造成的影響:同時終於可以忽略scrapy本身去重的不完美。(注意self.Real_Num+=1設置的位置)
此間我們沒有考慮爬蟲過程中新增用戶關注造成的影響,沒有考慮取關,沒有考慮其它變化。
一個簡陋的「偽動態斷點續爬」,大概就是這樣。五、總結
這篇文寫的好累呀,還是總結下比較好
1、爬蟲思路:你所沒有發現的API,原來可以抓許多欄位
2、Spiders邏輯:主要是數據清洗,值得注意的 in dict.keys()方法判斷屬性是否存在
3、MySQL存儲:
- 下載安裝
- 可視化工具(Navicat)
- 基礎知識
- 自定義模塊進行存儲:設置主鍵去重,裝飾器,python中操作MySQL兩種方式—— MySQLdb和mysql connector
完整代碼:github地址
對了想獲得山寨版,哦不,經濟適用版Navicat 下載安裝包 的童鞋
可以關注我的微信公眾號:
然後回復:Navicat,可獲取百度雲分享鏈接
么么噠,本篇就是這樣啦~
推薦閱讀:
※Python 中 Requests 庫的用法
※好像發現了一個不錯的小工具
※反反爬蟲利器!教你怎麼用代理,撥號換IP……
※使用webhook結合python腳本實現自動化部署