上一节,我们学习了使用Spider从页面中提取数据的方法,并且将提取出来的字段保存于字典中。字典使用虽然方便,但也有它的缺陷:
·字段名拼写容易出错且无法检测到这些错误。
·返回的数据类型无法确保一致性。
·不便于将数据传递给其他组件(如传递给用于数据处理的pipeline组件)。
为了解决上述问题,Scrapy定义了Item类专门用于封装数据。
Item对象是一个简单的容器,用于收集抓取到的数据,其提供了类似于字典(dictionary-like)的API,并具有用于声明可用字段的简单语法。
以起点中文网小说热销榜项目qidian_hot为例,在新建项目时,自动生成的items.py文件,就是用于封装数据的。之所以叫items,是因为源文件中可以定义多种Item,其原始代码为:
import scrapy class QidianHotItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass
已知需要爬取的小说的字段有小说名称、作者、类型和形式。在类QidianHotItem中声明这几个字段,代码如下:
import scrapy #保存小说热销榜字段数据 class QidianHotItem(scrapy.Item): # define the fields for your item here like: name = scrapy.Field() #小说名称 author = scrapy.Field() #作者 type = scrapy.Field() #类型 form = scrapy.Field() #形式
是不是很简单?确实,除了字段名不一样之外,其他的没什么不同。下面分析一下代码:
(1)类QidianHotItem继承于Scrapy的Item类。
(2)name、author、type、form为小说的各个字段名。
(3)scrapy.Field()生成一个Field对象,赋值给各自的字段。
(4)Field对象用于指定每个字段的元数据,并且Field对象对接受的数据没有任何限制。因此,在定义属性字段时,无须考虑它的数据类型,使用起来非常方便。
下面修改HotSalesSpider类中的代码,使用QidianHotItem替代Python字典存储数据,实现代码如下:
#-*-coding:utf-8-*- from scrapy import Request from scrapy.spiders import Spider #导入Spider类 from qidian_hot.items import QidianHotItem #导入模块 class HotSalesSpider(Spider): …… # 解析函数 def qidian_parse(self, response): #使用xpath定位到小说内容的div元素,并保存到列表中 list_selector = response.xpath("//div[@class='book-mid-info']") #依次读取每部小说的元素,从中获取小说名称、作者、类型和形式 for one_selector in list_selector: #获取小说名称 name = one_selector.xpath("h4/a/text()").extract_first() #获取作者 author = one_selector.xpath("p[1]/a[1]/text()").extract()[0] #获取类型 type = one_selector.xpath("p[1]/a[2]/text()").extract()[0] #获取形式(连载还是完本) form = one_selector.xpath("p[1]/span/text()").extract()[0] #将爬取到的一部小说保存到item中 item = QidianHotItem() #定义QidianHotItem对象 item["name"] = name #小说名称 item["author"] = author #作者 item["type"] = type #类型 item["form"] = form #形式 #使用yield返回item yield item #获取下一页URL,并生成Request请求提交给引擎 ……
以上代码分析如下:
(1)首先导入qidian_hot.items下的QidianHotItem模块。
(2)生成QidianHotItem的对象item,用于保存一部小说信息。
(3)将从页面中提取到的各个字段赋给item。赋值方法跟Pyton的字典一样,使用key-value的形式。key要与在QidianHotItem中定义的名称一致,否则会报错,value为各个字段值。Item复制了标准的字典API,因此可以按照字典的形式赋值。
有时会有这样的情况,使用Execl打开生成的CSV文件时出现中文乱码。
解决方法:使用记事本打开CSV文件,在“文件”菜单中选择“另存为”命令,“编码格式”选择UTF-8,然后单击“保存”按钮。之后再次使用Excel打开,中文就能正常显示了。
目前为止我们爬取的数据的字段较少,但是当项目很大、提取的字段数以百计时,数据的提取规则也会越来越多,再加上还要对提取到的数据做转换处理,代码就会变得庞大,维护起来十分困难。
为了解决这个问题,Scrapy提供了项目加载器(ItemLoader)这样一个填充容器。通过填充容器,可以配置Item中各个字段的提取规则,并通过函数分析原始数据,最后对Item字段赋值,使用起来非常便捷。
Item和ItemLoader的区别在于:
·Item提供了保存抓取到的数据的容器,需要手动将数据保存于容器中。
·Itemloader提供的是填充容器的机制。
下面使用ItemLoader来改写起点中文网小说热销榜的项目。打开爬虫(Spider)源文件qidian_hot.py。
1.导入ItemLoader类
导入ItemLoader类,代码如下:
from scrapy.loader import ItemLoader #导入ItemLoader类
2.实例化ItemLoader对象
在使用ItemLoader之前,必须先将其实例化。来看一下数据解析函数qidian_parse()的实现代码:
#解析函数 def qidian_parse(self, response): #使用xpath定位到小说内容的div元素,并保存到列表中 list_selector = response.xpath("//div[@class='book-mid-info']") #依次读取每部小说的元素,从中获取小说名称、作者、类型和形式 for one_selector in list_selector: #生成ItemLoader的实例 #参数item接收QidianHotItem实例,selector接收一个选择器 novel = ItemLoader(item=QidianHotItem(),selector=one_selector) #使用XPath选择器获取小说名称 novel.add_xpath("name","h4/a/text()") #使用XPath选择器获取作者 novel.add_xpath("author","p[1]/a[1]/text()") #使用XPath选择器获取类型 novel.add_xpath("type","p[1]/a[2]/text()") #使用CSS选择器获取小说形式(连载还是完本) novel.add_css("form",".author span::text") #将提取好的数据load出来,并使用yield返回 yield novel.load_item()
很明显,使用ItemLoader实现的Spider功能,代码量更少、更加清晰。因为ItemLoader将数据提取与数据封装的功能全实现了。
在实例化ItemLoader时,ItemLoader接收一个Item实例来指定要加载的Item(参数item);指定response或者selector来确定要解析的内容(参数response或selector)。
3.使用ItemLoader填充数据
实例化ItemLoader对象后,就要开始提取数据到ItemLoader中了。ItemLoader提供了3种重要的方法将数据填充进来。
·add_xpath():使用XPath选择器提取数据。
·add_css():使用CSS选择器提取数据。
·add_value():直接传值。
以上3个方法中都有两个参数,第一个参数指定字段名,第二个参数指定对应的提取规则或者传值。
在上面的代码中,小说名称、作者、类型都是通过add_xpath填充到ItemLoader实例对象中,而小说形式是通过add_css填充的。add_value用于直接传值,例如:
novel.add_value("form", "连载") #字段form值设置为字符串"连载"
4.给Item对象赋值
当提取到的数据被填充到ItemLoader后,还需要调用load_item()方法给Item对象赋值。
5.进一步处理数据
下面两个问题一定也困扰着大家:
(1)使用ItemLoader提取的数据,也是保存于列表中的,以前可以通过extract_first()或者extract()获取列表中的数据,但是在ItemLoader中如何实现呢?以下为生成的数据格式:
{'author': ['傅啸尘'], 'form': ['连载'], 'name': ['修炼狂潮'], 'type': ['玄幻']}
(2)很多时候,我们还需要将选择器(XPath或CSS)提取出来的数据做进一步的处理,例如去除空格、提取数字和格式化数据等。这些处理又在哪里实现呢?
解决方法是,使用输入处理器(input_processor)和输出处理器(output_processor)对数据的输入和输出进行解析。
下面来看一个例子,实现将提取出来的小说形式(连载/完结)转换为简写形式(LZ/WJ)。下面在items.py中实现这个功能,实现代码如下:
import scrapy from scrapy.loader.processors import TakeFirst #定义一个转换小说形式的函数 def form_convert(form): if form[0] == "连载": return "LZ" else: return "WJ" #保存小说热销榜字段数据 class QidianHotItem(scrapy.Item): #TakeFirst为Scrapy内置处理器,获取列表中第一个非空数据 name = scrapy.Field(output_processor=TakeFirst()) #小说名称 author = scrapy.Field(output_processor=TakeFirst()) #作者 type = scrapy.Field(output_processor=TakeFirst()) #类型 form = scrapy.Field(input_processor=form_convert, output_processor=TakeFirst()) #形式
首先定义了一个转换小说形式的函数form_convert( )。参数如果为“连载”,则返回LZ;否则返回WJ。
在QidianHotItem类中,scrapy.Field()中设置了两个参数(或其中之一):输入处理器(input_processor)和输出处理器(output_processor)。
output_processor绑定了函数TakeFirst( )。TakeFirst()函数为Scrapy内置的处理器,用于获取集合中第一个非空值。
form为小说形式的字段,scrapy.Field()的参数input_processor绑定了函数form_convert( )。当ItemLoader通过选择器(XPath或CSS)提取某字段数据后,就会将其发送给输入处理器进行处理,然后将处理完的数据发送给输出处理器做最后一次处理。最后调用load_item()函数将数据填充进ItemLoader,并得到填充后的Item对象。如图4-6所示为每个字段的数据处理过程。
图4-6 字段的处理过程
6.运行项目
通过以下命令运行项目:
>scrapy crawl hot -o hot.csv
打开hot.csv文件,在form列,查看所有的“连载”是否已变成LZ,所有的“完结”是否已变成WJ。
ItemLoader提供了一种灵活、高效和简单的机制,但是,如果爬取的字段不算太多,也没有必要使用它。