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等數字型

  • 主鍵

看到這把鑰匙了么?主鍵設置為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 -*-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

4、斷點續爬:核心是記錄最後的狀態,本篇中採用存入資料庫的方式,需要設置各種欄位。對於動態更新的項目,斷點續爬會有變動,有些麻煩。

完整代碼:github地址

對了想獲得山寨版,哦不,經濟適用版Navicat 下載安裝包 的童鞋

可以關注我的微信公眾號:

然後回復:Navicat,可獲取百度雲分享鏈接

么么噠,本篇就是這樣啦~


推薦閱讀:

Python 中 Requests 庫的用法
好像發現了一個不錯的小工具
反反爬蟲利器!教你怎麼用代理,撥號換IP……
使用webhook結合python腳本實現自動化部署

TAG:Python | 爬虫 | 断点续传 |