本文为译文,原文见地址:https://docs.scrapy.org/en/latest/topics/loaders.html
数据项加载器
数据项加载器提供了一个方便的机制来填充爬取到的数据项(Item)。即使数据项可以使用它们自身的类似于字典API的方式来填充数据,但是数据项加载器提供了一个更方便的API从抓取过程中来填充它们。整个过程简单说就是通过自动化一些常见的任务——比如在分配数据之前解析提取的原始数据。
换句话说,数据项提供了一个容器,用于存储爬取到的数据。而数据项加载器提供了填充该容器的机制。
数据项加载器设计宗旨是提供一个灵活的、高效的和简单的机制来扩展和覆盖不同的字段解析规则,既可以通过爬虫,也可以通过源格式(HTML,XML等),使维护更加简单。
使用数据项加载器来填充数据项
为了使用一个数据项加载器,首先你必须初始化它。你既可以使用类似于字典的对象(比如Item或者字典)来初始化它,也可以不使用它——在这种情况下,使用ItemLoader.default_item_class属性中指定的Item类,在数据项加载器构造函数中自动实例化一个Item。
然后,你可以在数据项加载器中收集值了,典型方式即使用Selector。你可以对同样的Item字段中添加不止一个值;数据项加载器自己会知道如何在以后使用适当的处理函数“连接”这些值。
在爬虫中有一个典型的数据项加载器,使用数据项(Item)章节中已经声明好的Product项:
from scrapy.laoder 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') # 你也可以使用文字值
return l.load_item()
快速浏览上述代码,我们可以看到name字段可以从页面中两个不同XPath位置提取:
- //div[@class=“product_name”]
- //div[@class=“product_title”]
换句话说,通过使用add_xpath()函数从两个XPath位置提取数据以便收集数据。随后,这个数据将分配给name字段。
然后,对price和stock字段使用类似的调用(后者使用带有add_css()函数的CSS选择器),最后使用不同的函数add_value()直接用字面值(today)填充last_updated字段。
最后,在收集所有数据时,将调用ItemLoader.load_item()函数,该函数实际返回先前调用add_xpath()、add_css()、add_value()函数提取和收集的数据来填充数据项。
输入和输出处理器
数据项加载器对每一个item字段,均包含一个输入处理器和一个输出处理器。一旦接收到提取的数据(通过add_xpath()、add_css()和add_value()函数),输入处理器就会对其进行处理,然后收集输入处理器的结果并保存在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)
这里发生了什么:
- 从xpath1来的数据被提取出,并且传递给name字段的输入处理器。输入处理器的结果已经被收集并且存储到数据项加载器中(但目前没有赋值给Item对象)。
- 从xpath2来的数据被提取出,并且传递给(1)中的输入处理器。输入处理器的结果被添加到(1)中已经收集了的数据(如果有任何数据的话)。
- 这里的情景与之前类似,除了数据是从css(CSS选择器)中提取出的,并且传递给(1)和(2)中的输入处理器。输入处理器的结果将被添加到(1)和(2)中已经收集的数据(如果有任何数据的话)。
- 这个情景同样与前面的类似,除了收集的数据是直接赋值,而不是从XPath表达式或者CSS选择器中提取的。然而,这个值依然传递给了输入处理器。在这个情境中,由于这个值不是迭代器,因此它在传递给输入处理器之前将被转换为一个只含有单独元素的迭代器,因为输入处理器总是接收迭代器。
- 在(1),(2),(3)和(4)步骤中收集的数据,将传递给name字段的输出处理器。输出处理器的结果是已经赋值给Item对象的name字段的值。
值得注意的是,处理器仅仅是可调用的对象,这个对象要与将被解析的数据一起调用,并且返回一个解析后的值。因此你可以使用任何函数作为输入或者输出处理器。这些函数只需要一个必备条件:必须接受一个(且只能是一个)位置参数,这个位置参数是一个迭代器。
注意:输入和输出处理器都必须接收一个迭代器作为它们的第一个参数。这些函数的输出可以是任何值。输入处理器的结果将被追加到一个内部列表(在加载程序中),该列表包含(该字段的)收集的值。输出处理器的结果是最终分配给Item对象的值。
如果你希望使用一个简单的函数作为处理器,请确定它的第一个参数是self:
def lowercase_processor(self, values):
for v in values:
yield v.lower()
class MyItemLoader(ItemLoader):
name_in = lowercase_processor
这是因为每当一个函数赋值给类变量时,这个变量就会成为一个类函数,并在被调用时作为第一个参数传递给实例。有关更多细节,请参阅stackoverflow上的回复。
另外你需要记住的是,输入处理器返回的值,是内部收集的(在列表中),并且随后将传递给输出处理器进行字段的填充。
为了方便使用,Scrapy还内置了一些通用的处理器,后续会讲到。
声明数据项加载器
数据项加载器的声明就像是声明数据项(Item)一样,使用类定义的语法。这里有一个示例:
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)
# ...
正如你所看到的,使用_in后缀声明输入处理器,同时使用_out后缀声明输出处理器。你也可以使用ItemLoader.default_input_processor和ItemLoader.default_output_processor属性来声明默认的输入/输出处理器。
声明输入和输出处理器
我们从前一小节了解到,输入和输出处理器能在数据项加载器(Item Loader)定义的时候进行声明,并且这是非常常用的声明输入处理器的方法。然而,这里还有其他方法指定输入和输出处理器:在数据项字段(Item Field)元数据中。示例如下:
import scrapy
from scrapy.laoder.processor 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(),
)
>>> from scrapy.loader import ItemLoader
>>> il = ItemLoader(item=Product())
>>> il.add_value('name', ['Welcome to my', '<strong>website</strong>'])
>>> il.add_value('price', ['€', '<span>1000</span>'])
>>> il.load_item()
{'name': 'Welcome to my website', 'price': '1000'}
对于输入和输出处理器的优先级,如下所示:
- 数据项加载器字段特定的属性:field_in和field_out(优先级最高)
- 字段元数据(input_processor和output_processor关键字)
- 数据项加载器默认值:ItemLoader.default_input_processor()和ItemLoader.default_output_processor()(优先级最低)
请参见:重用和扩展数据项加载器。
数据项加载器上下文
数据项加载器上下文是数据项加载器中所有输入和输出处理器共享的任意键/值的字典。它可以在声明、实例化或者使用数据项加载器时传递。它们用于修改输入/输出处理器的行为。
举个栗子,假设你有一个parse_length函数,这个函数接收一个文本值,并且提取这个文本值的长度:
def parse_length(text, loader_context):
unit = loader_context.get('unit', 'm')
# ... 这里是长度解析代码 ...
return parsed_length
通过接受loader_context参数,函数显示地告诉数据项加载器,它能够接收数据项上下文,因此数据项加载器在调用它时会传递当前活动的上下文,所以处理器函数(这里指的是parse_length)可以使用它们。
这里有一些修改数据项加载器上下文值的方法:
1.通过修改当前激活的数据项加载器上下文(context属性):
loader = ItemLoader(product)
loader.context['unit'] = 'cm'
2.在数据项加载器实例化的时候(数据项加载器的构造函数中,传递关键字参数将会存储在数据项加载器上下文中):
loader = ItemLoader(product, unit='cm')
3.在数据项加载器声明上,用于那些支持使用数据项加载器上下文实例化它们的输入/输出处理器。
MapCompose就是其中一种:
class ProductLoader(ItemLoader):
length_out = MapCompose(parse_length, unit='cm')
ItemLoader对象
class scrapy.loader.ItemLoader([item, selector, response, ]**kwargs)
返回一个新的数据项加载器来填充给定的item。如果没有给定item,那么将使用default_item_class中的一个来自动实例化。
当使用selector参数或者response参数实例化的时候,ItemLoader类提供了使用选择器从web页面提取数据的便利机制。
参数:
- item(Item对象)- 当调用add_xpath(),add_css()或者add_value()时,要填充item实例。
- selector(Selector对象)- 当使用add_xpath()(或者add_css())或者replace_xpath()(或者replace_css())函数时,要从其中提取数据的选择器。
- response(Response对象)- 通过使用default_selector_class来构建选择器的响应,当选择器参数已给定时,这个参数将被忽略。
item,selector,response和其他关键字参数被分配给加载器上下文(可通过context属性访问)。、
ItemLoader实例还有如下函数:
get_value(value, *processors, **kwargs)
通过给定的processors和关键字参数来处理给定的value值。
可用的关键字参数:
参数: re(字符串或者编译后的正则表达式)- 使用extract_regex()函数,从给定的value中按照一个正则表达式提取数据,在处理器之前应用。
举个栗子:
>>> from scrapy.loader.processors import TakeFirst
>>> loader.get_value('name: foo', TakeFirst(), str.upper, re='name: (.+)')
'FOO'
add_value(field_name, value, *processors, **kwargs)
将给定的value赋值到给定的字段。
该值首先通过传递给出的processors和kwargs参数到get_value()函数,然后通过字段输入处理器,其结果追加到该字段收集的数据中。如果字段已经包含收集的数据,则添加新数据。
在添加多个字段值的情况下,给定的field_name可以为None。并且处理过的值应该是一个存在field_name与value映射的字典。
举个栗子:
loader.add_value('name', 'Color TV')
loader.add_value('colours', ['white', 'blue'])
loader.add_value('length', '100')
loader.add_value('name', 'name: foo', TakeFirst(), re='name: (.+)')
loader.add_value(None, {'name': 'foo', 'sex': 'male'})
replace_value(field_name, value, *processors, **kwargs)
与add_value()类似,但是当有新的值时,直接替换了收集的数据,而不是添加。
get_xpath(xpath, *processors, **kwargs)
与ItemLoader.get_value()类似,但是接受一个XPath而不是value,这个XPath将从与此ItemLoader关联的选择器中提取unicode字符串列表。
参数:
- xpath(str)- 提取数据的XPath
- re(str或者编译后的正则表达式)- 一个正则表达式,用来从选中的XPath区域中提取数据
举个栗子:
# HTML片段:<p class="product-name">Color TV</p>
loader.get_xpath('//p[@class="product-name"]')
# HTML片段:<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)
与ItemLoader.add_value()类似,但是接受一个XPath而不是value,这个XPath被用来从与当前ItemLoader关联的选择器中提取unicode字符串列表。
kwargs参数参见get_xpath()解释。
参数:
- xpath(str)- 提取数据的XPath
举个栗子:
# HTML片段:<p class="product-name">Color TV</p>
loader.add_xpath('name', '//p[@class="product-name"]')
# HTML片段:<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)
与add_xpath()类似,但是替换了收集的数据而不是追加。
get_css(css, *processors, **kwargs)
与ItemLoader.get_value()类似,但是接受一个CSS选择器而不是value,这个CSS被用来从与当前ItemLoader关联的选择器中提取unicode字符串列表。
参数:
- css(str)- 提取数据的CSS选择器
- re(str或者编译后的正则表达式)- 一个正则表达式,用来从选中的CSS区域中提取数据
举个栗子:
# HTML片段:<p class="product-name">Color TV</p>
loader.get_css('p.product-name')
# HTML片段:<p id="price">the price is $1200</p>
loader.get_css('p#price', TakeFirst(), re='the price is (.*)')
add_css(field_name, css, *processors, **kwargs)
与ItemLoader.add_value()类似,但是接受一个CSS选择器而不是value,这个CSS选择器被用来从与当前ItemLoader关联的选择器中提取unicode字符串列表。
kwargs参数参见get_css()解释。
参数:
- css(str)- 提取数据的CSS选择器
举个栗子:
# HTML片段:<p class="product-name">Color TV</p>
loader.add_css('name', 'p.product-name')
# HTML片段:<p id="price">the price is $1200</p>
loader.add_css('price', 'p#price', re='the price is (.*)')
replace_css(field_name, css, *processors, **kwargs)
与add_css()类似,但是替换了收集的数据而不是追加。
load_item()
使用当前收集到的数据来填充数据项,并返回这个数据项。收集到的数据首先通过输出处理器传递,以获得分配给每个数据项字段的最终值。
nested_xpath(xpath)
使用xpath选择器来创建一个嵌套的加载器。提供的选择器将应用于与此ItemLoader关联的选择器。嵌套的加载器共享了其父级ItemLoader的数据项(Item),因此调用add_xpath()、add_value()、replace_value()等函数的行为与预期一致。
nested_css(css)
使用css选择器来创建一个嵌套的加载器。提供的选择器将应用于与此ItemLoader关联的选择器。嵌套的加载器共享了其父级ItemLoader的数据项(Item),因此调用add_xpath()、add_value()、replace_value()等函数的行为与预期一致。
get_collected_values(field_name)
返回给定字段的收集值。
get_output_value(field_name)
返回给定字段经过输出处理器解析后的收集值。这个函数不会填充或者修改数据项。
get_input_processor(field_name)
返回给定字段的输入处理器
get_output_processor(field_name)
返回给定字段的输出处理器
ItemLoader实例还有如下属性:
item
该ItemLoader解析后的Item对象。
context
该ItemLoader当前活跃的上下文(Context)。
default_item_class
一个Item类(或者工厂),当在构造函数中没有指定数据项时使用这个值进行初始化。
default_input_processor
没有指定输入处理器时,使用此默认值作为输入处理器。
default_output_processor
没有指定输出处理器时,使用此默认值作为输出处理器。
default_selecotr_class
该ItemLoader用来构造选择器(Selector)的类,当ItemLoader的构造函数中只给出了response参数时使用这个属性。如果ItemLoader的构造函数中给定了一个selector参数,那么将忽略这个属性。有时候可以在派生类中重写这个属性。
selector
用来提取数据的选择器(Selector)对象。这个值既可以在构造函数中给定,也可以由构造函数中给定的response使用default_selector_class来创建。这意味着这个属性只能是只读属性。
嵌套的加载器
当从文档的一个小节解析相关值时,创建嵌套加载器可能会很有用。假设你正在从一个页面的页脚提取某些细节,这个页脚看起来可能是这样的:
<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(或者css):
loader = ItemLoader(item=Item())
# 加载的东西不在页脚
loader.add_xpath('social', '//footer/a[@class="social"]/@href')
loader.add_xpath('email', '//footer/a[@class="email"]/@href')
loader.load_item()
上面的替换方案是:对页脚的选择器创建一个嵌套的加载器,并且添加与页脚相关的值。这样做的功能其实与之前一致,只是你可以避免重复页脚的选择器:
loader = ItemLoader(item=Item())
# 加载的东西不在页脚
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class="social"]/@href')
footer_loader.add_xpath('email', 'a[@class="email"]/@href')
# 不需要调用footer_loader.load_item()
loader.load_item()
你可以任意嵌套选择器,它们既可以与xpath选择器工作,也可以与css选择器工作。一般的指导原则是,当嵌套加载器使你的代码更简单,但又不过分嵌套或解析器变得难以阅读时,你就可以使用它。
重用和扩展数据项加载器
当你的项目变得越来越大并且需要更多的爬虫时,维护将成为一个最根本的问题,尤其是当你必须对每个爬虫处理不同的解析规则,拥有一大堆异常,但是也希望重用常用的处理器。
数据项加载器的设计目的就是减轻解析规则的维护负担,同时又不会失去灵活性,且为扩展和覆盖规则提供了便利的机制。出于这个原因,数据项加载器支持传统Python的类继承来处理特定爬虫(或者爬虫组)的差异。
例如,假设某个特定网站将它们的产品名称用三个横杠括起来(例,—Plasma TV—),并且你不希望在最终产品名称中使用这些横杠。
这里的示例演示了,如何重用和扩展默认的Product数据项加载器(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)
扩展数据项加载器非常有用的另一种情况是,当你有多种源格式时,例如XML和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)
这就是典型的扩展输入处理器的方式。
对于输出处理器来说,更加常用的是在字段元数据中声明它们,因为它们通常只依赖于字段,而不依赖于每个特定的站点解析规则(输入处理器就是这样做的)。可参见:声明输入和输出处理器。
这里还有一些其他可能的方式来扩展,继承和重写你的数据项加载器,并且不同的数据项加载器层次结构可能更适合不同的项目。Scrapy仅提供这个机制,它并不强制你的加载器集合的任何特定组织——这取决于你和你的项目需求。
可用的内置处理器
虽然你可以使用任意可调用的函数作为输入和输出处理器,Scrapy依然提供了一些常用的处理器,后面会有这些处理器的描述。其中一些处理器,比如MapCompose(常用来作为输入处理器),将按顺序执行的几个函数的输出组合到一起,以生成最终的解析值。
内置处理器列表:
class scrapy.loader.processors.Identity
最简单的处理器,不需要任何东西。它返回了没有更改的原始值。这个处理器不接收任何构造参数,也不接受加载器上下文。
示例:
>>> from scrapy.loader.processors import Identity
>>> proc = Identity()
>>> proc(['one', 'two', 'three'])
['one', 'two', 'three']
class scrapy.loader.processors.TakeFirst
从接收到的值中返回第一个不为None或者不为空的值,因此它常被用于只含有单个值的字段的输出处理器。这个处理器不接收任何构造参数,也不接受加载器上下文。
示例:
>>> from scrapy.loader.processors import TakeFirst
>>> proc = TakeFirst()
>>> proc(['', 'one', 'two', 'three'])
'one'
class scrapy.loader.processors.Join(separator=’ ')
返回与构造函数中给定的separator组合后的值,separator默认为’ '。该处理器不接受加载器上下文。
当使用默认separator时,这个处理器相当于函数:’ '.join
示例:
>>> 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来改变。
示例:
>>> from scrapy.loader.processors import Compose
>>> proc = Compose(lambda v: v[0], str.upper)
>>> proc(['hello', 'world'])
'HELLO'
每个函数能可选地接受一个loader_context参数。对于这种情况,处理器将通过这个参数传递当前活动的加载器上下文。
在构造函数中传递的关键字参数将被用作传递给每个函数调用的默认加载器上下文。然而,传递给函数的最终加载器上下文的值将覆盖当前活动的加载器上下文,当前活动的加载器上下文可通过ItemLoader.context属性访问。
class scrapy.loader.processors.MapCompose(*functions, **default_loader_context)
由给定函数组成的处理器,与Compose处理器类似。不同之处在于内部结果在函数之间传递的方式,如下所示:
这个处理器的输入值将被迭代,并且第一个函数将被应用到每个元素。第一个函数的调用结果(每个元素的调用结果)将被串联着构建成一个新的迭代器,这个迭代器随后将被用于第二个函数,依次类推,直到最后一个函数应用于到目前为止收集的值列表中的每个值为止。最后一个函数的输出值将被串联起来,作为这个处理器的最终输出。
每个单独的函数可以返回一个值或者一个值列表,该值或者值列表将与应用于其他输入值的同一函数返回的值列表平铺到一起(有点绕口,就是对于functions的某一个函数,每个元素都通过这个函数进行处理并返回处理后的值,这个值可能是单个值可能是列表,然后这些值或者列表全部平铺到一起)。这些函数也可能返回None,在这种情况下,该函数的输出将被忽略,以便对函数链作进一步处理。
这个处理器提供了便利的方式来仅为单个值(而不是迭代器)组合函数。为此,MapCompose处理器一般用作输入处理器,因为数据一般通过选择器(Selector)的extract()函数来提取,并返回一组unicode字符串。
下面示例演示了这个处理器是如何工作的:
>>> 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']
与Compose处理器一样,函数可以接收加载器上下文,构造函数的关键字参数被用作默认上下文值。有关更多信息,请参阅Compose处理器。
class scrapy.loader.processors.SelectJmes(json_path)
使用构造函数提供的json路径来查询值并返回输出。需要安装jmespath(https://github.com/jmespath/jmespath.py)模块来运行。这个处理器同一时间只携带一个输入。
示例:
>>> from scrapy.loader.processors import SelectJmes, Compose, MapCompose
>>> proc = SelectJmes("foo") # 直接用于列表和字典
>>> 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"}')
'bar'
>>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo')))
>>> proc_json_list('[{"foo":"bar"}, {"baz":"taz"}]')
['bar']