如何使用scrapy中的ItemLoader提取數據?

如何使用scrapy中的ItemLoader提取數據?

1. 簡述

  • 我們在用scrapy爬取數據時,首先就要明確我們要爬取什麼數據。scrapy提供了Item對象這種簡單的容器,我們可以通過Item定義提取數據的格式,需要爬取哪些字段,其提供了類似於字典的API以及用於聲明可用字段的簡單語法。如下所示:
  • 下面以爬取伯樂在線文章詳情頁爲範例:http://blog.jobbole.com/all-posts/
    這裏寫圖片描述
# 文件items.py
# Item使用簡單的class定義語法以及 Field 對象來聲明。
import scrapy

class articleDetailItem(scrapy.Item):
    # 標題
    title = scrapy.Field()
    # 文章創建時間
    create_date = scrapy.Field()
    # 文章鏈接地址
    url = scrapy.Field()
    # url經過md5映射後的值
    url_object_id = scrapy.Field()
    # 文章中圖片地址
    front_image_url = scrapy.Field()
    # 文件下載後本地保存的地址
    front_image_path = scrapy.Field()
    # 讚的個數
    praise_nums = scrapy.Field()
    # 評論數
    comment_nums = scrapy.Field()
    # 收藏數
    fav_nums = scrapy.Field()
    # 所有標籤
    tags = scrapy.Field()
    # 文章內容
    content = scrapy.Field(serializer = str)
  • Item字段說明:
    • Field 對象指明瞭每個字段的元數據(metadata)。例如上面例子中 content 字段中指明瞭該字段的序列化函數爲str。
    • 可以爲每個字段指明任何類型的元數據。Field 對象對接受的值沒有任何限制。Field 對象中保存的每個鍵可以由多個組件使用,並且只有這些組件知道這個鍵的存在。設置 Field 對象的主要目的就是在一個地方定義好所有的元數據。
    • 需要注意的是,用來聲明item的 Field 對象並沒有被賦值爲class的屬性。 不過可以通過 Item.fields 屬性進行訪問。
  • 然後在spider.py中,按照一定的規則來進行數據的提取,如下:
# 文件 boleSpider.py
from ArticleSpider.items import articleDetailItem

#...........此處省略..........
def parseArticelDetail(self, response):
    articleObject = articleDetailItem()
    #  提取出的內容是:6 收藏
    fav_nums = response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]
    # 用正則表達式提取其中的數字6
    match_re = re.match(".*?(\d+).*", fav_nums)
    if match_re:
        fav_nums = match_re.group(1)
    else:
        fav_nums = 0
  • 但是當項目很大,提取的字段數以百計,那麼各種提取規則會越來越多,按照這種方式來做,維護的工作將會是一場噩夢!
  • 所以scrapy就提供了ItemLoader這樣一個容器,在這個容器裏面可以配置item中各個字段的提取規則。可以通過函數分析原始數據,並對Item字段進行賦值,非常的便捷。
  • 可以這麼來看 Item 和 Itemloader:Item提供保存抓取到數據的容器,而 Itemloader提供的是填充容器的機制。
  • Itemloader提供的是一種靈活,高效的機制,可以更方便的被spider或source format (HTML, XML, etc)擴展並重寫,更易於維護,尤其是分析規則特別複雜繁多的時候。

2. 環境

  • 系統:win7
  • Scrapy 1.4.0
  • python 3.6.1

3. ItemLoader使用步驟

3.1. 實例化ItemLoader對象

# 文件 boleSpider.py
from scrapy.loader import ItemLoader
  • 要使用Itemloader,必須先將它實例化。可以使用類似字典的對象或者我們之前定義的Item對象來進行實例化。
# 文件 boleSpider.py 
import scrapy
from scrapy.loader import ItemLoader

# 如上面所示,我們首先在items.py中定義了一個articleDetailItem類(繼承自scrapy.Item),用於保存我們抓取到的數據

    # 解析函數
    def parse_detail(self, response):
        # 需要實例化ItemLoader, 注意第一個參數必須是實例化的對象...
        atricleItemLoader = ItemLoader(item = articleDetailItem(), response=response)
        # 調用xpath選擇器,提起title信息
        atricleItemLoader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')

        # 將提取好的數據load出來
        articleInfo = atricleItemLoader.load_item()
        # 輸出:articleInfo = {'title': ['在 Linux 中自動配置 IPv6 地址']}
        print(f"articleInfo = {articleInfo}")
  • 參數說明:重要的參數有兩個
    • 第一個參數:item對象, 傳遞進來的 Item是之前定義的,也可以是一個類似字典的對象。特別需要注意的是,傳遞的是一個實例,不是類名。……(當然不使用對象也可以,當不用對象進行實例化的時候,Item會自動使用ItemLoader.default_item_class 屬性中指定的Item 類在Item Loader constructor中實例化)
    • 第二個參數:response,指定用於提取數據的源數據。

3.2. ItemLoader填充數據的三種方法

  • 實例化ItemLoader對象之後,接下來,就要開始收集數值到ItemLoader了。ItemLoader提供了三個重要的方法將數據填充進來:
# 文件 boleSpider.py 

    # 解析頁面函數
    def parse_detail(self, response):
        # 需要實例化ItemLoader, 注意第一個參數必須是實例化的對象...
        atricleItemLoader = ItemLoader(item = articleDetailItem(), response=response)
        # 調用xpath選擇器,提取title信息
        atricleItemLoader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')
        # 調用css選擇器,提取praise_nums信息
        atricleItemLoader.add_css('praise_nums', '.vote-post-up h10::text')
        # 直接給字段賦值,尤其需要注意,不管賦值的數據是什麼,都會自動轉換成list類型
        atricleItemLoader.add_value('url', response.url)

        # 將提取好的數據load出來
        articleInfo = atricleItemLoader.load_item()
        # 觀察一下,發現三種方式填充的數據,均爲List類型
        '''
            輸出結果:
                articleInfo = {
                    'praise_nums': ['2'],
                    'title': ['100 倍價值的工程師'],
                    'url': ['http://blog.jobbole.com/113710/']
                }
        '''
        print(f"articleInfo = {articleInfo}")
  • 使用說明:
    • 第一個參數:指定字段名,如title。
    • 第二個參數:指定對應的提取規則,或者傳值。
    • 前面調用add_xpath等只是將提取的數據收集起來。最終,當所有數據被收集起來之後,還需要調用 ItemLoader.load_item() 方法, 實際上填充並且返回了之前通過調用 add_xpath(),add_css(),and add_value() 所提取和收集到的數據。
    • 特別注意:默認情況下,這些字段填入的全部是list類型。就算是傳值,傳遞了一個url,但是結果依然是一個list。
    • 從boleSpider.py核心代碼來看,我們可以對每個字段進行配置,匹配映射,非常的清晰,大大方便了可配置性和可維護性。
  • 但是實際項目中,一個字段的提取一般不會是直接配置一個規則,還需要更進一步的處理。那如何添加其他處理方法呢?接着往下看…

3.3. ItemLoader填充數據面臨的問題。

  • 從上面的示例中,可以看到,存在兩個問題:
    • 第一,提取的數據,填充進去的對象都是List類型。而我們大部分的需求是要取第一個數值,取List中的第一個非空元素,那麼如何實現取第一個呢?
    • 第二,在做item字段解析時,經常需要再進一步解析,過濾出我們想要的數值,例如用正則表達式將 $10 price中的數字10提取出來。那麼又如何對字段加一些處理函數呢?

3.4. 輸入處理器input_processor和輸出處理器output_processor

  • 首先來改寫一下articleDetailItem的定義:
# items.py
import datetime
import scrapy

# 定義一個時間處理轉換函數
# 將 '\r\n\r\n            2018/03/06 ·  ' 轉換成 datetime.date(2018, 3, 14)
def date_convert(value):
    try:
        create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
    except Exception as e:
        create_date = datetime.datetime.now().date()

    return create_date

# 用於存儲解析文章的詳細信息
class articleDetailItem(scrapy.Item):
    # 標題
    title = scrapy.Field()
    # 文章創建時間
    create_date = scrapy.Field(
        # 轉換前是'create_date':'\r\n\r\n            2018/03/14 ·  '
        # 轉換後是'create_date': datetime.date(2018, 3, 14),
        input_processor = MapCompose(date_convert),
        output_processor = TakeFirst()
    )
    # 文章鏈接地址
    url = scrapy.Field(
        # 轉換前是'url': ['http://blog.jobbole.com/113771/']
        # 轉換後是'url': 'http://blog.jobbole.com/113699/'
        output_processor = TakeFirst()
    )

    # url經過md5映射後的值
    url_object_id = scrapy.Field()
    # 文章中圖片地址
    front_image_url = scrapy.Field()
    # 文件下載後本地保存的地址
    front_image_path = scrapy.Field()
    # 讚的個數
    praise_nums = scrapy.Field()
    # 評論數
    comment_nums = scrapy.Field()
    # 收藏數
    fav_nums = scrapy.Field()
    # 所有標籤
    tags = scrapy.Field()
    # 文章內容
    content = scrapy.Field()
  • 然後在 boleSpider.py 中提取數據:
# 文件boleSpider.py
    # 解析頁面函數
    def parse_detail(self, response):
        # 需要實例化ItemLoader, 注意第一個參數必須是實例化的對象...
        atricleItemLoader = ItemLoader(item = articleDetailItem(), response=response)
        # 調用xpath選擇器,提取title信息
        atricleItemLoader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')
        # 調用xpath選擇器,提取create_date信息
        atricleItemLoader.add_xpath('create_date', "//p[@class='entry-meta-hide-on-mobile']/text()")
        # 調用css選擇器,提取praise_nums信息
        atricleItemLoader.add_css('praise_nums', '.vote-post-up h10::text')
        # 直接給字段賦值,尤其需要注意,不管賦值的數據是什麼,都會自動轉換成list類型
        atricleItemLoader.add_value('url', response.url)

        # 將提取好的數據load出來
        articleInfo = atricleItemLoader.load_item()
        '''
            輸出結果:
                articleInfo = {
                    'create_date': datetime.date(2018, 3, 14),
                    'praise_nums': ['1'],
                    'title': ['在 Linux 中自動配置 IPv6 地址'],
                    'url': 'http://blog.jobbole.com/113771/'}
        '''
        print(f"articleInfo = {articleInfo}")
  • Field 字段事實上有兩個參數:
    • 第一個是輸入處理器(input_processor) ,當這個item,title這個字段的值傳過來時,可以在傳進來的值上面做一些預處理。
    • 第二個是輸出處理器(output_processor) , 當這個item,title這個字段被預處理完之後,輸出前最後的一步處理。
  • 總結一下,每個字段的數據的處理過程是:

    • 第一步, 通過 add_xpath(), add_css() 或者 add_value() 方法),提取到數據。
    • 第二步,將提取到的數據,傳遞到輸入處理器(input_processor)中進行處理,處理結果被收集起來,並且保存在ItemLoader內(但尚未分配給該Item)。
    • 第三步,最後調用輸出處理器(output_processor)來處理之前收集到的數據(這是最後一步對數據的處理)。然後再存入到Item中,輸出處理器的結果是被分配到Item的最終值。
    • 第四步,收集到所有的數據後, 調用ItemLoader.load_item() 方法來填充,並得到填充後的 Item 對象。
  • 需要注意的是:input_processor和output_processor都是可調用對象,調用時傳入需要被分析的數據, 處理後返回分析得到的值。因此你可以使用任意函數作爲輸入、輸出處理器。唯一需注意的是它們必須接收一個(並且只是一個)迭代器性質的參數。

3.5. 處理原來的兩個問題

  • 再回到原來的問題,如何解決:

3.5.1. 如何取第一個?

# 文件items.py

import scrapy

# TakeFirst()是Scrapy提供的內置處理器,用於提取List中的第一個非空元素
class articleDetailItem(scrapy.Item):
    # 文章鏈接地址
    url = scrapy.Field(
        # 轉換前是'url': ['http://blog.jobbole.com/113771/']
        # 轉換後是'url': 'http://blog.jobbole.com/113699/'
        output_processor = TakeFirst()
    )

3.3.2. 如何在字段上加一些處理函數?

# 文件items.py
import datetime
import scrapy

# 定義一個時間處理轉換函數
# 將 '\r\n\r\n            2018/03/06 ·  ' 轉換成 datetime.date(2018, 3, 14)
def date_convert(value):
    try:
        create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
    except Exception as e:
        create_date = datetime.datetime.now().date()

    return create_date

# 用於存儲解析文章的詳細信息
class articleDetailItem(scrapy.Item):
    # 文章創建時間
    create_date = scrapy.Field(
        # 轉換前是'create_date':'\r\n\r\n            2018/03/14 ·  '
        # 轉換後是'create_date': datetime.date(2018, 3, 14),
        input_processor = MapCompose(date_convert),
        output_processor = TakeFirst()
    )

3.6. scrapy內置的處理器

  • 參考源碼: E:\Miniconda\Lib\site-packages\scrapy\loader\processors.py
  • 從上面的例子來看,我們可以自定義一下處理函數,作爲輸入輸出處理器,但是Scrapy還提供了一些常用的處理器。如MapCompose(能把多個函數執行的結果按順序組合起來,產生最終的輸出,通常用於輸入處理器),TakeFirst(取第一個非空的元素)。

3.6.1. TakeFirst

  • 返回第一個非空(non-null/ non-empty)值,常用於單值字段的輸出處理器,無參數。
# 源碼
# class scrapy.loader.processors.TakeFirst
class TakeFirst(object):
    def __call__(self, values):
        for value in values:
            if value is not None and value != '':
                return value
# 單獨直接使用
from scrapy.loader.processors import TakeFirst

proc = TakeFirst()

# 接收對象是一個可迭代的對象,如list
result = proc(['', 'one', 'two', 'three'])

# 結果:result = one
print(f"result = {result}")

3.6.2. Identity

  • 最簡單的處理器,不進行任何處理,直接返回原來的數據。無參數。
# 源碼
# class scrapy.loader.processors.Identity
class Identity(object):
    def __call__(self, values):
        return values
# 單獨直接使用
from scrapy.loader.processors import Identity

proc = Identity()

# 接收對象是一個可迭代的對象,如list
result = proc(['', 'one', 'two', 'three'])

# 結果:result = ['', 'one', 'two', 'three']
print(f"result = {result}")

3.6.3. Join

  • 返回用分隔符連接後的值。分隔符默認爲空格。不接受Loader contexts。
  • 當使用默認分隔符的時候,這個處理器等同於如下這個:
u' '.join
# 源碼
# class scrapy.loader.processors.Join(separator=u’ ‘)
class Join(object):
    def __init__(self, separator=u' '):
        self.separator = separator
    def __call__(self, values):
        return self.separator.join(values)
# 單獨直接使用
from scrapy.loader.processors import Join

# 如果不指定連接符,默認是使用空格連接
proc = Join(";")

# 接收對象是一個可迭代的對象,如list
result = proc(['', 'one', 'two', 'three'])

# 結果:result = ;one;two;three
print(f"result = {result}")

3.6.4. Compose

  • 用給定的多個函數的組合,來構造的處理器。list對象(注意不是指list中的元素),依次被傳遞到第一個函數,然後輸出,再傳遞到第二個函數,一個接着一個,直到最後一個函數返回整個處理器的輸出。
  • 默認情況下,當遇到None值(list中有None值)的時候停止處理。可以通過傳遞參數stop_on_none = False改變這種行爲。
class Compose(object):
    def __init__(self, *functions, **default_loader_context):
        self.functions = functions
        self.stop_on_none = default_loader_context.get('stop_on_none', True)
        self.default_loader_context = default_loader_context
    def __call__(self, value, loader_context=None):
        if loader_context:
            context = MergeDict(loader_context, self.default_loader_context)
        else:
            context = self.default_loader_context
        wrapped_funcs = [wrap_loader_context(f, context) for f in self.functions]
        for func in wrapped_funcs:
            if value is None and self.stop_on_none:
                break
            value = func(value)
        return value
# 單獨直接使用
from scrapy.loader.processors import Compose

# stop_on_none=True, 指定在遇到None時,不用中斷,還繼續處理
# lambda v: v[0], 指定取第一個元素
# str.upper , 大寫
proc = Compose(lambda v: v[0], str.upper, stop_on_none=True)

# 接收對象是一個可迭代的對象,如list
result = proc(['one', 'two', None, 'three'])

# 結果:result = ONE
print(f"result = {result}")
  • 每個函數可以選擇接收一個loader_context參數。

3.6.5. MapCompose

  • 與Compose處理器類似,區別在於各個函數結果在內部傳遞的方式(會涉及到list對象解包的步驟):
    • 輸入值是被迭代的處理的,List對象中的每一個元素被單獨傳入,第一個函數進行處理,然後處理的結果被連接起來形成一個新的迭代器,並被傳入第二個函數,以此類推,直到最後一個函數。最後一個函數的輸出被連接起來形成處理器的輸出。
    • 每個函數能返回一個值或者一個值列表,也能返回None(會被下一個函數所忽略)
    • 這個處理器提供了很方便的方式來組合多個處理單值的函數。因此它常用於輸入處理器,因爲傳遞過來的是一個List對象。
# 源碼
# class scrapy.loader.processors.MapCompose(*functions, **default_loader_context)
class MapCompose(object):

    def __init__(self, *functions, **default_loader_context):
        self.functions = functions
        self.default_loader_context = default_loader_context

    def __call__(self, value, loader_context=None):
        values = arg_to_iter(value)
        if loader_context:
            context = MergeDict(loader_context, self.default_loader_context)
        else:
            context = self.default_loader_context
        wrapped_funcs = [wrap_loader_context(f, context) for f in self.functions]
        for func in wrapped_funcs:
            next_values = []
            for v in values:
                next_values += arg_to_iter(func(v))
            values = next_values
        return values
# 單獨直接使用

from scrapy.loader.processors import MapCompose

def add_firstStr(value):
    return value + "_firstAdd"

def add_secondStr(value):
    return value + "_secondAdd"


# stop_on_none=True, 指定在遇到None時,不用中斷,還繼續處理
# 依次處理每個list元素
proc = MapCompose(add_firstStr, add_secondStr, str.upper, stop_on_none=True)

# 接收對象是一個可迭代的對象,如list
result = proc(['one', 'two', 'three'])

# 結果:result = ['ONE_FIRSTADD_SECONDADD', 'TWO_FIRSTADD_SECONDADD', 'THREE_FIRSTADD_SECONDADD']
print(f"result = {result}")
  • 與Compose處理器類似,它也能接受Loader context。

3.7. 重用和擴展ItemLoaders

3.7.1. 添加默認的處理機制

  • 從上面的信息來看,ItemLoaders是非常靈活的,但是假設有個需求,所有的字段,我們都要去取第一個,那麼如果有300個字段,我們就要添加300次,每個都要寫,就會覺得很麻煩。那麼有沒有辦法統一設置呢,答案是有的,如下:
    • 如果想要實現每個字段都只取第一個,那麼可以定義一個自己的ItemLoader類:ArticleItemLoader(繼承自ItemLoader類)
    • 我們首先可以看一下原始的 ItemLoader 的定義:
# E:\Miniconda\Lib\site-packages\scrapy\loader\__init__.py
class ItemLoader(object):

    default_item_class = Item
    # 可以看到是有默認的輸入/輸出處理器的,而且默認是什麼都不做
    default_input_processor = Identity()
    default_output_processor = Identity()
    default_selector_class = Selector
  • 可以定義一個自己的ItemLoader類:ArticleItemLoader,繼承自ItemLoader類, 同時改寫(重寫)default_output_processor
# 文件items.py
from scrapy.loader import ItemLoader

# 需要繼承內置的ItemLoader類
class ArticleItemLoader(ItemLoader):
    # 自定義itemloader,默認的輸出處理器爲取第一個非空元素
    default_output_processor = TakeFirst()
  • 然後在boleSpider中使用時,我們就不能再簡單的使用原有的ItemLoader,而是使用我們自己定義的 ArticleItemLoader 來填充數據:
# 文件boleSpider.py
from ArticleSpider.items import articleDetailItem, ArticleItemLoader

# 使用自定義的ArticleItemLoader實例化一個item_loader 對象
# 然後發現,結果都是從list中取出了一個值:說明我們的設置已經生效了。
item_loader = ArticleItemLoader(item = articleDetailItem(), response=response)
item_loader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')

3.7.2. 重寫,覆蓋默認的處理機制

  • 上面我們實現了所有字段都只取第一個的功能,但是如果有一些字段,我不需要取第一個,而是有其他的處理方式呢?
  • 那就需要重寫這個字段的輸出處理器(output_processor)。 下面的例子是,首先在輸入處理器中將 “評論” 這樣的字符過濾掉,然後將list中所有的元素用”,” 連接起來,成爲一個字符串。
def removeCommentTags(value):
    # 去掉Tags中提取的評論字符
    if "評論" in value:
        return ""
    else:
        return value

# Tags是一個list,我們需要用","將他們連接起來, 變成了字符串。
# 但是“評論”我們不需要。去掉。 如何去掉“評論”,在input_processor中,判斷value是否==“評論”,如果是,就去掉
class articleDetailItem(scrapy.Item):
    tags = scrapy.Field(
        # 去掉評論
        input_processor = MapCompose(removeCommentTags),
        # 將list中的元素,通過“,”連接起來
        output_processor = Join(",")
    )
  • 而如果,有些字段我們不想做任何處理,也不想去取第一個元素,那麼我們怎麼做呢?
  • 因爲,目前所有的字段都默認設置爲去取第一個非空元素,所以,我們需要將這個處理去掉。這個地方尤其要引起重視,因爲很容易遺忘自己有這個默認設置。處理方式如下:
def returnValue(value):
    return value

class articleDetailItem(scrapy.Item):
    content = scrapy.Field(
        # 會覆蓋掉默認的default_out
        output_processor = MapCompose(returnValue)
        # 或者使用Identity
        # output_processor = Identity()
    )
發佈了73 篇原創文章 · 獲贊 244 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章