爬虫 Scrapy 学习系列之七:Item Loaders

前言

这是 Scrapy 系列学习文章之一,本章主要介绍 Item Loaders 相关的内容;

本文为作者的原创作品,转载需注明出处;

简介

Item Loaders 提供了一个便利的机制来帮助 populating(填充) scrapted Items;虽然,Items 可以通过它类似 dict API 来填充,Item Loaders 提供了更多便利的方法来进行 populates;

简而言之,Items 提供了被爬取数据的一个容器,而 Item Loaders 为该容器提供了数据填充的机制;

Item Loaders 的目的是为不同的 field 设计出更为高效、简单的可覆盖和可扩展的解析规则,可以用于不同的 spiders,以及不同的 source format (比如 HTML,XML 等等),而不至于是维护编程噩梦;

使用 Item Loaders 来填充(populate) items

看一个例子,

1
2
3
4
5
6
7
8
9
10
11
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()

在构造 ItemLoader 的时候可以使用一个 dict-like 的实例 (Item 实例或者是 Dict实例均可)作为参数,当然也可以不使用这个参数,当不使用这个参数的时候,默认使用 ItemLoader.default_item_class 来进行构造;并且需要将 response 作为第二个参数传入;

从上面这个例子中可以看到name通过两个不同的 XPath locations 来提取:

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

另外需要注意的是stock是通过add_css()方法填充的,而last_updated则是通过 add_value() 进行填充的;

最后,当所有的数据都已经收集好了以后,将会通过 [ItemLoader.load_item()] 将其转换为 Item 并返回;

Input and Output processors

Item Loader 为每个 Item Field 单独提供了一个 Input processor 和一个 Output processor;Input processor 一旦它通过 add_xpath()add_css()add_value() 方法收到提取到的数据便会执行,执行以后所得到的数据将仍然保存在 ItemLoader 实例中;当数据收集完成以后,ItemLoader 通过 load_item() 方法来进行填充并返回已填充的 Item 实例;看下面这个例子,

1
2
3
4
5
6
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属性所对应的 Input processor;Input processor 将会立即进行处理,但是处理的结果将仍然放置在 ItemLoader 中,并没有直接赋值给 Item)
  2. 数据已经通过xpath2提取,然后该数据将会传递给该 #1 的 Input processor (因为两者的属性都是name);Input processor 处理的结果将会追加到 #1 的 data collected 中 (当前的 ItemLoader 中的一个对象)
  3. 和 #1 和 #2 基本上类同,唯一的区别是,它使用的是 CSS 选择器,使用相同的 Input processor,并且将处理结果追加到相同的 data collected 中;
  4. 该步没有使用任何的选择器,而是直接通过文本的方式追加;虽然不需要提取,但是该操作仍然需要通过上述的 Input processor 进行处理;在这个用例中,因为该值 ‘test’ 是不能被遍历的(iterable),所以在传递给 Input processor 之前,虽然将其转换为 iterable 的对象,因为 Input processor 只接受 iterable;
  5. 将上面四个步骤所收集到的数据 data collected 传递给name属性所对应的 Output processor;经过 Output processor 所处理的结果将会赋值给 Item 的name属性;

声明 Item Loaders

Item Loaders 的声明方式和 Item 类同,使用 class definition syntax,看一个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

# ...

As you can see, input processors are declared using the _in suffix while output processors are declared using the _out suffix. And you can also declare a default input/output processors using the ItemLoader.default_input_processor and ItemLoader.default_output_processor attributes.

声明 Input and Output processors

上述例子中,我们在 Item Loader 中声明了 Input 和 Output processors;还有一种方式可以声明它们,那就是在 Item Field 的 metadata 中定义它们;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(),
)

补充,需要牢记 Input processor 和 Output processor 的定义,它们是为每一个 Field 定义的;注意与 price 相关的 Input processor,是通过 filter_price 方法进行过滤过的;name 的 output_processor 使用 joni() 定义如何通过 Output processor 来结合需要输出的元素值,默认使用分隔符' '来连接不同的 name 值;

1
2
3
4
5
6
>>> from scrapy.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'}

Item Loader Context

Item Loader Context 是一个 dict 在 ItemLoader 中被所有的 Input processor 和 Output processor 所共用;它主要用来改变 Input processor 和 Output processor 的一些行为;Item Loader Context 可以通过声明的方式、实例化的时候或者是使用 Item Loader 的时候传入;

看一个例子,

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

注意,如何 parse length 的部分被省略了;假定该方法是 Item Loader 对象中用户定义的一个方法,通过显示的指定参数名loader_context告诉 Item Loader,当前的方法 parse_legnth() 需要接收 Item Loader Context 作为参数,然后在实例化的时候,将会把 Item Loader Context 作为参数传入;在执行过程中,便可以通过 Item Loader Context 的引用loader_context进行调用了;

有几种方式可以改变 Item Loader Context 中的值;

  1. 直接对当前可用的 Item Loader Context 进行修改

    1
    2
    3
    4
    def parse_length(text, loader_context):
    unit = loader_context.get('unit', 'm')
    # ... length parsing code goes here ...
    return parsed_length
  2. 在初始化 Item Loader 的时候进行赋值

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

    在初始化 ItemLoader 的时候用到的关键字参数既是给 Item Loader Context 进行赋值用的;

  3. 在声明 Item Loader 的时候,可以通过在初始化 Input 和 Output processors 的时候给 Item Loader Context 进行赋值

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

ItemLoader

1
class scrapy.loader.ItemLoader([item, selector, response, ]**kwargs)

参数解释,

  • item (Item object) – The item instance to populate using subsequent calls to add_xpath(), add_css(), or add_value().
  • selector (Selector object) – The selector to extract data from, when using the add_xpath() (resp. add_css()) or replace_xpath() (resp. replace_css()) method.
    selector表示当在 Item Loader 中使用 add_xpath() (resp. add_css()) or replace_xpath() (resp. replace_css()) 等方法的时候,将会从什么地方去获取数据,也就是从该 selector 当中去获取;
  • response (Response object) – The response used to construct the selector using the default_selector_class, unless the selector argument is given, in which case this argument is ignored.
    selector参数在构造的时候不存在的时候,将会使用该response通过一个默认的default_selector_class来构建一个 selector 以便从中提取数据;见 default_selector_class 相关介绍;
  • **kwargs对应所传入参数将会传递给 Item Loader Context

Item Loader 实例所拥有的方法

get_value(value, *processors, **kwargs)

通过传入的的 processors 和关键字参数来处理这里所对应的 value 值;可用的关键字参数,

re (str or compiled regex) – a regular expression to use for extracting data from the given value using extract_regex() method, applied before processors

看一个例子,

1
2
3
>>> from scrapy.loader.processors import TakeFirst
>>> loader.get_value(u'name: foo', TakeFirst(), unicode.upper, re='name: (.+)')
'FOO`
  1. processor: TakeFirst() 取出第一个值
  2. re: 'name: (.+)'表示取字符串name:的后面的部分;

add_value(field_name, value, *processors, **kwargs)

处理以后将 value 赋值给指定的 field;

先是将 value 随同 processors 和 kwargs 参数一起传递给 get_value(),然后将其传递给 filed input processor,之后将该 Input processor 的执行结果追加到与该 filed 相关的 data collected 中;

参数field_name可以为None,这种情况下可以通过 dict 以键值的方式来指定 field:value;

1
2
3
4
5
loader.add_value('name', u'Color TV')
loader.add_value('colours', [u'white', u'blue'])
loader.add_value('length', u'100')
loader.add_value('name', u'name: foo', TakeFirst(), re='name: (.+)')
loader.add_value(None, {'name': u'foo', 'sex': u'male'})

replace_value(field_name, value, *processors, **kwargs)

Similar to add_value() but replaces the collected data with the new value instead of adding it.

get_xpath(xpath, *processors, **kwargs)

Similar to ItemLoader.get_value() but receives an XPath instead of a value, which is used to extract a list of unicode strings from the selector associated with this ItemLoader.

与 get_value() 方法非常类似,但是是通过 XPath 的方式来取代 value 取值方式,用来从 ItemLoader 所关联的 selector 中提取出一组 unicode strings;

接受的参数有,

  • xpath (str) – the XPath to extract data from
  • re (str or compiled regex) – a regular expression to use for extracting data from the selected XPath region

用例,

1
2
3
4
# HTML snippet: <p class="product-name">Color TV</p>
loader.get_xpath('//p[@class="product-name"]')
# HTML snippet: <p id="price">the price is $1200</p>
loader.get_xpath('//p[@id="price"]', TakeFirst(), re='the price is (.*)')

add_xpath(field_name, xpath, *processors, **kwargs)

1
2
3
4
# HTML snippet: <p class="product-name">Color TV</p>
loader.add_xpath('name', '//p[@class="product-name"]')
# HTML snippet: <p id="price">the price is $1200</p>
loader.add_xpath('price', '//p[@id="price"]', re='the price is (.*)')

replace_xpath(field_name, xpath, *processors, **kwargs)

Similar to add_xpath() but replaces collected data instead of adding it.

get_css(css, *processors, **kwargs)

使用 CSS 选择器从 ItemLoader 所关联的 selector 去提取数据;

对应的参数有,

  • css (str) – the CSS selector to extract data from
  • re (str or compiled regex) – a regular expression to use for extracting data from the selected CSS region
1
2
3
4
# HTML snippet: <p class="product-name">Color TV</p>
loader.get_css('p.product-name')
# HTML snippet: <p id="price">the price is $1200</p>
loader.get_css('p#price', TakeFirst(), re='the price is (.*)')

replace_css(field_name, css, *processors, **kwargs)

Similar to add_css() but replaces collected data instead of adding it.

load_item()

Populate the item with the data collected so far, and return it. The data collected is first passed through the output processors to get the final value to assign to each item field.

简而言之就是将数据填充给 Item,然后返回该 Item;

nested_xpath(xpath)

Create a nested loader with an xpath selector. The supplied selector is applied relative to selector associated with this ItemLoader. The nested loader shares the Item with the parent ItemLoader so calls to add_xpath(), add_value(), replace_value(), etc. will behave as expected.

通过 nested_xpath() 方法可以创建一个嵌入式的 Item Loader;相关用例参考 Nested Loaders

nested_css(css)

Create a nested loader with a css selector. The supplied selector is applied relative to selector associated with this ItemLoader. The nested loader shares the Item with the parent ItemLoader so calls to add_xpath(), add_value(), replace_value(), etc. will behave as expected.

get_collected_values(field_name)

Return the collected values for the given field.

这个方法比较重要,可以返回某个 filed 当前所有的 data collected 数据;补充,这里的数据是经过 Input processor 处理以后所收集的数据;

get_output_value(field_name)

Return the collected values parsed using the output processor, for the given field. This method doesn’t populate or modify the item at all.

返回通过 output processor 处理以后所得到的与某个 field 相关的数据;

get_input_processor(field_name)

Return the input processor for the given field.

需要牢记的是,Input processor 是每个 field 所单独拥有的;

get_output_processor(field_name)

Return the output processor for the given field.

需要牢记的是,Output processor 是每个 field 所单独拥有的;

Item loader 实例所拥有的属性

item

The Item object being parsed by this Item Loader.

context

The currently active Context of this Item Loader.

default_item_class

An Item class (or factory), used to instantiate items when not given in the constructor.

一个 Item 类或者是 Item 的工厂类,当实例化 ItemLoader 没有传入 Item 实例的时候,将会使用该工厂类创建一个默认的 Item;

default_input_processor

The default input processor to use for those fields which don’t specify one.

default_output_processor

The default output processor to use for those fields which don’t specify one.

default_selector_class

The class used to construct the selector of this ItemLoader, if only a response is given in the constructor. If a selector is given in the constructor this attribute is ignored. This attribute is sometimes overridden in subclasses.

当只有 response 作为参数进行构造 ItemLoader 的时候,将会使用该默认的 default_selector_class 来构造一个默认的 selector 以便从中提取数据;

selector

The Selector object to extract data from. It’s either the selector given in the constructor or one created from the response given in the constructor using the default_selector_class. This attribute is meant to be read-only.

Selector是 ItemLoader 用来提取数据的地方;不管该 selector 是通过构造函数传入还是通过response使用 default_selector_class 所构建出来的;该属性是 read-only 的;

Nested Loaders

当你要提取某个文档的一个子段落的时候,使用 Nested Loaders 将会是非常有用的;比如,我们有下面这样一个段落需要进行爬去;

1
2
3
4
5
<footer>
<a class="social" href="http://facebook.com/whatever">Like Us</a>
<a class="social" href="http://twitter.com/whatever">Follow Us</a>
<a class="email" href="mailto:whatever@example.com">Email Us</a>
</footer>

当没有使用 nested loader 的时候,你需要指定全局 xpath 或者 css 的路径来进行爬取;

1
2
3
4
5
loader = ItemLoader(item=Item())
# load stuff not in the footer
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
loader.load_item()

相反的,你可以为 footer 元素创建一个 nested loader,然后为 footer 添加相应的 values;

1
2
3
4
5
6
7
loader = ItemLoader(item=Item())
# load stuff not in the footer
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
# no need to call footer_loader.load_item()
loader.load_item()

方法其实是一样的,只是可以变重复的使用//footer选择器;

重用和扩展 Item Loaders

当你的系统日益庞大,Spiders 也越来越多以后,维护将会成为一个棘手的问题,特别是,你将会不得不为每个 spider 去处理许多不同的解析规则,所以,你希望能够尽量的去重用有些规则和爬虫应用;

Item Loaders 正是在这种背景下设计出来的;因此 Item Loaders 被设计为能够通过普通的 Python class 来进行继承已达到重用的目的;

举个例子,我们需要从各个不同的网站中去爬取商品信息,将这些不同的商品信息转换为自己结构化的,格式统一的商品数据,并保存到数据库中;但是极个别的网站中,将商品的名称用一些比较奇怪符号进行了包裹,比如类似于这样---Plasma TV---,所以,针对这些极个别的网站的商品名称,需要定义不同的提取规则;如果,我们为这些极个别的网站的极个别的元素单独去定义一个 Spider,那么必将导致代码重复而难以维护;所以,我们可以通过集成 Item Loader 的方式来为极个别的个别元素提供单独的提取逻辑,还是以 Product Item Loader 既前文所描述的ProductLoader为例,通过集成该ProductLoader,然后为商品名称 name 单独定义提取规则,来看下面这个例子,

1
2
3
4
5
6
7
8
from scrapy.loader.processors import MapCompose
from myproject.ItemLoaders import ProductLoader

def strip_dashes(x):
return x.strip('-')

class SiteSpecificLoader(ProductLoader):
name_in = MapCompose(strip_dashes, ProductLoader.name_in)

再来看一个例子,假如你要爬取的内容包含 HTML 和 XML 等多种格式,而恰恰只有 XML 需要去掉CDATA元素,那么可以通过类似上面这种继承的方式为 XML 的某个元素单独定义提取规则,

1
2
3
4
5
6
from scrapy.loader.processors import MapCompose
from myproject.ItemLoaders import ProductLoader
from myproject.utils.xml import remove_cdata

class XmlProductLoader(ProductLoader):
name_in = MapCompose(remove_cdata, ProductLoader.name_in)

我的总结

正如本章开篇中所提到的那样,Item Loader 就是提供这样一种便利的机制使得定义和维护 spider 的解析规则更加的容易,正如上面的两个例子中可以看到的那样,通过在 input processor 中定义更多的其它处理数据的规则,使得解析和提取某个元素的规则更为灵活,可扩展;来思考这样一个例子,本来,我只想爬取一个网站的中的商品数据,而该网站是我事先就观察已久的某个网站,它的数据格式非常的规范,所以呢,就通过 ItemLoader 按照最标准的方式写了第一个版本的爬虫;但是没过多久,需要爬取该网站的子网站的商品信息或者其它新的网站信息,发现,大部分信息都是匹配的,只是有一些小的匹配规则和提取规则有了新的变化,如果完全重写一套爬虫程序只是为了实现部分不同的清洗规则,会导致成本过高,代码耦合度高,不利于将来的维护,所以这个时候,就是通过集成原有的 ItemLoader 能够排上用场的时候了,这样便可以重复利用后面将要介绍的 Item Pipeline 所对应的清洗规则,使得代码能够最大限度的得到重用;

OK,上面是我学完本章节以后的整体思考,通过梳理脑海中的一个例子来贯穿其中;将来打算写这么一个爬虫程序来进行验证;

有用的内置 processors

Identity

1
class scrapy.loader.processors.Identity

最简单的一个 processor,并不做任何事情,它的功能只是将原有的值照原样输出;

1
2
3
4
>>> from scrapy.loader.processors import Identity
>>> proc = Identity()
>>> proc(['one', 'two', 'three'])
['one', 'two', 'three']

TakeFirst

1
class scrapy.loader.processors.TakeFirst

在一组数据当中返回第一个 not-null / not-empty 的数据,所以它的典型应用就是为一个 single-valued field 作为它的 Output processor;

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

Join(separator=u’ ‘)

1
class scrapy.loader.processors.Join(separator=u' ')

通过由构造函数所传入的分隔符来联合需要输出的数据值,默认的分割符是u' ';当使用默认的分隔符,这个 processor 等价于使用 function, u' '.join

1
2
3
4
5
6
7
>>> 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'

Compose(*functions, **default_loader_context)

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

Compose processor 在上述例子中被普遍的用到,可想而知它的重要性了;该 processor 是由指定的一系列的 functions 所构成的,也就是说,第一个 function 接收原始输入数据,然后将处理结果发送给下一个 function,下一个 function 将处理结果再发送至下一个 function,直到,最后一个 function 将处理结果输出;默认情况下,整个传递过程将会在遇到None值以后既停止;不过这个行为可以在构造 Compose 的时候通过关键字参数stop_on_none=False将其停止;

看一个简单的例子,

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

很简单,通过Compose的构造函数传参了两个 Function,第一个方法是取队列中的第一个元素值,第二个方法是将该元素值变为大写;

另外,每一个 function 可以通过通过参数loader_context接收 Item Loader Context 实例;

在构造 Compose 的时候,通过构造函数所传递的关键字参数将会被当做默认的 Loader context values 并将其传递到每一个 function 的调用过程中;

注意,Componse是将 [‘hello’, ‘world’] 作为一个参数,先赋值给 function 1 既 lambda v: v[0],然后将 function 1 的执行结果作为参数赋值给 function 2 既 str.upper,最后直接输出 str.upper 的执行结果,期间,并不会对输入参数做任何的转换;这一点是它和MapCompose的根本区别;

MapCompose(*functions, **default_loader_context)

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

Compose processor 类似,它也是由一组 functions 所构造而成;区别是,内部的结果将会在多个 functions 中传递,执行规则如下,

  1. MapCompose接收和转换参数的过程,
    MapCompose接收一个 iterable 参数 $\Omega$,如果该参数不是 iterable 类型,将会被强制转换成 iterable 类型,也就是说,它接收的参数必须是一个数组,如果该参数不是数组,也会被强制转换成数组;
  2. MapCompose执行 functions 的过程,
    • 首先,MapCompose迭代 $\Omega$ 数组,然后依次得到数组元素 [$\omega_1$, $\omega_2$, $\omega_3$ …],
    • 然后,MapCompose依次将数组元素 $\omega_1$, $\omega_2$, $\omega_3$ … 作为参数分别调用 funtions 中的第一个方法 $f_1$,并依次得到结果 $r_1$、$r_2$、$r_3$
    • 然后,MapCompose自动的将 $r_1$、$r_2$、$r_3$… 拼接成一个新的数组 [$r_1$、$r_2$、$r_3$…],然后循环遍历这个 $r$ 数组,依次调用 $f_2$,并将结果再次拼接成一个数组,循环遍历,调用 $f_3$,以此类推,直到调用结束;

整个过程中要注意的是,

  1. 如果调用过程中,针对某一个输入元素 $\omega$ 的 function 返回的是None,那么该 $\omega$ 所对应的结果将会被自动忽略,并不会成为下一次级联的输入元素;
  2. MapCompose计算的结果,输出的是一个数组;
    正是因为每一次 function 调用都是以数组元素 $\omega$ 作为 function 的入参,因此,它经常被作为 Input processor 使用,因为数据经常是通过 SelectorList( selectors ) 的方法 extract() 进行提取的,因此,一个 MapCompose 便可以作用在 selectors 所返回的 values 上面,非常方便;

一些例子,

  • 例子 1,接收一个数组作为参数

    1
    2
    3
    4
    5
    6
    7
    >>> 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']

    MapCompose 在执行过程中,是将数组 [u’hello’, u’world’, u’this’, u’is’, u’scrapy’] 中的每一个元素单独作为参数分别调用 $f_1$ 既 filter_world(x),然后将结果合并成一个新的数组,然后再次以同样的逻辑调用 $f_2$ 既 unicode.upper;执行过程中,由于 $f_1$ 会过滤掉 world,过滤的方式就是当入参是字符 word 则返回一个 None,而由前面的描述中可知,None将会被自动的过滤掉,不会被 MapCompose 拼接到新的数组中;因此最后输出的结果是除了 word 以外其它字符的大写数组;

  • 例子 2,将入参转换成数组,

    1
    2
    3
    4
    >>> from scrapy.loader.processors import MapCompose
    >>> proc = MapCompose(lambda v: v[0], str.upper)
    >>> proc('hello')
    >>> ['H']

    这里的执行过程是,首先将 ‘hello’ 封装成 [‘hello’],然后迭代出仅有的 ‘hello’ 元素,作为参数调用 lambda v: v[0],这里自然,返回的就是 ‘hello’ 的第一个字母 h,然后将 h 封装成 [‘h’],调用 str.upper,得到 ‘H’,最后 MapCompose 将结果 ‘H’ 再次封装成数组返回;

MapCompose vs Compose

为了加深对 MapCompose 和 Compose 的理解,单独撰写一个小节的内容来对比两者之间的特性和异同;来看下面这个例子,

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

同样的例子,我们使用 MapCompose 来执行以下,

1
2
3
4
>>> from scrapy.loader.processors import MapCompose
>>> proc = MapCompose(lambda v: v[0], str.upper)
>>> proc(['hello', 'world'])
>>> ['H', 'W']

  • 可以看到 Compose 不会对入参做多余的任何操作,而是直接将参数 [‘hello’, ‘world’] 直接赋值给 $f_1$,所以 $f_1$ 既过滤方法 lambda v: v[0] 返回的是数组 [‘hello’, ‘world’] 中的第一个元素 ‘hello’,然后将结果直接赋值给 $f_2$,返回大写的 ‘HELLO’,可见,整个执行过程中,Compose 不对入参和结果做任何的转换,计算出来是什么值就返回什么值;因此 Compose 非常适合做 out processor
  • 而 MapCompose 则恰恰相反,不但对输入参数做转换(若不是 iterable 的参数会将其强制转换成 iterable 既数组)而且对结果也要做强制转换(将结果拼接并转换成数组),也正是因为这一个特性,使得 MapCompose 非常适合做 input processor

SelectJmes(json_path)

1
class scrapy.loader.processors.SelectJmes(json_path)

Queries the value using the json path provided to the constructor and returns the output. Requires jmespath (https://github.com/jmespath/jmespath.py) to run. This processor takes only one input at a time.

  1. 可以在 lists 或者 dict 上使用

    1
    2
    3
    4
    5
    6
    >>> 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'}
  2. 使用到 JSON string 上

    1
    2
    3
    4
    5
    6
    7
    >>> 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']

    注意,这里好玩的是第二个 Compose 构造器,第一个构造参数是一个普通的函数,但是注意第二个构造参数,是一个通过 SelectJmes() 作为构造参数所构造的MapCompose对象;那么我们来简单的分析一下它的执行流程,首先,输入的 json string 将会分别作用到这两个方法中,第一个是 json.loads 方法,将会把输入的 json string输入值拆分成两个键值元素的队列 $L$,然后该队列将会作为第一个 function 的输出传递给 MapCompose 对象,因为 MapCompose 是一个 processor,所以将会按照该 processor 的特性对输入值进行处理,这里根据MapCompose的特性,会将队列 $L$ 里面的所有元素分别作用于方法 SelectJmes(‘foo’),所以,只有 {“foo”:”bar”} 元素与之匹配,因此输出的是 ‘bar’;

    上面的例子中描述了一种特殊的 processor 调用情况,那就是 processor 中嵌入另外一个 processor,感觉这是一个比较重要的特性,可以针对这个特性单独再写出一个小节出来单独描述;

我的总结

Item Loader 的设计目的就是尽最大的可能性为所爬去的元素定义可扩展的规则,转换规则,并且通过重载 Item Loader 的方式能够最大限度的重用这些规则;使得当爬虫项目日益庞大以后,在运维上是可控的;

下一步要做的就是,写一个完整的例子,从前往后全部将其串通理解;