> 文档中心 > Scrapy源码学习-ItemLoader

Scrapy源码学习-ItemLoader


item

在scrapy项目结构中,有一个items.py的文件,在里面是专门存放和定义抓取数据字段的。这当然不是强制性要求的。但这种数据字段的定义能够更好地约束未来抓取字段,并且开发者可以一目了然的知道抓了哪些数据。
比如我们需要抓取影视网站,可以这样定义:

class MovieItem(scrapy.Item):    title = scrapy.Field()    url = scrapy.Field()    cover = scrapy.Field()    actors = scrapy.Field()    column = scrapy.Field()    ……

继承的父类scrapy.Item有一个metaclass元类,元类的讲解会在以后的文章中展开[挖坑]。

class Item(DictItem, metaclass=ItemMeta):    ……

它所做的,就是将继承scrapy.Item的类中所有属性类型为Field的加载到fields字典中。
摘选scrapy/item.py

class ItemMeta(_BaseItemMeta):    def __new__(mcs, class_name, bases, attrs): …… for n in dir(_class):     v = getattr(_class, n)     # 当属性类型为Field时候,加入fields字段     if isinstance(v, Field):  fields[n] = v     elif n in attrs:  new_attrs[n] = attrs[n] new_attrs['fields'] = fields …… return super().__new__(mcs, class_name, bases, new_attrs)

一旦爬虫数据字段出现非MovieItem类定义的属性时,比如由于拼写失误,导致title字段写成了tilte,则会产生KeyError: 'tilte'的报错。
相较于Python内置的字典,更加提升了程序的健壮性。

ItemLoader

讲完了item的作用和应用场景,接下来就谈谈ItemLoader。这是一个即使做了一些scrapy项目也不一定使用的组件。在scrapy数据流转的过程中,最终数据会到达pipeline,自定义的pipeline管道接收到item数据。但是如果item数据是杂乱的,我们可能就需要在pipeline中做一些数据后处理,这活显然不应该是pipeline该干的。(当然你也可以不管不顾一股脑塞进持久化存储中,如MongoDB)。但是最终你该清洗还得清洗,如果这活不太麻烦,那就可以交给ItemLoader。未来项目的维护也变得清晰明朗。

换句话说,item提供了抓取数据的容器,而itemloader提供了填充该容器的机制。

在项目中,你可以这样去定义:

class MovieItemLoader(ItemLoader):     ……

在spider文件中这样去使用:

    def parse(self, response, kwargs): topics = response.xpath("//div[@class='panel-max']/ul/li") for topic in topics:     item = MovieItemLoader(item=MovieItem(), response=response, selector=topic)     item.add_xpath('title', "./div[@class='video-info']/h5/a/@title")     item.add_xpath('url', "./div[@class='video-info']/h5/a/@href")     item.add_xpath('actors', "./div[@class='video-info']/p/text()")     item.add_xpath('cover', './a/img/@src')     item.add_value('column', self.column)     yield item.load_item()

ItemLoader提供了多种添加数据的方式:

  • add_xpath:通过xpath语法添加字段值
  • add_css:通过css语法添加字段值
  • add_value:直接添加字段值

难道做这些仅仅是为了字段属性与值的绑定吗?
那item[‘title’] = response.xpath("./div[@class=‘video-info’]/h5/a/@title")不也可以?
这时候就需要看看ItemLoader提供添加数据的API到底有什么不同了。

    def add_xpath(self, field_name, xpath, *processors, kw): values = self._get_xpathvalues(xpath, kw) self.add_value(field_name, values, *processors, kw)    def add_css(self, field_name, xpath, *processors, kw): values = self._get_cssvalues(css, kw) self.add_value(field_name, values, *processors, kw)

可以发现其实这三个API最终都是去操作add_value,只是它们先各自使用xpath和css规则去拿到value值。而add_value又去调用了_add_value

    def _add_value(self, field_name, value): value = arg_to_iter(value) processed_value = self._process_input_value(field_name, value) if processed_value:     self._values.setdefault(field_name, [])     self._values[field_name] += arg_to_iter(processed_value)

它就做了件有意思的事情,它会先去查一下咱们定义的MovieItemLoader类中有没有带字段名_in(如title_in)的属性,如果有,那就把属性的值交给该process来执行,要是没有,就看看这个属性的Field中有没有input_processor属性,如果有,那就一样交给process来执行。还没有的话就交给默认的default_input_processor。这里可以看出:
优先级:字段_in > input_processor属性 > default_input_processor

    def _process_input_value(self, field_name, value): proc = self.get_input_processor(field_name) # 把value值丢给指定的process来处理,返回给_add_value调用方 return proc(value)    def get_input_processor(self, field_name): # 优先找字段名_in的process proc = getattr(self, '%s_in' % field_name, None) if not proc:     # 没找到,再找属性为input_processor的process     proc = self._get_item_field_attr(  field_name,  'input_processor',  self.default_input_processor     ) return unbound_method(proc)

有了如上的经验,我们就可以丰富一下MovieItemLoader类

class AddFlag:    def __call__(self, values): return ['米乐推荐-' + value for value in values]class MovieItemLoader(ItemLoader):    title_in = AddFlag()

运行爬虫后得到这样的数据:

{'actors': ['\r\n'     '    '     '肖战,吴宣仪,辰亦儒,邱心志,钟镇涛,朱珠,高泰宇,黄灿灿,刘美彤,刘润南,丁笑滢,敖子逸\r\n'     '   '], 'column': ['国产剧'], 'cover': ['/static/picture/ss.png'], 'title': ['米乐推荐-斗罗大陆'], 'url': ['/detail/?74625.html']}

通过_in当数据进来的时候处理,自然就有_out当数据输出的时候。
而_out并不是在add_value的时候触发的,而是当我们load_item的时候。

    def load_item(self): adapter = ItemAdapter(self.item) for field_name in tuple(self._values):     value = self.get_output_value(field_name)     if value is not None:  adapter[field_name] = value return adapter.item

get_output_valueget_input_value一样,只是取属性的时候变成了字段_out
同样的,再丰富一下MovieItemLoader,上文输出的数据都是列表,我们只取列表第一个值。

class AddFlag:    def __call__(self, values): return ['米乐推荐-' + value for value in values]class First:    def __call__(self, values): for value in values:     if value is not None and value != '':  return valueclass MovieItemLoader(ItemLoader):    title_in = AddFlag()    title_out = First()
{'actors': ['\r\n'     '    '     '肖战,吴宣仪,辰亦儒,邱心志,钟镇涛,朱珠,高泰宇,黄灿灿,刘美彤,刘润南,丁笑滢,敖子逸\r\n'     '   '], 'column': ['国产剧'], 'cover': ['/static/picture/ss.png'], 'title': '米乐推荐-斗罗大陆', 'url': ['/detail/?74625.html']}

还有,actors数据是个数组,而我们要的数据在第三个,我们可以把它合并为一个字符串,并且删除首尾空。

class Join:    def __call__(self, values): return ''.join(values).strip()class MovieItemLoader(ItemLoader):    ……    actors_out = Join()
{'actors': '肖战,吴宣仪,辰亦儒,邱心志,钟镇涛,朱珠,高泰宇,黄灿灿,刘美彤,刘润南,丁笑滢,敖子逸', 'column': ['国产剧'], 'cover': ['/static/picture/ss.png'], 'title': '米乐推荐-斗罗大陆', 'url': ['/detail/?74625.html']}

当然了,大部分时候我们不需要傻傻的自己写处理器。在ItemLoader中内置了多个处理器在itemloaders.processors文件中供你选择。

总结

Scrapy源码学习-ItemLoader

简易ItemLoader

Scrapy源码学习-ItemLoader