1. 程式人生 > >Python3網路爬蟲:Scrapy入門之使用ImagesPipline下載圖片

Python3網路爬蟲:Scrapy入門之使用ImagesPipline下載圖片

Python版本: python3.+
執行環境: Mac OS
IDE: pycharm

一、前言

  上篇部落格用了一個簡單的實戰熟悉了一下scrapy框架的使用。但是下載圖片的方法使用的卻是requests庫,而scrapy本身就自帶有圖片下載的方法ImagesPipline

二、初識ImagesPipline

1. ImagesPipline的特性:

  • 避免重新下載最近已經下載過的資料
  • 指定儲存路徑
  • 將所有下載的圖片轉換成通用的格式(JPG)和模式(RGB)
  • 縮圖生成
  • 檢測影象的寬/高,確保它們滿足最小限制

2. ImagesPipline的工作流

  1. 在一個爬蟲裡,你抓取一個專案,把其中圖片的URL放入 image_urls
    (type = list) 組內。
  2. item從爬蟲內返回,進入Item Piplines
  3. item進入 ImagesPipeline,image_urls 組內的URLs將被Scrapy的排程器和下載器(這意味著排程器和下載器的中介軟體可以複用)安排下載,當優先順序更高,會在其他頁面被抓取前處理。專案會在這個特定的管道階段保持“locker”的狀態,直到完成檔案的下載(或者由於某些原因未完成下載)。
  4. 當檔案下載完後,另一個欄位(files)將被更新到結構中。這個組將包含一個字典列表,其中包括下載檔案的資訊,比如下載路徑、源抓取地址(從 image_urls 組獲得)和圖片的校驗碼(checksum)。 images 列表中的檔案順序將和源 image_urls
    組保持一致。如果某個圖片下載失敗,將會記錄下錯誤資訊,圖片也不會出現在 files 組中。

3.ImagesPipline使用樣例

一、 定義item

為了使用media pipeline,你僅需要 啟用 .
接著,如果spider返回一個具有 ‘file_urls’ 或者 ‘image_urls’(取決於使用Files 或者 Images
Pipeline) 鍵的dict,則pipeline會提取相對應(‘files’ 或 ‘images’)的結果。

如果你更喜歡使用 Item 來自定義item, 則需要設定相應必要的欄位,例如下面使用Image Pipeline的例子:


import scrapy

class MyItem(scrapy.Item):

    # ... other item fields ...
    image_urls = scrapy.Field()
    images = scrapy.Field()

  在這裡 我就自己定義了一個items

import scrapy

class MscrapyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    image_urls = scrapy.Field()
    image_ids = scrapy.Field()
    image_paths = scrapy.Field()
    pass

二、 設定setting

首先需要在專案中新增 ITEM_PIPELINES

ITEM_PIPELINES = {'scrapy.pipeline.images.ImagesPipeline': 1}

接著 IMAGES_STORE 設定為一個有效的資料夾,用來儲存下載的圖片。 否則管道將保持禁用狀態,即使你在
ITEM_PIPELINES 設定中添加了它。

對於Images Pipeline, 設定 IMAGES_STORE

IMAGES_STORE = '/path/to/valid/dir'

關於縮圖等其他屬性 可以參看官方文件

三、 ImagePipline修改圖片預設下載名稱

1. 文件解讀

ImagePipline的諸多屬性中需要特別注意的就是檔案系統儲存,因為它定義了檔案儲存時的預設名稱,我們想要修改圖片預設名稱,就得從這裡入手。

檔案系統儲存

檔案以它們URL的 SHA1 hash 作為檔名。

比如,對下面的圖片URL:

http://www.example.com/image.jpg 它的 SHA1 hash 值為:

3afec3b4765f8f0a07b78f98c07b83f013567a0a

將被下載並存為下面的檔案:

< IMAGES_STORE>/full/3afec3b4765f8f0a07b78f98c07b83f013567a0a.jpg

其中:

<IMAGES_STORE> 是定義在 IMAGES_STORE 設定裡的資料夾 > full是用來區分圖片和縮圖(如果使用的話)的一個子資料夾。

我們當然不希望自己下載下來的圖片名稱是這一串無法理解的數字。所以我們需要修改它檔名。

官方文件中 提供了2個可以重寫的方法:

  • get_media_requests(item, info)
  • item_completed(results, items, info)

get_media_requests(item, info)

在工作流程中可以看到,管道會得到檔案的URL並從專案中下載。為了這麼做,你需要重寫 get_media_requests()方法,並對各個圖片URL返回一個Request:

def get_media_requests(self, item, info):
    for file_url in item['file_urls']:
        yield scrapy.Request(file_url) 
  這些請求將被管道處理,當它們完成下載後,結果將以2-元素的元組列表形式傳送到 `item_completed()`方法: 每個元組包含

(success, file_info_or_error):

success 是一個布林值,當圖片成功下載時為True,因為某個原因下載失敗為False file_info_or_error
是一個包含下列關鍵字的字典(如果成功為 True )或者出問題時為 Twisted Failureurl - 檔案下載的url。這是從get_media_requests() 方法返回請求的url。 path - 圖片儲存的路徑(類似 FILES_STORE)
checksum - 圖片內容的 MD5 hash item_completed() 接收的元組列表需要保證與
get_media_requests() 方法返回請求的順序相一致。下面是 results 引數的一個典型值:

[(True,   {'checksum': '2b00042f7481c7b056c4b410d28f33cf',    'path':
'full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg',    'url':
'http://www.example.com/files/product1.pdf'}),  (False,  
Failure(...))] 

預設 get_media_requests() 方法返回 None ,這意味著專案中沒有檔案可下載。

item_completed(results, items, info)

當一個單獨專案中的所有圖片請求完成時(要麼完成下載,要麼因為某種原因下載失敗),
FilesPipeline.item_completed() 方法將被呼叫。

item_completed() 方法需要返回一個輸出,其將被送到隨後的專案管道階段,因此你需要返回(或者丟棄)專案,如你在任意管道里所做的一樣。 這裡是一個
item_completed() 方法的例子,其中我們將下載的圖片路徑(傳入到results中)儲存到 image_paths
專案組中,如果其中沒有圖片,我們將丟棄專案:

from scrapy.exceptions import DropItem

def item_completed(self, results, item, info):
    image_paths = [x['path'] for ok, x in results if ok]
    if not file_paths:
        raise DropItem("Item contains no files")
    item['image_paths'] = image_paths
    return item 

預設情況下, item_completed() 方法返回item

下面是一個圖片管道的完整例子,其方法如上所示:

import scrapy
from scrapy.pipeline.images import ImagesPipeline
from scrapy.exceptions import DropItem

class MyImagesPipeline(ImagesPipeline):

    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

2.程式碼實戰

繼續上篇部落格的實戰demo,在這裡我修改了piplines下的程式碼

class UnsplashPipeline(ImagesPipeline):
    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")
        if item['image_ids']:
            new_path = "full/"+item['image_ids'][0]+".jpg"
        os.rename(settings.IMAGES_STORE+"/"+image_paths[0],settings.IMAGES_STORE+"/"+new_path)
        item['image_paths'] = new_path
        return item

該方法實質上是在ImagesPipline完成預設檔名的儲存後,將檔案重新命名。

3.ImagePipline原始碼淺析

如果閱讀原始碼,會發現file_path()方法正是給圖片賦檔名的方法。所以直接重寫這個方法豈不是美滋滋。在這裡,我們先來看一下file_path()方法的原始碼:

def file_path(self, request, response=None, info=None):
        ## start of deprecation warning block (can be removed in the future)
        def _warn():
            from scrapy.exceptions import ScrapyDeprecationWarning
            import warnings
            warnings.warn('ImagesPipeline.image_key(url) and file_key(url) methods are deprecated, '
                          'please use file_path(request, response=None, info=None) instead',
                          category=ScrapyDeprecationWarning, stacklevel=1)

        # check if called from image_key or file_key with url as first argument
        if not isinstance(request, Request):
            _warn()
            url = request
        else:
            url = request.url

        # detect if file_key() or image_key() methods have been overridden
        if not hasattr(self.file_key, '_base'):
            _warn()
            return self.file_key(url)
        elif not hasattr(self.image_key, '_base'):
            _warn()
            return self.image_key(url)
        ## end of deprecation warning block

        image_guid = hashlib.sha1(url).hexdigest()  # change to request.url after deprecation
        return 'full/%s.jpg' % (image_guid)

如果只是為了修改檔案路徑而修改file_path,這對原始碼侵入太大。所以官方文件裡也沒有建議重寫file_path

以下是ImagesPinpline的原始碼,供大家參考

class ImagesPipeline(FilesPipeline):
    """Abstract pipeline that implement the image thumbnail generation logic

    """

    MEDIA_NAME = 'image'
    MIN_WIDTH = 0
    MIN_HEIGHT = 0
    THUMBS = {}
    DEFAULT_IMAGES_URLS_FIELD = 'image_urls'
    DEFAULT_IMAGES_RESULT_FIELD = 'images'

    @classmethod
    def from_settings(cls, settings):
        cls.MIN_WIDTH = settings.getint('IMAGES_MIN_WIDTH', 0)
        cls.MIN_HEIGHT = settings.getint('IMAGES_MIN_HEIGHT', 0)
        cls.EXPIRES = settings.getint('IMAGES_EXPIRES', 90)
        cls.THUMBS = settings.get('IMAGES_THUMBS', {})
        s3store = cls.STORE_SCHEMES['s3']
        s3store.AWS_ACCESS_KEY_ID = settings['AWS_ACCESS_KEY_ID']
        s3store.AWS_SECRET_ACCESS_KEY = settings['AWS_SECRET_ACCESS_KEY']

        cls.IMAGES_URLS_FIELD = settings.get('IMAGES_URLS_FIELD', cls.DEFAULT_IMAGES_URLS_FIELD)
        cls.IMAGES_RESULT_FIELD = settings.get('IMAGES_RESULT_FIELD', cls.DEFAULT_IMAGES_RESULT_FIELD)
        store_uri = settings['IMAGES_STORE']
        return cls(store_uri)

    def file_downloaded(self, response, request, info):
        return self.image_downloaded(response, request, info)

    def image_downloaded(self, response, request, info):
        checksum = None
        for path, image, buf in self.get_images(response, request, info):
            if checksum is None:
                buf.seek(0)
                checksum = md5sum(buf)
            width, height = image.size
            self.store.persist_file(
                path, buf, info,
                meta={'width': width, 'height': height},
                headers={'Content-Type': 'image/jpeg'})
        return checksum

    def get_images(self, response, request, info):
        path = self.file_path(request, response=response, info=info)
        orig_image = Image.open(StringIO(response.body))

        width, height = orig_image.size
        if width < self.MIN_WIDTH or height < self.MIN_HEIGHT:
            raise ImageException("Image too small (%dx%d < %dx%d)" %
                                 (width, height, self.MIN_WIDTH, self.MIN_HEIGHT))

        image, buf = self.convert_image(orig_image)
        yield path, image, buf

        for thumb_id, size in self.THUMBS.iteritems():
            thumb_path = self.thumb_path(request, thumb_id, response=response, info=info)
            thumb_image, thumb_buf = self.convert_image(image, size)
            yield thumb_path, thumb_image, thumb_buf

    def convert_image(self, image, size=None):
        if image.format == 'PNG' and image.mode == 'RGBA':
            background = Image.new('RGBA', image.size, (255, 255, 255))
            background.paste(image, image)
            image = background.convert('RGB')
        elif image.mode != 'RGB':
            image = image.convert('RGB')

        if size:
            image = image.copy()
            image.thumbnail(size, Image.ANTIALIAS)

        buf = StringIO()
        image.save(buf, 'JPEG')
        return image, buf

    def get_media_requests(self, item, info):
        return [Request(x) for x in item.get(self.IMAGES_URLS_FIELD, [])]

    def item_completed(self, results, item, info):
        if self.IMAGES_RESULT_FIELD in item.fields:
            item[self.IMAGES_RESULT_FIELD] = [x for ok, x in results if ok]
        return item

    def file_path(self, request, response=None, info=None):
        ## start of deprecation warning block (can be removed in the future)
        def _warn():
            from scrapy.exceptions import ScrapyDeprecationWarning
            import warnings
            warnings.warn('ImagesPipeline.image_key(url) and file_key(url) methods are deprecated, '
                          'please use file_path(request, response=None, info=None) instead',
                          category=ScrapyDeprecationWarning, stacklevel=1)

        # check if called from image_key or file_key with url as first argument
        if not isinstance(request, Request):
            _warn()
            url = request
        else:
            url = request.url

        # detect if file_key() or image_key() methods have been overridden
        if not hasattr(self.file_key, '_base'):
            _warn()
            return self.file_key(url)
        elif not hasattr(self.image_key, '_base'):
            _warn()
            return self.image_key(url)
        ## end of deprecation warning block

        image_guid = hashlib.sha1(url).hexdigest()  # change to request.url after deprecation
        return 'full/%s.jpg' % (image_guid)

    def thumb_path(self, request, thumb_id, response=None, info=None):
        ## start of deprecation warning block (can be removed in the future)
        def _warn():
            from scrapy.exceptions import ScrapyDeprecationWarning
            import warnings
            warnings.warn('ImagesPipeline.thumb_key(url) method is deprecated, please use '
                          'thumb_path(request, thumb_id, response=None, info=None) instead',
                          category=ScrapyDeprecationWarning, stacklevel=1)

        # check if called from thumb_key with url as first argument
        if not isinstance(request, Request):
            _warn()
            url = request
        else:
            url = request.url

        # detect if thumb_key() method has been overridden
        if not hasattr(self.thumb_key, '_base'):
            _warn()
            return self.thumb_key(url, thumb_id)
        ## end of deprecation warning block

        thumb_guid = hashlib.sha1(url).hexdigest()  # change to request.url after deprecation
        return 'thumbs/%s/%s.jpg' % (thumb_id, thumb_guid)

    # deprecated
    def file_key(self, url):
        return self.image_key(url)
    file_key._base = True

    # deprecated
    def image_key(self, url):
        return self.file_path(url)
    image_key._base = True

    # deprecated
    def thumb_key(self, url, thumb_id):
        return self.thumb_path(url, thumb_id)
    thumb_key._base = True

四、小結

  scrapy本身提供的工具已經很豐富而且實用。我對scrapy的理解很有限,僅僅是入門,本篇部落格也只是我對imagesPipline自學後的總結,如有錯,望指正。