Scrapy框架的使用之Item Pipeline的用法

崔慶才丨靜覓發表於2018-05-14

Item Pipeline是專案管道,本節我們詳細瞭解它的用法。

首先我們看看Item Pipeline在Scrapy中的架構,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

圖中的最左側即為Item Pipeline,它的呼叫發生在Spider產生Item之後。當Spider解析完Response之後,Item就會傳遞到Item Pipeline,被定義的Item Pipeline元件會順次呼叫,完成一連串的處理過程,比如資料清洗、儲存等。

Item Pipeline的主要功能有如下4點。

  • 清理HTML資料。

  • 驗證爬取資料,檢查爬取欄位。

  • 查重並丟棄重複內容。

  • 將爬取結果儲存到資料庫。

一、核心方法

我們可以自定義Item Pipeline,只需要實現指定的方法,其中必須要實現的一個方法是: process_item(item, spider)

另外還有如下幾個比較實用的方法。

  • open_spider(spider)

  • close_spider(spider)

  • from_crawler(cls, crawler)

下面我們詳細介紹這幾個方法的用法。

1. process_item(item, spider)

process_item()是必須要實現的方法,被定義的Item Pipeline會預設呼叫這個方法對Item進行處理。比如,我們可以進行資料處理或者將資料寫入到資料庫等操作。它必須返回Item型別的值或者丟擲一個DropItem異常。

process_item()方法的引數有如下兩個。

  • item,是Item物件,即被處理的Item。

  • spider,是Spider物件,即生成該Item的Spider。

process_item()方法的返回型別歸納如下。

  • 如果它返回的是Item物件,那麼此Item會被低優先順序的Item Pipeline的process_item()方法處理,直到所有的方法被呼叫完畢。

  • 如果它丟擲的是DropItem異常,那麼此Item會被丟棄,不再進行處理。

2. open_spider(self, spider)

open_spider()方法是在Spider開啟的時候被自動呼叫的。在這裡我們可以做一些初始化操作,如開啟資料庫連線等。其中,引數spider就是被開啟的Spider物件。

3. close_spider(spider)

close_spider()方法是在Spider關閉的時候自動呼叫的。在這裡我們可以做一些收尾工作,如關閉資料庫連線等。其中,引數spider就是被關閉的Spider物件。

4. from_crawler(cls, crawler)

from_crawler()方法是一個類方法,用@classmethod標識,是一種依賴注入的方式。它的引數是crawler,通過crawler物件,我們可以拿到Scrapy的所有核心元件,如全域性配置的每個資訊,然後建立一個Pipeline例項。引數cls就是Class,最後返回一個Class例項。

下面我們用一個例項來加深對Item Pipeline用法的理解。

二、本節目標

我們以爬取360攝影美圖為例,來分別實現MongoDB儲存、MySQL儲存、Image圖片儲存的三個Pipeline。

三、準備工作

請確保已經安裝好MongoDB和MySQL資料庫,安裝好Python的PyMongo、PyMySQL、Scrapy框架。

四、抓取分析

我們這次爬取的目標網站為:https://image.so.com。開啟此頁面,切換到攝影頁面,網頁中呈現了許許多多的攝影美圖。我們開啟瀏覽器開發者工具,過濾器切換到XHR選項,然後下拉頁面,可以看到下面就會呈現許多Ajax請求,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

我們檢視一個請求的詳情,觀察返回的資料結構,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

返回格式是JSON。其中list欄位就是一張張圖片的詳情資訊,包含了30張圖片的ID、名稱、連結、縮圖等資訊。另外觀察Ajax請求的引數資訊,有一個引數sn一直在變化,這個引數很明顯就是偏移量。當sn為30時,返回的是前30張圖片,sn為60時,返回的就是第31~60張圖片。另外,ch引數是攝影類別,listtype是排序方式,temp引數可以忽略。

所以我們抓取時只需要改變sn的數值就好了。

下面我們用Scrapy來實現圖片的抓取,將圖片的資訊儲存到MongoDB、MySQL,同時將圖片儲存到本地。

五、新建專案

首先新建一個專案,命令如下所示:

scrapy startproject images360複製程式碼

接下來新建一個Spider,命令如下所示:

scrapy genspider images images.so.com複製程式碼

這樣我們就成功建立了一個Spider。

六、構造請求

接下來定義爬取的頁數。比如爬取50頁、每頁30張,也就是1500張圖片,我們可以先在settings.py裡面定義一個變數MAX_PAGE,新增如下定義:

MAX_PAGE = 50複製程式碼

定義start_requests()方法,用來生成50次請求,如下所示:

def start_requests(self):
    data = {'ch': 'photography', 'listtype': 'new'}
    base_url = 'https://image.so.com/zj?'
    for page in range(1, self.settings.get('MAX_PAGE') + 1):
        data['sn'] = page * 30
        params = urlencode(data)
        url = base_url + params
        yield Request(url, self.parse)複製程式碼

在這裡我們首先定義了初始的兩個引數,sn引數是遍歷迴圈生成的。然後利用urlencode()方法將字典轉化為URL的GET引數,構造出完整的URL,構造並生成Request。

還需要引入scrapy.Request和urllib.parse模組,如下所示:

from scrapy import Spider, Request
from urllib.parse import urlencode複製程式碼

再修改settings.py中的ROBOTSTXT_OBEY變數,將其設定為False,否則無法抓取,如下所示:

ROBOTSTXT_OBEY = False複製程式碼

執行爬蟲,即可以看到連結都請求成功,執行命令如下所示:

scrapy crawl images複製程式碼

執行示例結果如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

所有請求的狀態碼都是200,這就證明圖片資訊爬取成功了。

七、提取資訊

首先定義一個Item,叫作ImageItem,如下所示:

from scrapy import Item, Field
class ImageItem(Item):
    collection = table = 'images'
    id = Field()
    url = Field()
    title = Field()
    thumb = Field()複製程式碼

在這裡我們定義了4個欄位,包括圖片的ID、連結、標題、縮圖。另外還有兩個屬性collectiontable,都定義為images字串,分別代表MongoDB儲存的Collection名稱和MySQL儲存的表名稱。

接下來我們提取Spider裡有關資訊,將parse()方法改寫為如下所示:

def parse(self, response):
    result = json.loads(response.text)
    for image in result.get('list'):
        item = ImageItem()
        item['id'] = image.get('imageid')
        item['url'] = image.get('qhimg_url')
        item['title'] = image.get('group_title')
        item['thumb'] = image.get('qhimg_thumb_url')
        yield item複製程式碼

首先解析JSON,遍歷其list欄位,取出一個個圖片資訊,然後再對ImageItem賦值,生成Item物件。

這樣我們就完成了資訊的提取。

八、儲存資訊

接下來我們需要將圖片的資訊儲存到MongoDB、MySQL,同時將圖片儲存到本地。

MongoDB

首先確保MongoDB已經正常安裝並且正常執行。

我們用一個MongoPipeline將資訊儲存到MongoDB,在pipelines.py裡新增如下類的實現:

import pymongo

class MongoPipeline(object):
    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DB')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        self.db[item.collection].insert(dict(item))
        return item

    def close_spider(self, spider):
        self.client.close()複製程式碼

這裡需要用到兩個變數,MONGO_URIMONGO_DB,即儲存到MongoDB的連結地址和資料庫名稱。我們在settings.py裡新增這兩個變數,如下所示:

MONGO_URI = 'localhost'
MONGO_DB = 'images360'複製程式碼

這樣一個儲存到MongoDB的Pipeline的就建立好了。這裡最主要的方法是process_item()方法,直接呼叫Collection物件的insert()方法即可完成資料的插入,最後返回Item物件。

MySQL

首先確保MySQL已經正確安裝並且正常執行。

新建一個資料庫,名字還是images360,SQL語句如下所示:

CREATE DATABASE images360 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci複製程式碼

新建一個資料表,包含id、url、title、thumb四個欄位,SQL語句如下所示:

CREATE TABLE images (id VARCHAR(255) NULL PRIMARY KEY, url VARCHAR(255) NULL , title VARCHAR(255) NULL , thumb VARCHAR(255) NULL)複製程式碼

執行完SQL語句之後,我們就成功建立好了資料表。接下來就可以往表裡儲存資料了。

接下來我們實現一個MySQLPipeline,程式碼如下所示:

import pymysql

class MysqlPipeline():
    def __init__(self, host, database, user, password, port):
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            host=crawler.settings.get('MYSQL_HOST'),
            database=crawler.settings.get('MYSQL_DATABASE'),
            user=crawler.settings.get('MYSQL_USER'),
            password=crawler.settings.get('MYSQL_PASSWORD'),
            port=crawler.settings.get('MYSQL_PORT'),
        )

    def open_spider(self, spider):
        self.db = pymysql.connect(self.host, self.user, self.password, self.database, charset='utf8', port=self.port)
        self.cursor = self.db.cursor()

    def close_spider(self, spider):
        self.db.close()

    def process_item(self, item, spider):
        data = dict(item)
        keys = ', '.join(data.keys())
        values = ', '.join(['%s'] * len(data))
        sql = 'insert into %s (%s) values (%s)' % (item.table, keys, values)
        self.cursor.execute(sql, tuple(data.values()))
        self.db.commit()
        return item複製程式碼

如前所述,這裡用到的資料插入方法是一個動態構造SQL語句的方法。

這裡又需要幾個MySQL的配置,我們在settings.py裡新增幾個變數,如下所示:

MYSQL_HOST = 'localhost'
MYSQL_DATABASE = 'images360'
MYSQL_PORT = 3306
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'複製程式碼

這裡分別定義了MySQL的地址、資料庫名稱、埠、使用者名稱、密碼。

這樣,MySQL Pipeline就完成了。

Image Pipeline

Scrapy提供了專門處理下載的Pipeline,包括檔案下載和圖片下載。下載檔案和圖片的原理與抓取頁面的原理一樣,因此下載過程支援非同步和多執行緒,下載十分高效。下面我們來看看具體的實現過程。

官方文件地址為:https://doc.scrapy.org/en/latest/topics/media-pipeline.html。

首先定義儲存檔案的路徑,需要定義一個IMAGES_STORE變數,在settings.py中新增如下程式碼:

IMAGES_STORE = './images'複製程式碼

在這裡我們將路徑定義為當前路徑下的images子資料夾,即下載的圖片都會儲存到本專案的images資料夾中。

內建的ImagesPipeline會預設讀取Item的image_urls欄位,並認為該欄位是一個列表形式,它會遍歷Item的image_urls欄位,然後取出每個URL進行圖片下載。

但是現在生成的Item的圖片連結欄位並不是image_urls欄位表示的,也不是列表形式,而是單個的URL。所以為了實現下載,我們需要重新定義下載的部分邏輯,即要自定義ImagePipeline,繼承內建的ImagesPipeline,重寫幾個方法。

我們定義ImagePipeline,如下所示:

from scrapy import Request
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline

class ImagePipeline(ImagesPipeline):
    def file_path(self, request, response=None, info=None):
        url = request.url
        file_name = url.split('/')[-1]
        return file_name

    def item_completed(self, results, item, info):
        image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem('Image Downloaded Failed')
        return item

    def get_media_requests(self, item, info):
        yield Request(item['url'])複製程式碼

在這裡我們實現了ImagePipeline,繼承Scrapy內建的ImagesPipeline,重寫下面幾個方法。

  • get_media_requests()。它的第一個引數item是爬取生成的Item物件。我們將它的url欄位取出來,然後直接生成Request物件。此Request加入到排程佇列,等待被排程,執行下載。

  • file_path()。它的第一個引數request就是當前下載對應的Request物件。這個方法用來返回儲存的檔名,直接將圖片連結的最後一部分當作檔名即可。它利用split()函式分割連結並提取最後一部分,返回結果。這樣此圖片下載之後儲存的名稱就是該函式返回的檔名。

  • item_completed(),它是當單個Item完成下載時的處理方法。因為並不是每張圖片都會下載成功,所以我們需要分析下載結果並剔除下載失敗的圖片。如果某張圖片下載失敗,那麼我們就不需儲存此Item到資料庫。該方法的第一個引數results就是該Item對應的下載結果,它是一個列表形式,列表每一個元素是一個元組,其中包含了下載成功或失敗的資訊。這裡我們遍歷下載結果找出所有成功的下載列表。如果列表為空,那麼該Item對應的圖片下載失敗,隨即丟擲異常DropItem,該Item忽略。否則返回該Item,說明此Item有效。

現在為止,三個Item Pipeline的定義就完成了。最後只需要啟用就可以了,修改settings.py,設定ITEM_PIPELINES,如下所示:

ITEM_PIPELINES = {
    'images360.pipelines.ImagePipeline': 300,
    'images360.pipelines.MongoPipeline': 301,
    'images360.pipelines.MysqlPipeline': 302,
}複製程式碼

這裡注意呼叫的順序。我們需要優先呼叫ImagePipeline對Item做下載後的篩選,下載失敗的Item就直接忽略,它們就不會儲存到MongoDB和MySQL裡。隨後再呼叫其他兩個儲存的Pipeline,這樣就能確儲存入資料庫的圖片都是下載成功的。

接下來執行程式,執行爬取,如下所示:

scrapy crawl images複製程式碼

爬蟲一邊爬取一邊下載,下載速度非常快,對應的輸出日誌如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

檢視本地images資料夾,發現圖片都已經成功下載,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

檢視MySQL,下載成功的圖片資訊也已成功儲存,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

檢視MongoDB,下載成功的圖片資訊同樣已成功儲存,如下圖所示。

Scrapy框架的使用之Item Pipeline的用法

這樣我們就可以成功實現圖片的下載並把圖片的資訊存入資料庫。

九、本節程式碼

本節程式碼地址為:https://github.com/Python3WebSpider/Images360。

十、結語

Item Pipeline是Scrapy非常重要的元件,資料儲存幾乎都是通過此元件實現的。請讀者認真掌握此內容。


本資源首發於崔慶才的個人部落格靜覓: Python3網路爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注我的個人微信公眾號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)


相關文章