1. 程式人生 > >Scrapy筆記(5)- Item詳解

Scrapy筆記(5)- Item詳解

Item是儲存結構資料的地方,Scrapy可以將解析結果以字典形式返回,但是Python中字典缺少結構,在大型爬蟲系統中很不方便。

Item提供了類字典的API,並且可以很方便的宣告欄位,很多Scrapy元件可以利用Item的其他資訊。

定義Item

定義Item非常簡單,只需要繼承scrapy.Item類,並將所有欄位都定義為scrapy.Field型別即可

import scrapy

class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)

Item Fields

Field物件可用來對每個欄位指定元資料。例如上面last_updated的序列化函式指定為str,可任意指定元資料,不過每種元資料對於不同的元件意義不一樣。

Item使用示例

你會看到Item的使用跟Python中的字典API非常類似

建立Item

>>> product = Product(name='Desktop PC', price=1000)
>>> print product
Product(name='Desktop PC', price=1000)

獲取值

>>> product['name']
Desktop PC
>>> product.get('name')
Desktop PC

>>> product['price']
1000

>>> product['last_updated']
Traceback (most recent call last):
    ...
KeyError: 'last_updated'

>>> product.get('last_updated', 'not set')
not set

>>> product['lala'] # getting unknown field
Traceback (most recent call last):
    ...
KeyError: 'lala'

>>> product.get('lala', 'unknown field')
'unknown field'

>>> 'name' in product  # is name field populated?
True

>>> 'last_updated' in product  # is last_updated populated?
False

>>> 'last_updated' in product.fields  # is last_updated a declared field?
True

>>> 'lala' in product.fields  # is lala a declared field?
False

設定值

>>> product['last_updated'] = 'today'
>>> product['last_updated']
today

>>> product['lala'] = 'test' # setting unknown field
Traceback (most recent call last):
    ...
KeyError: 'Product does not support field: lala'

訪問所有的值

>>> product.keys()
['price', 'name']

>>> product.items()
[('price', 1000), ('name', 'Desktop PC')]

Item Loader

Item Loader為我們提供了生成Item的相當便利的方法。Item為抓取的資料提供了容器,而Item Loader可以讓我們非常方便的將輸入填充到容器中。

下面我們通過一個例子來展示一般使用方法:

from scrapy.loader import ItemLoader
from myproject.items import Product

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath('name', '//div[@class="product_name"]')
    l.add_xpath('name', '//div[@class="product_title"]')
    l.add_xpath('price', '//p[@id="price"]')
    l.add_css('stock', 'p#stock]')
    l.add_value('last_updated', 'today') # you can also use literal values
    return l.load_item()

注意上面的name欄位是從兩個xpath路徑添累加後得到。

輸入/輸出處理器

每個Item Loader對每個Field都有一個輸入處理器和一個輸出處理器。輸入處理器在資料被接受到時執行,當資料收集完後呼叫ItemLoader.load_item()時再執行輸出處理器,返回最終結果。

l = ItemLoader(Product(), some_selector)
l.add_xpath('name', xpath1) # (1)
l.add_xpath('name', xpath2) # (2)
l.add_css('name', css) # (3)
l.add_value('name', 'test') # (4)
return l.load_item() # (5)

執行流程是這樣的:

  1. xpath1中的資料被提取出來,然後傳輸到name欄位的輸入處理器中,在輸入處理器處理完後生成結果放在Item Loader裡面(這時候沒有賦值給item)
  2. xpath2資料被提取出來,然後傳輸給(1)中同樣的輸入處理器,因為它們都是name欄位的處理器,然後處理結果被附加到(1)的結果後面
  3. 跟2一樣
  4. 跟3一樣,不過這次是直接的字面字串值,先轉換成一個單元素的可迭代物件再傳給輸入處理器
  5. 上面4步的資料被傳輸給name的輸出處理器,將最終的結果賦值給name欄位

自定義Item Loader

使用類定義語法,下面是一個例子

from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join

class ProductLoader(ItemLoader):

    default_output_processor = TakeFirst()

    name_in = MapCompose(unicode.title)
    name_out = Join()

    price_in = MapCompose(unicode.strip)

    # ...

通過_in_out字尾來定義輸入和輸出處理器,並且還可以定義預設的ItemLoader.default_input_processorItemLoader.default_input_processor.

在Field定義中宣告輸入/輸出處理器

還有個地方可以非常方便的新增輸入/輸出處理器,那就是直接在Field定義中

import scrapy
from scrapy.loader.processors import Join, MapCompose, TakeFirst
from w3lib.html import remove_tags

def filter_price(value):
    if value.isdigit():
        return value

class Product(scrapy.Item):
    name = scrapy.Field(
        input_processor=MapCompose(remove_tags),
        output_processor=Join(),
    )
    price = scrapy.Field(
        input_processor=MapCompose(remove_tags, filter_price),
        output_processor=TakeFirst(),
    )

優先順序:

  1. 在Item Loader中定義的field_infield_out
  2. Filed元資料(input_processoroutput_processor關鍵字)
  3. Item Loader中的預設的

Tips:一般來講,將輸入處理器定義在Item Loader的定義中field_in,然後將輸出處理器定義在Field元資料中

Item Loader上下文

Item Loader上下文被所有輸入/輸出處理器共享,比如你有一個解析長度的函式

def parse_length(text, loader_context):
    unit = loader_context.get('unit', 'm')
    # ... length parsing code goes here ...
    return parsed_length

初始化和修改上下文的值

loader = ItemLoader(product)
loader.context['unit'] = 'cm'

loader = ItemLoader(product, unit='cm')

class ProductLoader(ItemLoader):
    length_out = MapCompose(parse_length, unit='cm')

內建的處理器

  1. Identity 啥也不做
  2. TakeFirst 返回第一個非空值,通常用作輸出處理器
  3. Join 將結果連起來,預設使用空格’ ‘
  4. Compose 將函式連結起來形成管道流,產生最後的輸出
  5. MapCompose 跟上面的Compose類似,區別在於內部結果在函式中的傳遞方式.它的輸入值是可迭代的,首先將第一個函式依次作用於所有值,產生新的可迭代輸入,作為第二個函式的輸入,最後生成的結果連起來返回最終值,一般用在輸入處理器中。
  6. SelectJmes 使用json路徑來查詢值並返回結果