1. 程式人生 > 程式設計 >如何使用scrapy中的ItemLoader提取資料

如何使用scrapy中的ItemLoader提取資料

1. 簡述

我們在用scrapy爬取資料時,首先就要明確我們要爬取什麼資料。scrapy提供了Item物件這種簡單的容器,我們可以通過Item定義提取資料的格式,需要爬取哪些欄位,其提供了類似於字典的API以及用於宣告可用欄位的簡單語法。如下所示:
下面以爬取伯樂線上文章詳情頁為範例:http://blog.jobbole.com/all-posts/

如何使用scrapy中的ItemLoader提取資料

# 檔案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=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,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,'//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',response.url)

  # 將提取好的資料load出來
  articleInfo = atricleItemLoader.load_item()
  '''
   輸出結果:
    articleInfo = {
     'create_date': datetime.date(2018,'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,"%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,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(['','three'])

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

3.6.3. Join

  • 返回用分隔符連線後的值。分隔符預設為空格。不接受Loader contexts。
  • 當使用預設分隔符的時候,這個處理器等同於如下這個:u' '.join1
# 原始碼
# 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(['','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',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,**default_loader_context):
  self.functions = functions
  self.default_loader_context = default_loader_context

 def __call__(self,loader_context=None):
  values = arg_to_iter(value)
  if loader_context:
   context = MergeDict(loader_context,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,'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()
 ) 

到此這篇關於如何使用scrapy中的ItemLoader提取資料的文章就介紹到這了,更多相關scrapy ItemLoader提取內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!