Scrapy框架(三)——Item

上一篇博客的例子中,提到了parse方法一般会返回一个request对象或item对象。那么这篇博客就记录下item,及处理item的管道pipelines。

Scrapy框架(三)——Item

引入

在抓取数据的过程中,主要要做的事就是从杂乱的数据中提取出结构化的数据。ScrapySpider可以把数据提取为一个Python中的字典,虽然字典使用起来非常方便,对我们来说也很熟悉,但是字典有一个缺点:缺少固定结构。在一个拥有许多爬虫的大项目中,字典非常容易造成字段名称上的语法错误,或者是返回不一致的数据。

在上一篇博客中,我们可以发现,保存在job.json文件的每条数据没有一致对应。如下:


{"job_label": "技术类", "num": "1", "time": "2019-03-26", "addr": "深圳", "job_name": "18796-数据分析师(深圳)"}
{"addr": "深圳", "num": "1", "job_name": "32032-互娱数据经营分析师(深圳)", "time": "2019-03-26", "job_label": "产品/项目类"}

所以Scrapy中,定义了一个专门的通用数据结构:Item。这个Item对象提供了跟字典相似的API,并且有一个非常方便的语法来声明可用的字段。

Item

  • 定义item

item对象结构的定义在项目录下的items.py文件中定义。以类的方式定义item对象的各个字段,这个类继承与scrapy.Item。如下所示:

class MyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    job_name = scrapy.Field()
    job_label = scrapy.Field()
    job_time = scrapy.Field(serializer=str)
  
  • 使用item

在spiders文件下,我们使用的parse方法将解析的数据存放于item对象中。首先实例化item,然后以字典的方式进行传值。

item = MyItem()
item['job_name'] = '语文老师'
item['job_label'] = '教师'
  • Field对象

可以看到,在声明Item时,声明字段使用的是Field对象。这个Field对象其实完全继承自Python的字典,并且没有做任何改动,所以在使用Field声明字段时,可以传入数据作为这个字段的元数据(metadata),上方的serializer=str其实就是一个指定序列化函数的元数据。

字段的元数据与字段的值之间没有必然的联系。如果我们直接查看Item对象,那么获取的是字段的值;

In [3]: item
Out[3]: {'job_name': '语文老师', 'job_label':'教师'}

如果使用.fields,获取的就是字段的元数据了:

In [3]: item
Out[3]: {'job_time':{}'job_name': {'serializer':'str'}, 'job_label':{}}
  • 注意点

如果获取没有声名的字段或给其赋值,将会报错。但可以通过get方法给没有声名的字段设置一个默认值。

实例化item时,可以直接传递一个字典作为参数,但要注意传递字典的key值是已声明的,否则会报错。

定义的item,还可以给其他item所继承。

Item Pipeline

Spider中返回一个Item后,这个Item将会被发送给Item Pipeline,每个Item Pipeline都是一个Python类,实现了几个简单的方法。其主要有以下几种作用:

  1. 清洗数据
  2. 验证抓取下来的数据(检查是否含有某些字段)
  3. 检查去重
  4. 存储数据

几个简单的方法:

  • process_item(self, item, spider)

每一个Item Pipeline都会调用这个方法,用来处理Item,返回值为item或dict。

这个方法还可以抛出一个DropItem异常,这样将会不再继续调用接下来的Item Pipeline

参数item(Item对象或者Dict) 是parse方法传来的。

参数spider(Spider对象) - 抓取这个ItemSpider

  • open_spider(self, spider)

​​​​​​​这个方法将会在Spider打开时调用。

  • close_spider(self, spider)

​​​​​​​这个方法将会在Spider关闭时调用。

启用pipeline:在setting文件中配置,demo是项目名。

ITEM_PIPELINES = {
   'demo.pipelines.TanzhouPipeline': 300,
}

小例子

下面我们增加item、pipelines功能来实现腾讯python招聘信息的抓取。

class Job2_Tenxun(scrapy.Spider):
    name = 'job2'
    start_urls = ['https://hr.tencent.com/position.php?keywords=python',]
    def parse(self, response):
        item = MyItem()
        res = response.xpath("//tr[@class='even']|//tr[@class='odd']")
        for tr in res:
            item['job_name'] = tr.xpath("./td[1]/a/text()").extract_first()
            item['job_label'] = tr.xpath("./td[2]/text()").extract_first()
            item['num'] = tr.xpath("./td[3]/text()").extract_first()
            item['addr'] = tr.xpath("./td[4]/text()").extract_first()
            item['time'] = tr.xpath("./td[5]/text()").extract_first()
            yield  item
        next_url = response.xpath("//a[@id='next']/@href").extract_first()
        yield scrapy.Request('https://hr.tencent.com/%s'%next_url)
class MyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    job_name = scrapy.Field()
    job_label = scrapy.Field()
    num = scrapy.Field()
    addr = scrapy.Field()
    time = scrapy.Field()
import json
class MyPipeline(object):
    def open_spider(self,spider):
        self.f = open('job2.json','w',encoding='utf-8')
    def process_item(self, item, spider):
        self.f.write(json.dumps(dict(item),ensure_ascii=False))
        self.f.write('\n')
        return item
    def close_spider(self,spider):
        self.f.close()

这里要注意的是,item对象不能直接转成json数据,故需要先转成dict。