第4章
使用Item封装数据

在第3章中,我们学习了从页面中提取数据的方法,本章来学习如何封装爬取到的数据。以爬取某图书网站的书籍信息为例,对于网站中的每一本书可以提取出书名、价格、作者、出版社、出版时间等多个信息字段。应该用怎样的数据结构来维护这些零散的信息字段呢?最容易想到是使用Python字典(dict)。

回顾第1章example项目中BooksSpider的代码:

     class BooksSpider(scrapy.Spider):
        ...
        def parse(self, response):
            for sel in response.css('article.product_pod'):
                name = sel.xpath('./h3/a/@title').extract_first()
                price = sel.css('p.price_color::text').extract_first()
                yield {
                   'name': name,
                   'price': price,
                }
        ...

在该案例中,我们便使用了Python字典存储一本书的信息,但字典可能有以下缺点:

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

(1)无法一目了然地了解数据中包含哪些字段,影响代码可读性。

(2)缺乏对字段名字的检测,容易因程序员的笔误而出错。

(3)不便于携带元数据(传递给其他组件的信息)。

为解决上述问题,在Scrapy中可以使用自定义的Item类封装爬取到的数据。

4.1 Item和Field

Scrapy提供了以下两个类,用户可以使用它们自定义数据类(如书籍信息),封装爬取到的数据:

● Item基类

自定义数据类(如BookItem)的基类。

● Field类

用来描述自定义数据类包含哪些字段(如name、price等)。

自定义一个数据类,只需继承Item,并创建一系列Field对象的类属性(类似于在Django中自定义Model)即可。以定义书籍信息BookItem为例,它包含两个字段,分别为书的名字name和书的价格price,代码如下:

     >>> from scrapy import Item, Field
     >>> class BookItem(Item):
     ...    name = Field()
     ...    price = Field()

Item支持字典接口,因此BookItem在使用上和Python字典类似,可按以下方式创建BookItem对象:

     >>> book1 = BookItem(name='Needful Things', price=45.0)
     >>> book1
     {'name': 'Needful Things', 'price': 45.0}
     >>> book2 = BookItem()
     >>> book2
     {}
     >>> book2['name'] = 'Life of Pi'
     >>> book2['price'] = 32.5
     {'name': 'Life of Pi', 'price': 32.5}

对字段进行赋值时,BookItem内部会对字段名进行检测,如果赋值一个没有定义的字段,就会抛出异常(防止因用户粗心而导致错误):

     >>> book = BookItem()
     >>> book['name'] = 'Memoirs of a Geisha'
     >>> book['prize'] = 43.0 # 粗心, 把price 拼写成了prize.
     Traceback (most recent call last):
        ...
     KeyError: 'BookItem does not support field: prize'

访问BookItem对象中的字段与访问字典类似,示例如下:

     >>> book = BookItem(name='Needful Things', price=45.0)
     >>> book['name']
     'Needful Things'
     >>> book.get('price', 60.0)
     45.0
     >>> list(book.items())
     [('price', 45.0), ('name', 'Needful Things')]

接下来,我们改写第1章example项目中的代码,使用Item和Field定义BookItem类,用其封装爬取到的书籍信息项目目录下的items.py文件供用户实现各种自定义的数据类,在items.py中实现BookItem,代码如下:

     from scrapy import Item, Field


     class BookItem(Item):
        name = Field()
        price = Field()

修改之前的BooksSpider,使用BookItem替代Python字典,代码如下:

     from ..items import BookItem


     class BooksSpider(scrapy.Spider):
        ...
        def parse(self, response):
           for sel in response.css('article.product_pod'):
              book = BookItem()
          book['name'] = sel.xpath('./h3/a/@title').extract_first()
          book['price'] = sel.css('p.price_color::text').extract_first()
          yield book
     ...

4.2 拓展Item子类

有些时候,我们可能要根据需求对已有的自定义数据类(Item子类)进行拓展。例如,example项目中又添加了一个新的Spider,它负责在另外的图书网站爬取国外书籍(中文翻译版)的信息,此类书籍的信息比之前多了一个译者字段,此时可以继承BookItem定义一个ForeignBookItem类,在其中添加一个译者字段,代码如下:

     >>> class ForeignBookItem(BookItem):
     ...   translator = Field()
     ...
     >>> book = ForeignBookItem()
     >>> book['name'] = '巴黎圣母院'
     >>> book['price'] = 20.0
     >>> book['translator'] = '陈敬容'

4.3 Field元数据

在第2章中曾讲到,一项数据由Spider提交给Scrapy引擎后,可能会被递送给其他组件(Item Pipeline、Exporter)处理。假设想传递额外信息给处理数据的某个组件(例如,告诉该组件应以怎样的方式处理数据),此时可以使用Field的元数据。请看下面的例子:

     class ExampleItem(Item):
        x = Field(a='hello', b=[1, 2, 3])       # x 有两个元数据,a是个字符串,b是个列表
        y = Field(a=lambda x: x ** 2)           # y 有一个元数据,a是个函数

访问一个ExampleItem对象的fields属性,将得到一个包含所有Field对象的字典:

     >>> e = ExampleItem(x=100, y=200)
     >>> e.fields
     {'x': {'a': 'hello', 'b': [1, 2, 3]},
      'y': {'a': <function __main__.ExampleItem.<lambda>>}}
     >>> type(e.fields['x'])
     scrapy.item.Field
     >>> type(e.fields['y'])
     scrapy.item.Field

实际上,Field是Python字典的子类,可以通过键获取Field对象中的元数据:

     >>> issubclass(Field, dict)
     True
     >>> field_x = e.fields['x']      # 注意,不要混淆e.fields['x']和e['x']
     >>> field_x
     {'a': 'hello', 'b': [1, 2, 3]}
     >>> field_x['a']
     'hello'
     >>> field_y = e.fields['y']
     >>> field_y
     {'a': <function __main__.ExampleItem.<lambda>>}
     >>> field_y.get('a', lambda x: x)
     <function __main__.ExampleItem.<lambda>>

接下来,看一个应用Field元数据的实际例子。假设我们要把爬取到的书籍信息写入csv文件,那每一项数据最终由Scrapy提供的CsvItemExporter写入文件(数据导出在第7章详细讲解),在爬取过程中提取到的信息并不总是一个字符串,有时可能是一个字符串列表,例如:

     >>> book['authors'] = ['李雷', '韩梅梅', '吉姆']

但在写入csv文件时,需要将列表内所有字符串串行化成一个字符串,串行化的方式有很多种,例如:

     1. '李雷|韩梅梅|吉姆'                # '|'.join(book['authors'])
     2. '李雷;韩梅梅;吉姆'                # ';'.join(book['authors'])
     3. "['李雷', '韩梅梅', '吉姆']" # str(book['authors'])

我们可以通过authors字段的元数据告诉CsvItemExporter如何对authors字段串行化:

     class BookItem(Item):
        ...
        authors = Field(serializer=lambda x: '|'.join(x))
        ...

其中,元数据的键serializer是CsvItemExporter规定好的,它会用该键获取元数据,即一个串行化函数对象,并使用这个串行化函数将authors字段串行化成一个字符串。以下是Scrapy源码中的相关实现:

     # exports.py


     class BaseItemExporter(object):
        ...


        def _get_serialized_fields(self, item, default_value=None, include_empty=None):
            ...
            for field_name in field_iter:
                if field_name in item:
                   field = {} if isinstance(item, dict) else item.fields[field_name]
                   value = self.serialize_field(field, field_name, item[field_name])
                else:
                   value = default_value
                yield field_name, value
        ...


     class CsvItemExporter(BaseItemExporter):


        ...


        def export_item(self, item):
            ...
            fields = self._get_serialized_fields(item, default_value='',
                                             include_empty=True)
            values = list(self._build_row(x for _, x in fields))
            self.csv_writer.writerow(values)


        ...


        def serialize_field(self, field, name, value):
            serializer = field.get('serializer', self._join_if_needed)
            return serializer(value)
        ...

解释上述代码如下:

● 爬取到的每一项数据由export_item方法导出到文件,写入文件之前,先调用_get_serialized_fields方法(在基类中实现)获得数据中每个字段串行化的结果。

● 在_get_serialized_fields方法中调用serialize_field方法,获取其中一个字段串行化的结果。

● 在serialize_field方法中获取字段的元数据serializer,得到串行化函数(如果不存在,就使用默认的_join_if_needed函数),最终调用该函数对字段串行化,并将结果返回。

在实际应用中,我们可以仿照上面的例子灵活使用Field元数据。

4.4 本章小结

本章介绍了在Scrapy中如何封装爬取到的数据,先了解了Item基类以及用来定义字段的Field类,然后展示了一个使用它们封装数据的例子。最后,还介绍了使用Field元数据给其他组件传递信息的方法。