Item Loaders
item loaders 为填充scraped items 提供了方便的机制。即使 items 可以使用自己的 字典一样的 API 填充,但是 item loaders 在抓取过程中提供了更方便的API ,通过自动执行一些常见的任务,像在分配提取出来的原始数据之前解析它
也就是说,items 提供了抓取数据的容器,item loaders 提供了填充这 个容器的机制。
item loaders 设计用来提供一个灵活,高效简单的机制,通过爬虫或资源格式(HTML XML 等)用来里扩展和重写不同的字段的解析规则,而不是成为维护的噩梦。
Using Item Loaders to populate items
为了使用,你首先要实例化它,你可以实例化带有一个字典样式的对象(item or dict)或啥也没有,这种情况下,使用在 itemloader.default_item_class 属性中指定的 item 类在 item loaders _init_ 方法中自动实例化item
然后你可以收集值进item loaders ,通常使用 selectors. 你可以增加多个值给一个字段;item loader 将知道如何使用正确的处理功能来将这些值加进去。
Note
收集的数据在内部保存成列表,可以给同一个字段增加一些值,如果当创建一个 loader时传递了 item 的参数,每个 item的值如果是可以迭代的就原样保存,如果是单个值就包装在列表中。
这是爬虫中典型的用法,使用在上一章中声明的项目.
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 提出来的数据
//div[@class="product_name"]
//div[@class="product_title"]
也就是说,数据使用add_xpath() 方法收集了两个xpath 的位置提取的数据。然后name字段会接受这个数据。
然后,相同的用法对 price and stock 字段(后者使用了CSS选择),最后 last_update字段直接用add_value() 方法填充了一个文字值(today)。
当所有的数据被收集, ItemLoader.load.item()方法被调用,实际上返回的之前使用 add_xpaht() 等提取收集的数据。
Input and Output processors
item loader 对每一个字段都包含了一个输出和输入处理器。输入处理器处理接受的提取的数据,结果收集并保存在itemloader里。在收集完所有数据后,itemloader.load_item()方法被调用来填充并获得要填充的item 对象。在先前的收集过程中输出处理器被调用。输出处理器的结果就是分配给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)
So what happens is:
- 从 xpath1 提取出数据,传递给 name 字段的输入处理器。结果收集并保存在 item loader(但是还没分配给item)
- xpath2 提取来的数据也是一样
- 处理使用CSS 选择器其他跟上面一样
- 这个值是直接分配的,而不是从选择器中提取来的。但是这个值仍然通过输入处理器传递。这个例子中,因为值是不可迭代的所以在传递给输入处理器是会转换成一个可迭代的单一元素。因为输入处理器总是接受可迭代的。
- 以上几步收集的数据传递给name 字段的输出处理器。然后分配给item里的 name 字段
处理器只是可调用的对象,用来解析数据并返回解析的值。你可以用任何函数最为输入或输出处理。只是他们必须接受一个(只能是一个)可迭代的位置参数。
Changed in version 2.0: Processors no longer need to be methods.
Note
输入输出处理器必须接受一个可迭代对象作为第一个参数。这些函数的输出可以是任意的。输入处理器的结果将被添加进内部的包含收集值的列表。输入处理器的结果是最终分配个item的值。
其他要记住的是输入处理的值被收集进内部列表然后传递给输出处理器来填充字段。
最后但不是重要的(高中写作文就会用这个。。),scrapy 为了方便也内置了一些常用处理器。
Declaring Item Loaders
item loaders 像item一样声明,通过使用类定义的语法。
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join # 这三个下面有
class ProductLoader(ItemLoader):
default_output_processor = TakeFirst() # 这个应该是写在 spider 文件里的
name_in = MapCompose(unicode.title)
name_out = Join()
price_in = MapCompose(unicode.strip)
# ...
输入处理器使用_in 后缀来声明,_out 后缀来声明输出处理器。你可以使用 ItemLoader.default_input_processor
and ItemLoader.default_output_processor
属性来声明默认的输入输出处理器。
Declaring Input and Output Processors
在之前的部分,输入和输出处理器可以在 item loader 定义时声明,这种方式很常用的。然而,这有更多的位置可以来指定处理器:在 item 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), # 感觉就是多加了一层对数据的处理,这个就是写在 item 文件里的吧
output_processor=Join(),
)
price = scrapy.Field(
input_processor=MapCompose(remove_tags, filter_price),
output_processor=TakeFirst(),
)
>>> 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'€', u'<span>1000</span>'])
>>> il.load_item()
{'name': u'Welcome to my website', 'price': u'1000'}
对所有输入和输出处理器的优先级顺序如下:
- item loader 指定的字段属性:field_in and field_out(优先级最高)
- 字段元数据(input_processor and output_processor 关键字)
- Item Loader defaults:
ItemLoader.default_input_processor()
andItemLoader.default_output_processor()
(优先级最低。)
See also: Reusing and extending Item Loaders.
Item Loader Context
item loader context 是任意键/值的字典,在所有的输入输出处理器间分享。可以在声明,实例化或使用 item loader 时传递。用来修改输入输出处理器的行为。
假设你有一个 parse_length 函数,接收一个文本并从中提取一个长度。
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 在调用时,传递当前活跃的context, 并且处理器函数(这里就是parse_length)可以使用他们。
修改 item loader context 值的几种方式:
- 通过修改当前活动的item loader context( context 属性)
loader = ItemLoader(product)
loader.context['unit'] = 'cm'
-
在item loader 的实例中(itemloader的_init_ 方法的关键字参数存储在 item loader context 中。)
loader = ItemLoader(product, unit='cm')
-
在声明中,就是那些支持使用item loader context 来实例化输入输出处理器的,MapCompose 就是其中之一
class ProductLoader(ItemLoader):
length_out = MapCompose(parse_length, unit='cm')
ItemLoader objects
class scrapy.loader.ItemLoader([item,selector,responses,] \ **kwargs)
返回一个新的item loader 来填充所给的 item。如果没给item ,自动使用 default_item_class 里的类。
当带有 selector or resposne参数实例化时,itemloader 类提供了便捷的机制使用selector 来从网页中提取数据
-
Parameters
item (
Item
object) – 使用随后调用的 add_xpath add_css()等来填充item 的实例。selector (
Selector
object) – 使用 。。。 方法来提取数据response (
Response
object) – response 使用 defaultl_selector_class 来构造选择器,除非选择器参数被给了,然后这个参数就忽略了。
item,selector,reponse 和其他的关键字参数被分配给loader context(可以通过 context 属性访问)
itemloader 实例有以下方法:
get_value(value,*processors,**kwargs)
通过所给的processors 和 关键字参数处理所给的 value
可用的关键字参数
-
Parameters
re (str or compiled regex) – 一个正则使用 extract_regx() 方法从所给的值来提取数据,在处理器之前使用。
Examples:
>>> from scrapy.loader.processors import TakeFirst
>>> loader.get_value(u'name: foo', TakeFirst(), unicode.upper, re='name: (.+)')
'FOO`
add_value(field_name, value, *processors, **kwargs)
处理然后为所给的字段增加值。
value 首先通过所给的 processors and kwargs 传递给get_value(),然后通过 field inpit processor 传递,他的结果添加到字段的数据收集里。如果字段已经包含了收集的数据,新的就加上。
field_name 可以是 None,这时值可以为大量的字段添加,处理后的值应该时字段名和值匹配的字典。
Examples:
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)[source]
跟 add_value() 一样,但新的值不是增加而是替换。
get_xpath
(xpath, *processors, **kwargs)[source]
跟 get_value() 一样,但是接受xpath 而不是值。用来从这个 itemloader 关联的选择器中提取 unicode 字符串的列表。
-
Parameters
xpath (str) – the XPath to extract data from
re (str or compiled regex) – 正则,从xpath 提取出来的区域中进一步提取。
Examples:
# 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)[source]
跟 add_value()一样。。。
See get_xpath()
for kwargs
.
-
Parameters
xpath (str) – the XPath to extract data from
Examples:
# 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)[source]
一样
load_item
()[source]
用目前收集的数据填充item ,然后返回,收集的数据首先通过输出处理器传递,以得到最终的值然后分配给每个 item 字段。
nested_xpath
(xpath)[source]
创造一个带有一个xpath 选择器的嵌套加载器。提供的选择器相对于与itemloader 关联的选择器应用(嗯,,实在看不懂什么意思欸)。嵌套加载器与父类的itemloader 分享item,因此调用 add_xpath() add_value() replace_value() 等行为都是预期的。
get_collected_values
(field_name)[source]
返回为所给字段收集的值
get_output_value
(field_name)[source]
返回所给字段使用输出处理器解析后收集的值,这个方法不填充或修改item
get_input_processor
(field_name)[source]
返回所给字段的输入处理器
get_output_processor
(field_name)[source]
Return the output processor for the given field.
itemLoader 实例有以下方法:
-
item
item 对象通过这个 item loader 解析,最重要的用法时作为属性,所以在尝试重写此值时,你要先检测 default_item_class 。
-
context
这个item loader 当前活跃的 Context
-
default_item_class
一个 item 类,当 _init_ 方法中没赋值时用来实例化 item
-
default_input_processor
给那些没有指定的字段用的默认输入处理器。
-
default_output_processor
The default output processor to use for those fields which don’t specify one.
-
default_selector_class
如果在_init_ 方法里只给了一个响应,这个类就用来构建这个 ItemLoader 的 selector,如果在 _init_ 方法中指定了选择器,这个属性就忽略。有时会在子类中重写这个属性。
-
selector
提取数据的选择器对象,它可以是_init_ 方法中所给的 selector 或是使用default_selector_class 从_init_ 方法中给出的响应创建的选择器。这个属性是只读的。
Nested Loaders
当从一个文档的一个小节解析值时,可以创建一个嵌套加载器。想象你正在从下面这个页面的页脚中提取数据。
Example:
<footer>
<a class="social" href="https://facebook.com/whatever">Like Us</a>
<a class="social" href="https://twitter.com/whatever">Follow Us</a>
<a class="email" href="mailto:whatever@example.com">Email Us</a>
</footer>
没有嵌套处理器,你需要指定完整的 xpath or css 。
Example:
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()
.你可以创建一个页脚选择器的嵌套加载器并增加相对于页脚的值,功能时一样的但是可以避免重复的页脚选择器。
Example:
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()
你可以任意嵌套加载器,他们都是用 xpath or css 选择器工作。一个指导原则是,在你的代码更加简单但是不会过度嵌套或难以阅读时使用嵌套加载器。
Reusing and extending Item Loaders
当你的项目变大需要更多的爬虫,保养就变成了基本的问题,特别是当你需要解决许多不同的解析规则,对每个爬虫都有列外,但是你也会想重用常用的处理器。
Item Loaders 被设计来缓解保养解析规则的负担,而不会失去灵活性。同时,提供了方便的机制来扩展或重写他们。因此,item loader 支持传统的python类继承来解决指定爬虫(或一组爬虫)的不同
假如,一些特别的网站将他们的产品名包含到了三个括折号里(即 —TV—),而且你不想抓取这些括折号进最终的产品名里。
这是你怎样通过重用和扩展默认项目的 item loader(ProductLoader) 来移除这些括折号。
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) # 就是在原来的基础上加个功能吧
另一种例子是当你有大量的资源格式时,扩展item loaders 是很有用的。例如XML and HTML,在XML 版本中,你可能想要删除 CDATA 出现的地方。
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 ,不同item loaders 的层次结构可能更适合不同的项目。scrapy 只提供了机制,他不会给你的loaders 集合加上任何指定的组织,这取决于你项目的需要。
Available built-in processors
尽管你可以调用任何可调用的函数作为输入输出处理器,scrapy 提供一些通用的处理器。他们中的一些,像 MapCompose(通常用来做输入处理器) 构成了几个函数按顺序执行的输出,来产生最终解析出来的值。
Here is a list of all built-in processors:
class scrapy.loader.processors.Identitty
这是最简单的处理器,他不会做任何事,他返回没有改变的最初的值。他不会接受任何_init_ 方法的属性,也不接受 loader contexts。
Example:
>>> from scrapy.loader.processors import Identity
>>> proc = Identity()
>>> proc(['one', 'two', 'three'])
['one', 'two', 'three']
class scrapy.loader.processors.TakeFirst
从接收的值中返回第一个不是缺失值不是空 的值,所以这个通常作为一个输出处理器产生单一值字段来使用。他不会接受任何_init_ 方法的属性,也不接受 loader contexts。
Example:
>>> from scrapy.loader.processors import TakeFirst
>>> proc = TakeFirst()
>>> proc(['', 'one', 'two', 'three'])
'one'
class scrapy.loader.processors.Join(separator='')
返回使用_init_ 方法给定的分隔符连接的值,默认是 u’’ ,不接受loader contexts 。
当使用默认的分隔符,这个处理器等价于函数 u’ '.join
Examples:
>>> from scrapy.loader.processors import Join
>>> proc = Join()
>>> proc(['one', 'two', 'three'])
'one two three'
>>> proc = Join('<br>')
>>> proc(['one', 'two', 'three'])
'one<br>two<br>three'
class scrapy.loader.processors.Compose(*functions,**default_loader_context)
从所给的函数的组成构建一个处理器。也就是处理器输出的每一个值都会传递给第一个函数,这个函数的结果在传递给第二个函数,等等知道最后的函数返回这个处理器的输出值。
默认在出现None 后停止进程。这个行为可以通过传递 stop_on_none=False 这个关键字参数来改变,
Example:
>>> from scrapy.loader.processors import Compose
>>> proc = Compose(lambda v: v[0], str.upper)
>>> proc(['hello', 'world'])
'HELLO'
每个函数可以选择接收一个loader_context 参数,对于接收了的,这个处理器将通过这个参数传递当前活跃的 loader context。
传递进 _init_ 方法的关键字参数默认用来作为传递给每个函数调用的Loader context 值,但是,最终传递给函数的loader context 会通过 itemloader.context() 属性访问当前活跃的loader context 来重写原来那个。
class scrapy.loader.processors.MapCompose(*functions,**default_loader_context)
跟 compose 一样,也是通过所给函数的组成来构造处理器。不同的是传递给其中函数值的方式,如下:
迭代这个处理器的输入值,第一个函数被应用到每个函数。这些函数调用的结果被串联构造出一个新的代理器,然后应用到第二个函数,等等直到最后一个函数被目前收集的值列表应用。最后一个函数输出的值被串联在一起来产生这个处理器的输出。
每个特别的函数都可以返回一个值或值列表,这些通过输入不同值的相同函数返回的值列表扁平化。(Each particular function can return a value or a list of values, which is flattened with the list of values returned by the same function applied to the other input values.这个逻辑我真是理不清啊)。这些函数可以返回None ,这时,该函数的输出被忽略,来对链上进行更深入的处理
这个处理器提供了方便的方式来组合只处理单个值的函数(而不是可迭代的)。所以MapCompose 处理器通常作为输入处理器,因为使用 extract() 方法提取出来的数据返回的是Unicode 字符串的列表。
The example below should clarify how it works:
>>> def filter_world(x):
... return None if x == 'world' else x
...
>>> from scrapy.loader.processors import MapCompose
>>> proc = MapCompose(filter_world, str.upper)
>>> proc(['hello', 'world', 'this', 'is', 'scrapy'])
['HELLO, 'THIS', 'IS', 'SCRAPY']
跟conpose processor一样,函数可以接受 loader contexts,并且_init_ 方法的关键字参数被用作默认的 context 值。详见 compose。
class scrapy.loader.processer.SelectJmes(json_path)
使用提供给 _init_ 方法的json 路径查询值,并返回输出。需要 jmespaht(https://github.com/jmespath/jmespath.py) 才能运行。这个处理器一次只接受一个输入。
Example:
>>> 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'}
Working with Json:
>>> import json
>>> proc_single_json_str = Compose(json.loads, SelectJmes("foo"))
>>> proc_single_json_str('{"foo": "bar"}')
'bar'
>>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo')))
>>> proc_json_list('[{"foo":"bar"}, {"baz":"tar"}]')
['bar']
翻译真是不简单啊。。。。。