6.scrapy数据持久化

scrapy数据持久化

爬取到的数据想要进行保存的话, 首先要对数据进行格式化话,这样数据格式统一才方便进行保存

1. 数据格式化

1.1 item.py

在我们创建的爬虫项目中item.py这个文件就是负责进行格式化数据的

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#=
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


# 这里我们可以创建多个class,每个对象中的字段就是我们要格式化的数据
# 当然一个对象也可以有多个字段,但只能是scrapy.Field()

# 特殊的字典结构 可以在scrapy中传递数据
class Spider1Item(scrapy.Item):
    # Field 字段
  	# 就是类似于产生一个类似字典格式的数据 拥有字典的一些属性
  	# 字段默认为空
  	# 我们可以通过实例化 像着键赋值 但是如果没有写这个键 就不能赋值 但是字典可以
    name = scrapy.Field()
    age = scrapy.Field()
    gender = scrapy.Field()

在爬虫文件中的应用

# -*- coding: utf-8 -*-
import scrapy
from ..items import Spider1Item # 导入类


class BaiduSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['douban.com']
    start_urls = ['https://movie.douban.com/top250?start=0&filter=']

    def parse(self, response):
        # 实例化一个item对象
        item = Spider1Item()
        # 将提取到的数据封装
        item['title'] = response.css('div.hd>a>span::text')
        # 当yield的是一个item对象,就会触发pipeline中的process_item方法
        yield item

1.2 多个Item处理

# item.py 

class SpiderItem1(scrapy.Item):
	pass

class SpiderItem2(scrapy.Item):
	pass

class SpiderItem3(scrapy.Item):
	pass


# pipelines.py
# 注意导入路径从当前项目开始
from spider1.spider1.items import Spider1Item, Spider2Item, Spider3Item,

def process_item(self, item, spider):
  # 使用isinstance然后通过if判断方式进行items类别筛选

  if isinstance(item,  Spider1Item):
    pass
  elif isinstance(item,  Spider2Item):
    pass
  elif isinstance(item,  Spider3Item):
    pass
  return item

2. 数据持久化

在项目中pipeline.py文件就是专门用来对接收到的item对象进行数据持久化的,在本文件中,一个Pipeline类就对

应一种数据持久化的方案,比如你可以将数据保存成csv文件,你可以写一个pipeline类,你要保存到mysql数据

库,你也可以写一个类,一般比较重要有价值的数据都会进行多种形式的持久化。因为这些类中的方法都是一样

的,只不过方法的功能不同,这里我们就举一个pipeline类,进行剖析。

2.1 pipeline方法解读

# 在pipeline类中,可以写这5个方法,可以少,但不可以多。有就执行,没有的话就不执行
# 通常情况下默认只有一个process_item(self, item, spider):方法

class Spider1Pipeline:
    def __init__(self):
        print('实例化一个对象')
        pass

    def process_item(self, item, spider):
        # 当爬虫文件中回调函数每yield一个item对象时会自动调用一次这个方法
        # 对item对象中的数据进行持久化操作
        d = dict(item)
        print(d)
        return item

    @classmethod
    def from_crawler(cls, crawler):
        # 先会检测是否存在这个方法,存在就调用,不存在的话就会直接实例化对象
        print('正在执行from_crawler方法')
        return cls()

    def open_spider(self, spider):
        # 爬虫开启时会自动调用的方法
        print('爬虫{}开启'.format(spider.name))

    def close_spider(self, spider):
        # 爬虫结束时会自动调用的方法
        print('爬虫{}关闭'.format(spider.name))

2.1.1 from_crawler(cls, crawler)

当爬虫开始的时候,先会检测Spider1Pipeline类中是否有 from_crawler方法,通过hasattr(反射)检测,如果

有那么就会Spider1Pipeline.Spider1Pipeline()这个方法,这个方法会返回当前类的一个实例化对象,那么

crawler有什么用呢?他可以通过crawler.settings.get ( "名字" ) 取到配置文件中的内容, 比如中配置文件中取到连

接的数据库的配置信息, 然后封装到当前对象中, 在 其他方法中应用。注意这里的settings是一个对象,

是settings.py这个文件实例化形成的对象,在爬虫启动时,存在放在当前内存中。

# settings.py 
# HOST ='127.0.0.1'
# PORT = 3306


@classmethod
def from_crawler(cls, crawler):
    # 先会检测是否存在这个方法,存在就调用,不存在的话就会直接实例化对象
    print('正在执行from_crawler方法')
    
    # cls().host = crawler.settings.get ( "HOST" )
    # cls().port = crawler.settings.get ( "PORT" )
    
    return cls()

如果在当前类中没有from_crawler(cls, crawler)方法,那么就会直接实例化对象。

2.1.2 _ _init _ _ ( self )

这个方法就没什么特别的啦,就是当实例化对象时会自动调用该方法,通常用来接收从配置文件中的获得的数据,

然后进行封装。

    def __init__(self):
        print('实例化一个对象')
        pass

2.1.3 open_spider(self,spider)

当pipeline类实例化一个对象之后就会自动调用该方法,而且这个方法是在爬虫开启之前执行的方法,如果你想针

对你的爬虫做一下开始爬取之前的自定义的话,可以写在这个方法中,比如连接数据库,或者打开文件,因为在整

个爬虫的过程中这个方法只会执行一次,但是千万不要忘记在爬虫执行完毕后关闭文件或者断开数据库的连接。

如果你在process_item这个方法中连接数据库或者打开文件的话会造成重复连接数据库或打开文件。

    def open_spider(self, spider):
        print('爬虫{}开启'.format(spider.name))
        # 打开文件
        self.f = open('douban.csv', 'w', encoding='utf-8')

2.1.4 process_item(self, item, spider)

  • item: 爬虫文件中 yield 的对象
  • spider: 当前运行的爬虫
    def process_item(self, item, spider):
        d = dict(item)
        # 将数据写入文件
        self.f.write(','.join(d.values()) + '\n')
        print('{}正在执行中....'.format(spider.name))
        return item

为这么要 return item ?

因为如果 pipelines.py 中有多个类对爬取的数据进行持久化, 那么就回涉及到 item 对象的传递, 这里的return item

就是将item 对象传递给下一个 pipeline类中的 process_item 方法, 进行数据的持久化操作。

如果不写 return item 或者 return 空 ,传递不会被打断,只不过下一级的 process_item 接收的 item 对象是

None , 如果你想终止传递的话 需要在 process_item 抛出一个异常。

from scrapy.exceptions import DropItem

raise DropItem()

2.1.5 close_spider(self,spider)

当爬虫运行结束的时候,会自动调用该函数一般用来关闭文件或者断开与数据库的连接。

	def close_spider(self, spider):
        # 关闭文件
        self.f.close()
        print('爬虫{}关闭'.format(spider.name))

2.2 数据保存的格式

2.2.1 保存到文件

class Spider1Pipeline:
    def __init__(self):
        print('实例化一个对象')
       

    @classmethod
    def from_crawler(cls, crawler):
        print('正在执行from_crawler方法')
        obj = cls()
        return obj

    def open_spider(self, spider):
        print('爬虫{}开启'.format(spider.name))
        # 爬虫开启前创建文件
        self.f = open('douban.csv', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        d = dict(item)
        # 将数据写入文件
        self.f.write(','.join(d.values()) + '\n')
        print('{}正在执行中....'.format(spider.name))
        return item

    def close_spider(self, spider):
        print('爬虫{}关闭'.format(spider.name))
        # 关闭文件
        self.f.close()

2.2.2 保存到数据库

class Spider2Pipeline:
    def __init__(self, db, cur):
        print('实例化一个对象')
        # 封装数据库, 游标
        self.db = db
        self.cur = cur

    @classmethod
    def from_crawler(cls, crawler):
        print('正在执行from_crawler方法')
        # 从配置文件中获取 HOST, USER, PASSWORD, DATABASE
        host = crawler.settings.get('HOST')
        user = crawler.settings.get('USER')
        password = crawler.settings.get('PASSWORD')
        database = crawler.settings.get('DATABASE')
    	# 利用pymysql连接mysql数据库
        db = pymysql.connect(host=host, user=user, password=password,
                             database=database, )
        # 创建一个游标
        cur = db.cursor()
        # 把数据库和游标封装到当前对象中
        obj = cls(db, cur)
        return obj

    def open_spider(self, spider):
        print('爬虫{}开启'.format(spider.name))
        # 连接数据库

    def process_item(self, item, spider):
        d = dict(item)
 		# 将数据通过游标cur写入连接的数据库中
        self.cur.execute('insert into movies_info (title,comment_num,score) values(%s,%s,%s)', list(d.values()))
        print('{}正在执行中....'.format(spider.name))
        return item

    def close_spider(self, spider):
        # 提交数据到数据库
        self.db.commit()
        # 关闭数据库连接
        self.db.close()
        print('爬虫{}关闭'.format(spider.name))

2.2.3 ImagesPipeline

scrapy专门封装了一个下载图片的 ImagesPipeline , 使用 ImagesPipeline for图像文件的优点是,您可以配

置一些额外的功能,例如生成缩略图和根据图像大小过滤图像。

有时候可能会采集图片资源,Scrapy帮我们实现了图片管道文件,很方便保存图片:

# spider爬虫文件

class VmgirlsSpider(scrapy.Spider):
  name = 'vmgirls'
  allowed_domains = ['vmgirls.com']
  start_urls = ['https://www.vmgirls.com/12985.html']

  def parse(self, response: scrapy.Selector):
    item = PictureItem()
    item['title'] = response.css('h1::text').extract_first()
    item['img_s'] = response.css('.post-content img::attr(data-src)').extract()
    print(item['img_s'])
    yield item

重点:

  • get_media_requests() 是用来发送请求的,需要传入图片的网址。
  • file_path() 是用来指定保存的文件的名字。
  • item_completed () 当请求完成后进行的操作

除了编写图片管道文件,还要在配置环境中激活,以及指定图片的存储位置

# settings.py

# 图片存储的路径
IMAGES_STORE = './images'
class DownloadPicturePipeline(ImagesPipeline):
    
  def get_media_requests(self, item, info):
    for image_url in item['img_src']:
        # 获取图片的url,并发请求
        yield scrapy.Request(image_url, meta={'filename': item['title']})
    
  def file_path(self, request, response=None, info=None):
    # 重命名,若不重写这函数,图片名为哈希
    # 获取图片的文件名
    filename = request.meta.get('filename')
    # 获取图片url中的文件名字
    image_guid = request.url.split('/')[-1]
    # request 对象请求到的图片的保存名字
    return os.path.join(filename, image_guid)

  def item_completed(self, results, item, info):
        # 最小的操作单位
        # 图片下载完成之后做的一些操作
        # true 下载成功

        # results 结构是什么东西
        # l = [
        #     (True, {'url': 'http://pic1.win4000.com/mobile/2020-05-25/5ecb68ff747b7_130_170.jpg',
        #             'path': '清纯金发美女高清手机壁纸\\5ecb68ff747b7_130_170.jpg',
        #             'checksum': 'f68c281392671dfb3a949811caad6435'}),
        #     (True, {'url': 'http://pic1.win4000.com/mobile/2020-05-25/5ecb69003d850_130_170.jpg',
        #             'path': '清纯金发美女高清手机壁纸\\5ecb69003d850_130_170.jpg',
        #             'checksum': 'd29a0f4d30f689d0742802e50d2b26cf'}),
        #     ]
    # 保存图片下载的路径
    image_paths = [x['path'] for ok, x in results if ok]
    if not image_paths:
        raise DropItem("Item contains no images")
    item['image_paths'] = image_paths
    return item

3. 关于pipeline

3.1 注册

pipeline ( 管道 ) , 首先你要想应用你写的管道 , 在运行爬虫之前要在 settings.py 中注册。

# settings.py line:68

ITEM_PIPELINES = {
   'spider1.pipelines.Spider1Pipeline': 299,
   'spider1.pipelines.Spider2Pipeline': 300,
}

# 可以注册多个管道, 数值小的优先级高,先执行,一般放在上面

3.2 执行流程

  1. 检测pipeline类中是否存在 from_crawler (cls, crawler) 方法
    • 有就执行
    • 否则就 _ _ init _ _ (self) 实例化对象
  2. 调用 open_spider(self, spider) 方法
  3. 执行爬虫文件 遇到 yield item 反复执行 process_item(self, item, spider)
  4. 当爬虫执行完毕的话 调用 close_spider(self, spider) 方法

如果存在多个pipeline类,会按照注册的优先级在第一个pipeline执行完 from_crawler (cls, crawler) 方法后

依次执行其他 pipeline类 中的 from_crawler (cls, crawler) 方法,然后依次执行 open_spider(self, spider)

再依次执行 process_item(self, item, spider)。但是 close_spider(self, spider) 方法则是优先级低的先执行。

注意:pipeline默认是全局的,无论是那个爬虫文件都会走所有的 pipeline ,如果你想对某个爬虫文件自定义

的话,可以通过 spider.name == 'xxx' , 做选择。

3. 数据去重

当我们的item对象中可能存在重复时,这个时候我们就需要对item对象进行检测是否存在重复。

这里我的思路是:在管道中维护一个列表,每次接收到item对象时先检测是否已经存在我们维护的列表中。

# 爬虫程序重新启动一次,过滤就会再来一次
# 爬虫结束之前把去重结果保存一次(写到一个临时文件),下次启动时,把去重结果加载一遍
# 不需要保存过的结果,重新再保存一次

# 维护这个列表
filter_set = []


class Qd04EnglishPipeline(object):
    def __init__(self):
        self.f = open('eng.csv', mode='w', encoding='utf-8', newline="")
        self.csv_write = csv.writer(self.f)

    # 结果去重
     def process_item(self, item, spider):
         """需要保存的数据与已经保存的数据有没有重复的
         读取文件,然后再判断文件里面的内容
         """
         d = dict(item)
         h = hashlib.md5(json.dumps(d).encode()).hexdigest()
         if h not in filter_set:
             # 如果这个结果是第一次出现,就先保存,并且记录下来
             filter_set.append(h)
             self.csv_write.writerow(d.values())
             return item

这里简单提一下,最好不要将item对象直接放到列表,如果每个item对象中封装很多字段或者有很多item对象,

那么这个列表中就会有很多内容,当数据内容非常多是,判断就会占用很多的内存空间,这样每次都要查询的话,

会很浪费时间,降低效率。所以我们采用摘要算法,对item对象进行摘要,对象摘要的结果是一个字符串,而且

相同的对象摘要出来的字符串是一样的。所以我们可以摘要之后才放到列表中。

摘要算法就是通过摘要函数对任意长度的数据 data 计算出固定长度的摘要,目的是为了发现原始数据是否被人篡

改过。摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算哈希很容易,但通过

digest反推 data 却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。

如果你想详细了解摘要算法可以自行百度。

示例:

import hashlib
import json

md5 = hashlib.md5()
d = {'name':'asd'}
d_str = json.dumps(d)
md5.update(d_str.encode())

h = md5.hexdigest()
print(h) # h就是对象摘要出来的结果

4. 小结

希望你看完本文章能够对scrapy框架中的 item.py文件和pipeline.py文件,以及数据多种方式的持久化,数据的

去重能够有自己的认识,并熟练掌握,其中保存到数据库这个是必须要会的,公司里面都是把数据保存到数据库。

posted @ 2020-07-06 22:42  Mn猿  阅读(178)  评论(0编辑  收藏  举报