Scrapy进阶之ItemLoader

2021-07-16

ItemLoader是Scrapy框架中非常有用,非常灵活的设计之一。也是容易被大家忽略的爬虫好帮手。

编写爬虫有很大一部分工作是处理怎么从HTML页面中抽取可靠的数据供后续数据存储和处理用,而这项工作看似简单,但是想要效率的可靠的抽取数据牵涉到大量的细节处理。经常需要抽取数据然后进行各种格式和各种情况的处理。为了解决这类普遍的uuqiu,Scrapy提供了Item和ItemLoader配合使用方便处理网页数据提取。在Scrapy框架内,Item可以理解为是抓取数据的容器,而ItenLoader则提供了一套完整而灵活的机制来操作Item的数据容器。

接下来我们用Item和ItemLoader来迭代我们之前文章中构建的豆瓣电影的简单爬虫,同时给予相应的解释和探讨。

使用Item和ItemLoader

之前的爬虫没有使用Item机制,直接在解析函数中用Dict数据结构保存抓取到的数据,返回给Scrapy做后续处理。首先我们需要定义Item。我们编辑douban/items.py文件,定义Item:

class DoubanItem(scrapy.Item):
    name = scrapy.Field()
    year = scrapy.Field()
    director = scrapy.Field()
    genre = scrapy.Field()
    rating = scrapy.Field()
    rating_count = scrapy.Field()

定义Item不需要特别指定数据类型,把我们需要保存的数据定义成scrapy.Feild()即可。然后我们改写我们的解析函数,现在我们暂时不用ItemLoader:

    def parse_movie(self, response):
        m = DoubanItem()
        m["name"] = response.css('div#content > h1 > span:first-child::text').get()
        m["year"] = response.css('div#content > h1 > span.year::text').get()
        m["director"] = response.css('div#info > span > span.attrs > a::text').get()
        m["genre"] = response.css('span[property="v:genre"]::text').get()
        m["rating"] = response.css('div.rating_self > strong.rating_num::text').get()
        m["rating_count"] = response.css('a.rating_people > span::text').get()
        yield m

我们仅仅是创建DoubanItem的一个实例,这个实例的行为跟Dict非常类似,现在我们运行爬虫,跟我们之间的行为是一样的。那么问题来了,既然item就可以完成我们需要的工作,我们为什么要使用ItemLoader呢?

我们观察我们抓取下来的数据,可以看到我们抽取的year数据不是特别理想,我们现在抓取的数据带了括号。我们需要做一些处理来去掉括号,让数据更加可靠符合要求。我们可以这样做:


    m["year"] = response.css('div#content > h1 > span.year::text').get().split('(')[1].split(')').[0]

额,并不优雅。而且这里会有一个出错的可能,如果我们抓取的页面并没有年份信息,也就收获说,从css中抽取的数据是空值,那么接下来的split就会报错。我们不得不对空值进行判断后再进行字符串的处理。非常不优雅!

从上面一个最简单的数据字段处理就能感觉到,因为网页内容的不确定性,我们需要经常性的对数据的各种情况做处理。这种恶心的代码会散落在各个数据抽取的函数中。很难维护。

ItemLoader

ItemLoader便是应对这种场景的解决方案,它提供了一套简单,可靠且灵活的架构来处理这个常见的过程。我们先引入ItemLoader来改造我们代码,然后我们再详细解释:

    def parse_movie(self, response):
        l = ItemLoader(item=DoubanItem(), response=response)
        l.add_css("name", 'div#content > h1 > span:first-child::text')
        l.add_css("year", 'div#content > h1 > span.year::text')
        l.add_css("director", 'div#info > span > span.attrs > a::text')
        l.add_css("genre", 'span[property="v:genre"]::text')
        l.add_css("rating", 'div.rating_self > strong.rating_num::text')
        l.add_css("rating_count", 'a.rating_people > span::text')
        m = l.load_item()
        yield m

这次我们先创建ItemLoader,并且指定item就是我们之前定义的DoubanItem,并且指定response为我们需要抽取数据的默认来源。然后我们用ItemLoader提供的add_css方法替换之前直接对response的数据抽取操作。除了add_css方法以外,ItemLoader提供了两种种主要的方式来赋值数据:

  1. add_xpath: 利用XPath定位节点抽取数据。
  2. add_value: 不抽取数据,直接赋值给Item定义的属性。

运行爬虫,能工作,而且抓到的数据内容变多了,所有的字段都变成了数组形式。这个我们先放一边,稍后来处理。我们先解释一下ItemLoader的原理。

ItemLoader的原理

在抽取数据的过程,一般会是这样的处理过程:

  1. 通过CSS选择器或者XPath语法定位到要抽取数据的节点,
  2. 对抽取出来的数据进行判断,主要是判断是否为空值或者是否是有效数据
  3. 对数据进行转换操作,让数据符合我们期望的数据类型或格式

ItemLoader基本上是贴合着这样的步骤设计对应的结构,如下图所示:

结构图

从结构图可以看出来,ItemLoader提供的赋值函数函数主要为数据字段提供数据,然后通过load_item()函数形成数据Item。这里面最关键的是Input Processor和Output Processor。数据要进入数据临时存储区,必须经过Input Processor处理,处理的的数据会以数组的形式跟数据字段建立关联。当要提取Item的时候,数据数组会经过Output Processor的处理,最终形成Item数据对应的数值。

在Input和Output处理阶段,可以灵活的应用和组合我们想要的操作。Scrapy内置提供了一些基本的数据操作的[处理器],可以直接拿来用。当然我们也可以自己定义我们要的处理器函数。

现在我们来改造年份的数据获取,我们利用add_css可以直接使用正则来处理加入的数据,相当于设置一个正则的处理器:

    l.add_css("year", 'div#content > h1 > span.year::text', re='\((.*?)\)')

进过这个处理,我们可以观察到,现在year数据已经去掉括号,应该是这样的数据:'year': ['2008']。再进一步,我们希望年份数据是Int类型的。那么我们定义自己的处理函数,然后应哟到Input Processor去:

def to_int(value):
    try:
        return int(value)
    except:
        return value

def parse_movie(self, response):
    # 省略其他代码
    l.add_css("year", 'div#content > h1 > span.year::text', MapCompose(to_int), re='\((.*?)\)')

这时候,我们会得到类似这样的数据: 'year': [2008]。经过处理,已经是我们想要的数据类型和格式了。但是因为默认的Output Processor是什么都不处理。所以返回的还是数组类型。我们需要取出数组中的第一个作为我们最终的数据,我们可以在Item的定义中指定:

from itemloaders.processors import TakeFirst

class DoubanItem(scrapy.Item):
    # 省略其他代码
    year = scrapy.Field(output_processor=TakeFirst())

Bingo! 我们得到了我们最终想要的数据:'year': 2008

理解了ItemLoader的结构,我们就可以做几乎所有想要做的操作。ItemLoader的优势在于:

  1. 处理器可以复用,而不是散落在各个爬虫中。
  2. 自动的零值处理。
  3. 可以动态的添加处理器,甚至可以把处理器存储到数据库中。

好了,希望这篇文章能展现ItemLoader的一些好处,官方文档写得非常详细,可以参考:

Item ItemLoader