返回介绍

13.2 Item Loader

发布于 2024-01-26 22:39:51 字数 8260 浏览 0 评论 0 收藏 0

在讲Item Loader之前,首先回顾一下Item的用法。Item对象是种简单的容器,保存了爬取到的数据,提供了类似于词典的API以及用于声明可用字段的简单语法。Item使用简单的class定义语法以及Field对象来声明,例如定义一个Product类:

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

以下都以Product类为例,讲解Item与Item Loaders的关系。

13.2.1 Item与Item Loader

Item Loader提供了一种便捷的方式填充抓取到的Items。虽然Items可以使用自带的类字典形式API填充,但是Items Loader提供了更便捷的API,可以分析原始数据并对Item进行赋值。换句话说,Items提供保存抓取数据的容器,而Item Loader提供的是填充容器的机制。

Item Loader提供的是一种灵活、高效的机制,可以更方便地被spider或HTML、XML等文件扩展,重写不同字段的解析规则,这对大型的爬虫项目的后期维护非常有利,拓展新的功能更加方便。下面是Item Loader在Spider中的典型应用方式,代码如下:

  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')
     return l.load_item()

我们可以发现name字段被从页面中两个不同的XPath位置提取:

  l.add_xpath('name', '// div[@class="product_name"]')
  l.add_xpath('name', '// div[@class="product_title"]')

也就是说使用add_xpath方法,将数据从两处XPath位置上收集起来,之后会分配给name字段。

类似操作被应用于price、stock和last_updated,pirce的字段数据通过XPath方式收集,stock字段数据通过CSS选择器方式收集,而last_updated字段数据直接被填充字符串today。当所有的数据被收集起来后,使用l.load_item()将数据实际填充到Item中。

13.2.2 输入与输出处理器

从上面的分析可以看到,Item Loader负责了数据的收集、处理和填充,Item仅仅承载了数据本身而已。数据的收集、处理和填充,归功于Item Loader中两个重要的组件:输入处理器(input processors)和输出处理器(output processors)。下面说一下Item Loaders这两个处理器的职能:

·首先Item Loader在每个字段中都包含了一个输入处理器和一个输出处理器

·输入处理器收到response后时立刻通过add_xpath()、add_css()或者add_value()等方法提取数据,经过输入处理器的结果被收集起来并且保存在ItemLoader内,这个时候数据还没有给Item。

·收集到所有的数据后,调用ItemLoader.load_item()方法来填充并返回Item对象。load_item()方法内部先调用输出处理器来处理收集到的数据,处理后的结果最终存入Item中。

说完了输入和输出处理器的职能,下面声明一个Item Loader。Item Loader的声明类似于Items,以class的语法来声明,代码如下:

  from scrapy.contrib.loader import ItemLoader
  from scrapy.contrib.loader.processor 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_processor和ItemLoader.default_output_processor属性来声明默认的输入/输出处理器。

除了可以在ItemLoader类中声明输入输出处理器,也可以在Item中声明。示例如下:

  import scrapy
  from scrapy.contrib.loader.processor import Join, MapCompose, TakeFirst
  from w3lib.html import remove_tags
  
  def filter_price(value):
     if value.isdigit():
       return value
  
  class ProductItem(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(),
     )

以上说了三种输入输出处理器的声明方式:

·ItemLoader类中声明类似field_in和field_out的属性。

·Item的字段中声明。

·Item Loader默认处理器:ItemLoader.default_input_processor()和ItemLoader.default_output_processor()。

这种三种方式的响应优先级是从上到下依次递减。

13.2.3 Item Loader Context

Item Loader Context是一个任意的键值对字典,能被Item Loader中的输入输出处理器所共享。它在Item Loader声明、实例化、使用的时候传入,可以调整输入输出处理器的行为。举个例子,假设有个parse_length方法用于接收text值并且获取其长度:

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

通过接收一个loader_context参数,这个方法告诉Item Loader它能够接收Item Loader context,因此当这个方法被调用时,Item Loader能将当前的active Context传递给它。

有以下几种方式可以修改Item Loader Context的值:

·通过context属性修改当前active Item Loader Context:

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

·在Item Loader实例化的时候:

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

·对于那些支持Item Loader Context实例化的输入输出处理器(例如MapCompose),可以在Item Loader定义时修改Context:

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

13.2.4 重用和扩展Item Loader

当爬虫项目越来越大,使用的Spider越来越多时,项目的维护将成为一个要紧的问题。特别是维护每一个Spider中许多而且不同的解析规则时,会出现很多异常,这个时候需要考虑重用和拓展的问题了。

Item Loader本身的设计就是为了减轻维护解析规则的负担,而且提供了方便的接口,用来重写和拓展他们。Item Loader支持通过传统Python类继承的方式来处理不同Spider解析的差异。

比如你之前写了一个ProductLoader来提取和解析某家公司网站的产品名称Plasma TV,但是一段时间之后公司网站更新,产品用三个短线封装起来,如---Plasma TV---。现在的需求是去掉这些短线,提取其中的产品名。示例代码如下:

  def strip_dashes(x):
     return x.strip('-')
  
  class SiteSpecificLoader(ProductLoader):
     name_in = MapCompose(strip_dashes, ProductLoader.name_in)

通过继承ProductLoader类,通过strip_dashes方法将name中的短线去掉,这便是拓展输入处理器的方法。

对于输出处理器,更常用的方式是在Item字段元数据里声明,因为通常它们依赖于具体的字段而不是网站,这个可以参考13.2.2节在Item的字段中声明输入和输出处理器。

13.2.5 内置的处理器

除了可以使用可调用的函数作为输入输出处理器,Scrapy提供了一些常用的处理器。例如MapCompose,通常用于输入处理器,能把多个方法执行的结果按顺序组合起来产生最终的输出。

下面是一些内置的处理器。

1.Identity

类原型:class scrapy.loader.processors.Identity

最简单的处理器,不进行任何处理,直接返回原来的数据,无参数。

2.TakeFirst

类原型:class scrapy.loader.processors.TakeFirst

返回第一个非空值,常用于单值字段的输出处理器,无参数。

在Scrapy shell运行示例如下:

  >>> from scrapy.loader.processors import TakeFirst
  >>> proc = TakeFirst()
  >>> proc(['', 'one', 'two', 'three'])
  'one'

3.Join

类原型:class scrapy.loader.processors.Join(separator=u’‘)

返回用分隔符separator连接后的值,分隔符separator默认为空格。不接受Loader contexts。当使用默认分隔符的时候,这个处理器等同于Python字符串对象中的join方法:’‘.join。

在Scrapy shell运行示例如下:

  >>> from scrapy.loader.processors import Join
  >>> proc = Join()
  >>> proc(['one', 'two', 'three'])
  u'one two three'
  >>> proc = Join('<br>')
  >>> proc(['one', 'two', 'three'])
  u'one<br>two<br>three'

4.Compose

类原型:class scrapy.loader.processors.Compose(*functions,**default_loader_context)

用给定的多个方法的组合来构造处理器,每个输入值被传递到第一个方法,然后其输出再传递到第二个方法,诸如此类,直到最后一个方法返回整个处理器的输出。默认情况下,当遇到None值的时候停止处理,可以通过传递参数stop_on_none=False改变这种设定。

在Scrapy shell运行示例如下:

  >>> from scrapy.loader.processors import Compose
  >>> proc = Compose(lambda v: v[0], str.upper)
  >>> proc(['hello', 'world'])
  'HELLO'

每个方法可以选择接收一个loader_context参数。

5.MapCompose

类原型:class scrapy.loader.processors.MapCompose(*functions,**default_loader_context)

和Compose类似,也是用给定的多个方法的组合来构造处理器,不同的是内部结果在方法间传递的方式:

·处理器的输入值是被迭代处理的,每一个元素被单独传入第一个函数进行处理,处理的结果被串连起来形成一个新的迭代器,并被传入第二个函数,以此类推,直到最后一个函数。最后一个函数的输出被连接起来形成处理器的输出。

·每个函数能返回一个值或者一个值列表,也能返回None。如果返回值是None,此值会被下一个函数所忽略。

·这个处理器提供了方便的方式来组合多个处理单值的函数。因此它常用于输入处理器,因为通过selectors中的extract()函数提取出来的值是一个unicode strings列表。

在Scrapy shell运行示例如下:

  >>> def filter_world(x):
  ...      return None if x == 'world' else x
  ...
  >>> from scrapy.loader.processors import MapCompose
  >>> proc = MapCompose(filter_world, unicode.upper)
  >>> proc([u'hello', u'world', u'this', u'is', u'scrapy'])
  [u'HELLO, u'THIS', u'IS', u'SCRAPY']

与Compose处理器类似,它也能接受Loader context。

6.SelectJmes

类原型:class scrapy.loader.processors.SelectJmes(json_path)

使用指定的json_path查询并返回值。需要jmespath的支持,而且每次只接受一个输入。jmespath:https://github.com/jmespath/jmespath.py

在Scrapy shell运行示例如下:

  >>> from scrapy.loader.processors import SelectJmes, Compose, MapCompose
  >>> proc = SelectJmes("foo") # for direct use on lists and dictionaries
  >>> proc({'foo': 'bar'})
  'bar'
  >>> proc({'foo': {'bar': 'baz'}})
  {'bar': 'baz'}

和JSON一起使用:

  >>> import json
  >>> proc_single_json_str = Compose(json.loads, SelectJmes("foo"))
  >>> proc_single_json_str('{"foo": "bar"}')
  u'bar'
  >>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo')))
  >>> proc_json_list('[{"foo":"bar"}, {"baz":"tar"}]')
  [u'bar']

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文