Scrapy Item Loaders机制详解

来源:互联网 发布:灭茶苦茶 知乎 编辑:程序博客网 时间:2024/04/30 00:21

1. Items

爬虫的主要任务就是从非结构化的数据中获得结构化的数据。
Item 对象是种简单的容器,保存了爬取到得数据。 其提供了 类似于词典(dictionary-like) 的API以及用于声明可用字段的简单语法。

声明Item
Item使用简单的class定义语法以及 Field 对象来声明。例如:

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

Item字段:
Field 对象指明了每个字段的元数据(metadata)。例如上面例子中 last_updated 中指明了该字段的序列化函数。

可以为每个字段指明任何类型的元数据。Field 对象对接受的值没有任何限制。Field 对象中保存的每个键可以由多个组件使用,并且只有这些组件知道这个键的存在。设置 Field 对象的主要目的就是在一个地方定义好所有的元数据。

需要注意的是,用来声明item的 Field 对象并没有被赋值为class的属性。 不过您可以通过 Item.fields 属性进行访问。

2. 用Item Loader来填充Item

Item Loaders提供了一种便捷的方式填充抓取到的 Items 。 虽然Items可以使用自带的类字典形式API填充,但是Items Loaders提供了更便捷的API, 可以分析原始数据并对Item进行赋值。

从另一方面来说, Items 提供保存抓取数据的 容器 , 而 Item Loaders提供的是 填充 容器的机制。

Item Loaders提供的是一种灵活,高效的机制,可以更方便的被spider或source format (HTML, XML, etc)扩展,并override更易于维护的、不同的内容分析规则。

要使用Item Loader, 你必须先将它实例化. 可以使用类似字典的对象来进行实例化, 或者不使用对象也可以, 当不用对象进行实例化的时候,Item会自动使用 ItemLoader.default_item_class 属性中指定的Item 类在Item Loader constructor中实例化.

然后,你开始收集数值到Item Loader时,通常使用 Selectors. 你可以在同一个item field 里面添加多个数值;Item Loader将知道如何用合适的处理函数来“添加”这些数值.

下面是在 Spider 中典型的Item Loader的用法, 使用 Items chapter 中声明的 Product item:

from scrapy.contrib.loader import ItemLoaderfrom myproject.items import Productdef 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位置提取:

  1. //div[@class="product_name"]
  2. //div[@class="product_title"]

换言之,数据通过用 add_xpath() 的方法,把从两个不同的XPath位置提取的数据收集起来. 这是将在以后分配给 name 字段中的数据。

之后,类似的请求被用于 pricestock 字段 (后者使用 CSS selector 和 add_css() 方法), 最后使用不同的方法 add_value()last_update 填充文本值( today ).

最终, 当所有数据被收集起来之后, 调用 ItemLoader.load_item() 方法, 实际上填充并且返回了之前通过调用 add_xpath(), add_css(), and add_value() 所提取和收集到的数据的Item.

3. 输入处理器与输出处理器

  • Item Loader在每个字段中都包含了一个输入处理器和一个输出处理器。
  • 输入处理器收到数据时立刻提取数据 (通过 add_xpath(), add_css() 或者 add_value() 方法) 之后输入处理器的结果被收集起来并且保存在ItemLoader内(但尚未分配给该Item).
  • 收集到所有的数据后, 调用 ItemLoader.load_item() 方法来填充,并得到填充后的 Item 对象。在这一步中先调用输出处理器来处理之前收集到的数据,然后再存入Item中。输出处理器的结果是被分配到Item的最终值。

需要注意的是,输入和输出处理器都是可调用对象,调用时传入需要被分析的数据, 处理后返回分析得到的值。因此你可以使用任意函数作为输入、输出处理器。 唯一需注意的是它们必须接收一个(并且只是一个)迭代器性质的positional参数。

4. 声明Items Loaders

Item Loaders 的声明类似于Items,以class的语法来声明:

from scrapy.contrib.loader import ItemLoaderfrom scrapy.contrib.loader.processor import TakeFirst, MapCompose, Joinclass ProductLoader(ItemLoader):    default_output_processor = TakeFirst()    name_in = MapCompose(unicode.title)    name_out = Join()    price_in = MapCompose(unicode.strip)    # ...

input processors 以_in为后缀来声明,output processors 以_out 为后缀来声明。也可以用ItemLoader.default_input_processorItemLoader.default_output_processor 属性来声明默认的 input/output processors。

5. 声明Input and Output Processors

前面讲到,input and output processors可以在定义Item Loaders的时候声明,这是非常普遍的使用方法。但是,你也可以在定义Item的时候声明输入输出处理器。下面是例子:

import scrapyfrom scrapy.contrib.loader.processor import Join, MapCompose, TakeFirstfrom w3lib.html import remove_tagsdef filter_price(value):    if value.isdigit():        return valueclass 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(),    )

使用Item:

>>> from scrapy.contrib.loader import ItemLoader>>> il = ItemLoader(item=Product())>>> il.add_value('name', [u'Welcome to my', u'<strong>website</strong>'])>>> il.add_value('price', [u'&euro;', u'<span>1000</span>'])>>> il.load_item(){'name': u'Welcome to my website', 'price': u'1000'}

关于集中声明 input and output processors方式的优先级排序如下:

  1. 在Item Loader 中声明的 field-specific 属性: field_in and field_out (most precedence)
  2. Item中的字段元数据(input_processor and output_processor key)
  3. Item Loader 默认处理器: ItemLoader.default_input_processor() and ItemLoader.default_output_processor() (least precedence)

6. 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的值:

  1. 修改当前 active Item Loader context:

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

  2. 在Item Loader实例化的时候:

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

  3. 对于那些支持带Item Loader context实例化的输入输出处理器(例如MapCompose),在Item Loader声明的时候修改它context:

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

7. ItemLoader object

参见官方文档

8. 重用和扩展Item Loaders

当你的项目逐渐变大,使用了越来越多的spider的时候,维护变成了一个基本的问题。尤其是当你需要处理每个spider的许多不同的解析规则的时候,会出现很多的异常,迫使你开始考虑重用的问题。

Item Loader支持传统的Python继承机制来处理spider之间的差异。

例如,有些网站把它们的产Product名用三个短线封装起来(如:---Plasma TV---),而你想要去掉这些东西。

你可以通过reusing and extending默认Product Item Loader的方式去掉短线:

from scrapy.loader.processors import MapComposefrom myproject.ItemLoaders import ProductLoaderdef strip_dashes(x):    return x.strip('-')class SiteSpecificLoader(ProductLoader):    name_in = MapCompose(strip_dashes, ProductLoader.name_in)

另一种情形时继承Item Loader也很有用:有多种格式的源数据(如XML, HTML),在XML版本里面你想要去除CDATA:

from scrapy.loader.processors import MapComposefrom myproject.ItemLoaders import ProductLoaderfrom myproject.utils.xml import remove_cdataclass XmlProductLoader(ProductLoader):    name_in = MapCompose(remove_cdata, ProductLoader.name_in)

这便是扩展输入处理器的方法。

对于输出处理器,更常用的方式是在Item字段元数据里声明。因为通常它们依赖于具体的字段而不是网站

还有很多其他方式开扩展、继承和覆盖Item Loader,不同的层次结构适于不同的项目。Scrapy只是提供了这些机制,不强制要求具体的组织方式。

9. 内置的处理器

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

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

9.1 Identity

class scrapy.loader.processors.Identity

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

9.2 TakeFirst

class scrapy.loader.processors.TakeFirst

返回第一个非空(non-null/non-empty)值,常用于单值字段的输出处理器。无参数。

示例如下:

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

9.3 Join

class scrapy.loader.processors.Join(separator=u’ ‘)

返回用分隔符连接后的值。分隔符默认为空格。不接受Loader contexts。

当使用默认分隔符的时候,这个处理器等同于这个函数:

u' '.join

使用示例:

>>> 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'

9.4 Compose

class scrapy.loader.processors.Compose(*functions, **default_loader_context)

用给定的多个函数的组合而构造的处理器。每个输入值被传递到第一个函数,然后其输出再传递到第二个函数,诸如此类,直到最后一个函数返回整个处理器的输出。

默认情况下,当遇到None值的时候停止处理。可以通过传递参数stop_on_none=False改变这种行为。

使用示例:

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

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

9.5 MapCompose

class scrapy.loader.processors.MapCompose(*functions, **default_loader_context)

与Compose处理器类似,区别在于各个函数结果在内部传递的方式:

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

  • 每个函数能返回一个值或者一个值列表,也能返回None(会被下一个函数所忽略)

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

下面的例子能说明这个处理器的工作方式:

>>> 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。

9.6 SelectJmes

class scrapy.loader.processors.SelectJmes(json_path)

查询指定的JSON path并返回输出。需要jmespath(https://github.com/jmespath/jmespath.py)支持。每次接受一个输入。

示例:

>>> 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']
1 0
原创粉丝点击