Scrappy入門:百度貼吧圖片爬蟲
Scrapy 是Python非常有名的爬蟲框架,框架本身已經為爬蟲性能做了很多優化:多線程、整合xpath和圖片專用管道等等,開發人員只要專注在功能需求上。
基本Scrapy使用教程參考: 初窺Scrapy 和 Scrapy入門教程 。
學習一種技術或者一個框架最好的方式當然是用它做一些小工程,入門第一步我先選擇了百度貼吧圖片爬蟲,因為既夠簡單又比較實用。
因為這次涉及到圖片的下載,而Scrapy本身為此提供了特殊的圖片管道,所以果斷直接用Scrapy的圖片管道來幫助完成。Scrapy中管道的定義如下:
當Item在Spider中被收集之后,它將會被傳遞到Item Pipeline,一些組件會按照一定的順序執行對Item的處理。每個item pipeline組件(有時稱之為“Item Pipeline”)是實現了簡單方法的Python類。他們接收到Item并通過它執行一些行為,同時也決定此Item是否繼續通過pipeline,或是被丟棄而不再進行處理。
對于管道的典型應用場景如下:
清理HTML數據
驗證爬取的數據(檢查item包含某些字段)
查重(并丟棄)
將爬取結果保存到數據庫中
Scrappy圖片管道的使用教程參考: 下載項目圖片 。
使用Scrapy最重要的就是編寫特定的spider類,本文指定的spider類是BaiduTieBaSpider,來看下它的定義:
import scrapy import requests import os from tutorial.items import TutorialItem class BaiduTieBaSpider(scrapy.spiders.Spider): name = 'baidutieba' start_urls = ['http://tieba.baidu.com/p/2235516502?see_lz=1&pn=%d' % i for i in range(1, 38)] image_names = {} def parse(self, response): item = TutorialItem() item['image_urls'] = response.xpath("http://img[@class='BDE_Image']/@src").extract() for index, value in enumerate(item['image_urls']): number = self.start_urls.index(response.url) * len(item['image_urls']) + index self.image_names[value] = 'full/%04d.jpg' % number yield item
這里要關注Scrappy做的兩件事情:
-
根據start_urls中的URL地址訪問頁面并得到返回
-
parse(self, response)函數就是抓取到頁面之后的解析工作
那么首先就是start_urls的構造,這里是觀察了百度貼吧里的URL規則,其中see_lz=1表示只看樓主,pn=1表示第一頁,根據這些規則得出了一個URL數組。然后再觀察單個頁面的HTML源碼,得出每個樓層發布的圖片對應的img標簽的類為BDE_Image,這樣就可以得出xpath的表達式:xpath("http://img[@class='BDE_Image']/@src"),來提取樓層中所有圖片的src,賦值到item對象的image_urls字段中,當spider返回時,item會進入圖片管道進行處理(即Scrapy會自自動幫你下載圖片)。
對應的item類的編寫和setting.py文件的修改詳見上文的教程。
到這里下載圖片的基本功能都完成了,但是有個問題:我想要按順序保存圖片怎么辦?
造成這個問題的關鍵就是Scrapy是多線程抓取頁面的,也就是對于start_urls中地址的抓取都是異步請求,以及item返回之后到圖片管道后對每張圖片的URL也是異步請求,所以是無法保證每張圖片返回的順序的。
那么這個問題怎么解決呢?試了幾種辦法之后,得到一個相對理想的解決方案就是:制作一個字典,key是圖片地址,value是對應的編號。所以就有了代碼中的image_names和number = self.start_urls.index(response.url) * len(item['image_urls']) + index,然后再定制圖片管道,定制的方法詳見上文給出的教程鏈接,在本文中定制需要做的事情就是重寫file_path函數,代碼如下:
import scrapy from scrapy.contrib.pipeline.images import ImagesPipeline from scrapy.exceptions import DropItem from tutorial.spiders.BaiduTieBa_spider import BaiduTieBaSpider class MyImagesPipeline(ImagesPipeline): def file_path(self, request, response=None, info=None): image_name = BaiduTieBaSpider.image_names[request.url] return image_name def get_media_requests(self, item, info): for image_url in item['image_urls']: yield scrapy.Request(image_url) def item_completed(self, results, item, info): image_paths = [x['path'] for ok, x in results if ok] if not image_paths: raise DropItem("Item contains no images") item['image_paths'] = image_paths return item
file_path函數就是返回每張圖片保存的路徑,當我們有一張完整的字典之后,只要根據request的URL去取相應的編號即可。
這個方法顯然是比較消耗內存的,因為如果圖片很多的話,需要維護的字典的條目也會很多,但從已經折騰過的幾個解決方案(例如不用管道而采用手動阻塞的方式來下載圖片)來看,它的效果是最好的,付出的代價也還算可以接受。
Scrappy入門第一個小demo就寫到這里。