標籤:

【爬蟲】用Scrapy爬取知乎用戶信息

這一篇記得相當凌亂。

更一個Scrapy手冊:Scrapy入門教程 - Scrapy 0.24.6 文檔

使用Scrapy的基本步驟是:

  1. 創建一個Scrapy項目
  2. 定義提取的Item
  3. 編寫爬取網站的 spider 並提取 Item
  4. 編寫 Item Pipeline 來存儲提取到的Item(即數據)

其中,第三步為了創建一個Spider,您必須繼承 scrapy.Spider 類, 且定義以下三個屬性:

  • name: 用於區別Spider。
  • start_urls: 包含了Spider在啟動時進行爬取的url列表, 後續的URL則從初始的URL獲取到的數據中提取。
  • parse() 是spider的一個方法,輸入是 Response 對象,輸出是Item對象或新的Request對象。

執行流程:

1、Scrapy為Spider的 start_urls 屬性中的每個URL創建了 scrapy.Request 對象,它的回調函數默認是parse()。

2、Request對象經過調度,執行生成 scrapy.http.Response 對象並送回給spider parse() 方法。

parse()中從網頁中提取數據,會用到Selectors選擇器。

Selector有四個基本的方法:

  • response.selector.xpath(): 傳入xpath表達式,返回該表達式所對應的所有節點的selector list列表 。
  • response.selector.css(): 傳入CSS表達式,返回該表達式所對應的所有節點的selector list列表.
  • response.selector.extract(): 序列化該節點為unicode字元串並返回list。
  • response.selector.re() 根據傳入的正則表達式對數據進行提取,返回unicode字元串list列表。

注意:當返回是selector對象時,需要用extract()或者extract_one()提取,才是內容


幾乎照搬:

騰訊雲技術社區:利用Scrapy爬取所有知乎用戶詳細信息並存至MongoDB

1、首先是創建project-創建genspider

這樣就有了基本的目錄結構:

不過這時候嘗試請求知乎主頁一次,返回的狀態碼是500:

因為……Request Headers中還沒有加User-Agent,改!

User-Agent的取值可以直接從取瀏覽器拿,但是Python代碼寫在哪兒呢?

2、這裡我們來看一下scrapy的源碼中,關於requests請求的生成,有兩個方法:

一個是Spider.start_requests(self),還有一個是Spider.make_requests_from_url(self, url)。

順便來分別看看二者的實現,以及關係:

make_requests_from_url,就是進去一個url,出來一個Request對象

默認start_requests遍歷start_urls列表,對其中每個url返回一個Request對象。但如果make_requests_from_url()方法被重載了,就換成:對每個url調用make_requests_from_url()方法。

可以看出,如果兩個方法都不做重載的話,是等效的。但往往我們會根據需求改寫,而且一般是寫在start_requests()。原因是:

"Spider.make_requests_from_url method is deprecated; it ""wont be called in future Scrapy releases. Please ""override Spider.start_requests method instead (see %s.%s)."也就是說,make_requests_from_url()方法只會在第一輪爬取時被調用,往後就不會再調用了。所以如果後面還想調用的話,應該寫在start_requests()裡面。

現在回到剛剛的問題來,我們要解決請求失敗的問題,猜想是因為request headers裡面沒有User-Agent而不成功,到底是不是這樣呢?

查看Request類的源碼,看到默認確實headers=None

於是,我們就重寫start_requests()方法,其中構造Request實例的時候,傳入headers參數。

重寫start_requests方法,在請求的header中加上User-Agent,再爬一次,可以正常拿到響應了!

(提醒這裡除了yield Request一句,還要記得在開頭引入class Request,原本沒有引入是因為原本這一頁沒有構造Request,另外url也記得要顯示的給,原本這些都是封裝在start_requests方法裡面的)

哦對,這個也改成False……不要遵守君子協議……

對了,我們要爬的是用戶的關注者,這樣才能從1個用戶-10個關注者-100個關注者……一直一直爬下去。

所以url得換成這個:

爬一次報401……headers還不夠豐富,還要加authorization:欄位,才能獲得正確的響應

request headers太長,每次在構造Request對象的時候傳入太麻煩,可以寫進settings.py,本來也是固定的

3、我們的任務是爬用戶信息,那麼既然已經可以拿到response,現在這一步就是抽取數據。

根據response的不同,這裡有兩種情況:

(1)response是用戶信息,回調parse_user(),需要做的僅僅提取響應的欄位(相對簡單)

(2)response是follows或者followers,回調parsefollows()或者parse_followers()。這時需要對每個follow或者folloer,再次發送一次請求,才可以拿到每個人的用戶信息。

首先是要有一個自己的Item對象,因為parse()方法中需要構造它的實例,用於裝在parse後的數據。

於是,首先在iterms.py構造自己的Item類,指定一個Item會包含哪些field。之後在class ZhihuSpider的parse()方法中實例化這個類。

parse()方法是start_requests()的默認回調方法,在start_requests()r請求到response後,就會回調parse()並將response作為參數傳進去,實現對response的解析。

(當然這裡是為了使方法的名稱具有更明確的含義,上述方法的名稱修改過了。)

修改前

修改後

定義UserItem後記得在zhihu.py中引入:

from zhihuuser.items import UserItem

這些欄位構成了用戶的信息,其中爬到的url_token是一個比較關鍵的欄位,會參與url構造、

url_token,我的就是wei-wei-34-52

對用戶信息的解析,從response,提取所需要的field,然後集中在item對象里一起返回

然後對用戶followers和follows的解析是同樣的道理,所以總共:

這裡start_requests()中將有3個yield,對應回調的parse(方法也是3個.

相比用戶信息頁,follows這頁的解析稍微複雜:如果data不為空,表示有關注者,就要對每一個關注者再發一次請求,如果paging的is_end不為True,表示還有下一頁,要對下一頁再發一次請求

然後這裡既然涉及到offset構造url了,那我們順便把url的公式構造出來,剛才一直是用固定url來的。

請求的url很長,看起來雜亂無章,其實參數可以在瀏覽器下面找到:

也就是這個include欄位比較長……

把有變化的欄位提到外面,再用str.format()賦值,看起來會比較清晰

3個parse方法的代碼:

def parse_user(self, response): data=json.loads(response.text)#json格式反序列化 item=UserItem() for field in item.fields:#item.fields列出item的所有的屬性 if field in data.keys():#如果我們想要的欄位,確實爬到了相應的數據,就用它填充 item[field]=data.get(field) yield item def parse_follows(selfs,response): data = json.loads(response.text) # json格式反序列化 if data in data.keys():#有關注的人,才有data欄位 for follow in data.get(data): url_token=follow.get(url_token) yield Request(self.user_url.format(user=url_token, include=self.user_query), callback=self.parse_user) if paging in data.keys() and data.get(paging).get(is_end)==False: yield Request(data.get(paging).get(next),callback=self.parse_follows) def parse_followers(self,response): data = json.loads(response.text) # json格式反序列化 if data in data.keys():#有關注者,才有data欄位 for follower in data.get(data): url_token=follower.get(url_token) yield Request(self.user_url.format(user=url_token, include=self.user_query), callback=self.parse_user) if paging in data.keys() and data.get(paging).get(is_end)==False: yield Request(data.get(paging).get(next),callback=self.parse_followers)

4、至此,數據的抽取已經完成了,終端運行spider可以看到源源不斷的數據正在產生……

最後一步是保存到MongDB,這裡需要用到Pipeline這個組件。

首先是自定義一個Pipiline對象,用於存庫,這裡我們命名為MongoPipeline,其中最關鍵的是實現process.item()方法,真正的寫入數據就在這裡。

同時還實現了其他幾個方法,用於在spider啟動的時候連接到db,做好準備,以及spider關閉的時候連接也一起關閉。

最後要記得在settings.py中加入MongoPipeline,它才能生效。

命令時有敲錯,可以在scrapy shell下檢查:

scrapy check

看吧,我這裡有錯……

原因是,忘記把這兩個配置信息寫進去了……加上再scrapy check就好了

5、最後,讓程序運行起來……

我們用Robomongo這個工具連接到MongoDB看看效果。發現一個問題:

爬了很久,庫里始終只有50個人!

主要原因還是程序里有錯誤:

檢查url格式,可以知道,這是在爬用戶信息……

返回了200,但沒有parse到數據。檢查發現中途有報錯……但是看不出來錯在哪裡啊。。

另外一個影響用戶數量的原因是parse_user(0方法除了解析完自己yield item以外,還要跟著yield2個Request對象,用於請求它的follows和followers。

否則,程序首先從1個用戶爬到了他的關注者和關注的人,然後對這些人進行一次請求,拿到他們的詳細信息,也就是都來到了parse_user()這是因為運行了,這一步完就結束了,不會有然後了。

而實際上,我們還應該再對這些人的關注者和關注的人進行爬取,這樣源源不斷地爬到所有用戶……所以加上……

yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0), self.parse_follows) yield Request(self.followers_url.format(user=self.start_user, include=self.followers_query, limit=20, offset=0), self.parse_followers)

======待續

推薦閱讀:

【Python爬蟲實戰】為啥學Python,BOSS告訴你
Python 的函數是怎麼傳遞參數的?
用 Python 玩轉 Facebook 數據
左手用R右手Python系列——多進程/線程數據抓取與網頁請求
Python爬蟲聯想詞視頻和代碼

TAG:Python |