Scrapy框架的使用之Item Pipeline的用法

Item Pipeline是项目管道,本节我们详细了解它的用法。

首先我们看看Item Pipeline在Scrapy中的架构,如下图所示。

图中的最左侧即为Item Pipeline,它的调用发生在Spider产生Item之后。当Spider解析完Response之后,Item就会传递到Item Pipeline,被定义的Item Pipeline组件会顺次调用,完成一连串的处理过程,比如数据清洗、存储等。

Item Pipeline的主要功能有如下4点。

  • 清理HTML数据。
  • 验证爬取数据,检查爬取字段。
  • 查重并丢弃重复内容。
  • 将爬取结果保存到数据库。

一、核心方法

我们可以自定义Item Pipeline,只需要实现指定的方法,其中必须要实现的一个方法是: process_item(item, spider)

另外还有如下几个比较实用的方法。

  • open_spider(spider)
  • close_spider(spider)
  • from_crawler(cls, crawler)

下面我们详细介绍这几个方法的用法。

1. process_item(item, spider)

process_item()是必须要实现的方法,被定义的Item Pipeline会默认调用这个方法对Item进行处理。比如,我们可以进行数据处理或者将数据写入到数据库等操作。它必须返回Item类型的值或者抛出一个DropItem异常。

process_item()方法的参数有如下两个。

  • item,是Item对象,即被处理的Item。
  • spider,是Spider对象,即生成该Item的Spider。

process_item()方法的返回类型归纳如下。

  • 如果它返回的是Item对象,那么此Item会被低优先级的Item Pipeline的process_item()方法处理,直到所有的方法被调用完毕。
  • 如果它抛出的是DropItem异常,那么此Item会被丢弃,不再进行处理。

2. open_spider(self, spider)

open_spider()方法是在Spider开启的时候被自动调用的。在这里我们可以做一些初始化操作,如开启数据库连接等。其中,参数spider就是被开启的Spider对象。

3. close_spider(spider)

close_spider()方法是在Spider关闭的时候自动调用的。在这里我们可以做一些收尾工作,如关闭数据库连接等。其中,参数spider就是被关闭的Spider对象。

4. from_crawler(cls, crawler)

from_crawler()方法是一个类方法,用@classmethod标识,是一种依赖注入的方式。它的参数是crawler,通过crawler对象,我们可以拿到Scrapy的所有核心组件,如全局配置的每个信息,然后创建一个Pipeline实例。参数cls就是Class,最后返回一个Class实例。

下面我们用一个实例来加深对Item Pipeline用法的理解。

二、本节目标

我们以爬取360摄影美图为例,来分别实现MongoDB存储、MySQL存储、Image图片存储的三个Pipeline。

三、准备工作

请确保已经安装好MongoDB和MySQL数据库,安装好Python的PyMongo、PyMySQL、Scrapy框架。

四、抓取分析

我们这次爬取的目标网站为:https://image.so.com。打开此页面,切换到摄影页面,网页中呈现了许许多多的摄影美图。我们打开浏览器开发者工具,过滤器切换到XHR选项,然后下拉页面,可以看到下面就会呈现许多Ajax请求,如下图所示。

我们查看一个请求的详情,观察返回的数据结构,如下图所示。

返回格式是JSON。其中list字段就是一张张图片的详情信息,包含了30张图片的ID、名称、链接、缩略图等信息。另外观察Ajax请求的参数信息,有一个参数sn一直在变化,这个参数很明显就是偏移量。当sn为30时,返回的是前30张图片,sn为60时,返回的就是第31~60张图片。另外,ch参数是摄影类别,listtype是排序方式,temp参数可以忽略。

所以我们抓取时只需要改变sn的数值就好了。

下面我们用Scrapy来实现图片的抓取,将图片的信息保存到MongoDB、MySQL,同时将图片存储到本地。

五、新建项目

首先新建一个项目,命令如下所示:

scrapy startproject images360

接下来新建一个Spider,命令如下所示:

scrapy genspider images images.so.com

这样我们就成功创建了一个Spider。

六、构造请求

接下来定义爬取的页数。比如爬取50页、每页30张,也就是1500张图片,我们可以先在settings.py里面定义一个变量MAX_PAGE,添加如下定义:

MAX_PAGE = 50

定义start_requests()方法,用来生成50次请求,如下所示:

def start_requests(self):
    data = {'ch': 'photography', 'listtype': 'new'}
    base_url = 'https://image.so.com/zj?'
    for page in range(1, self.settings.get('MAX_PAGE') + 1):
        data['sn'] = page * 30
        params = urlencode(data)
        url = base_url + params
        yield Request(url, self.parse)

在这里我们首先定义了初始的两个参数,sn参数是遍历循环生成的。然后利用urlencode()方法将字典转化为URL的GET参数,构造出完整的URL,构造并生成Request。

还需要引入scrapy.Request和urllib.parse模块,如下所示:

from scrapy import Spider, Request
from urllib.parse import urlencode

再修改settings.py中的ROBOTSTXT_OBEY变量,将其设置为False,否则无法抓取,如下所示:

ROBOTSTXT_OBEY = False

运行爬虫,即可以看到链接都请求成功,执行命令如下所示:

scrapy crawl images

运行示例结果如下图所示。

所有请求的状态码都是200,这就证明图片信息爬取成功了。

七、提取信息

首先定义一个Item,叫作ImageItem,如下所示:

from scrapy import Item, Field
class ImageItem(Item):
    collection = table = 'images'
    id = Field()
    url = Field()
    title = Field()
    thumb = Field()

在这里我们定义了4个字段,包括图片的ID、链接、标题、缩略图。另外还有两个属性collectiontable,都定义为images字符串,分别代表MongoDB存储的Collection名称和MySQL存储的表名称。

接下来我们提取Spider里有关信息,将parse()方法改写为如下所示:

def parse(self, response):
    result = json.loads(response.text)
    for image in result.get('list'):
        item = ImageItem()
        item['id'] = image.get('imageid')
        item['url'] = image.get('qhimg_url')
        item['title'] = image.get('group_title')
        item['thumb'] = image.get('qhimg_thumb_url')
        yield item

首先解析JSON,遍历其list字段,取出一个个图片信息,然后再对ImageItem赋值,生成Item对象。

这样我们就完成了信息的提取。

八、存储信息

接下来我们需要将图片的信息保存到MongoDB、MySQL,同时将图片保存到本地。

MongoDB

首先确保MongoDB已经正常安装并且正常运行。

我们用一个MongoPipeline将信息保存到MongoDB,在pipelines.py里添加如下类的实现:

import pymongo

class MongoPipeline(object):
    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DB')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        self.db[item.collection].insert(dict(item))
        return item

    def close_spider(self, spider):
        self.client.close()

这里需要用到两个变量,MONGO_URIMONGO_DB,即存储到MongoDB的链接地址和数据库名称。我们在settings.py里添加这两个变量,如下所示:

MONGO_URI = 'localhost'
MONGO_DB = 'images360'

这样一个保存到MongoDB的Pipeline的就创建好了。这里最主要的方法是process_item()方法,直接调用Collection对象的insert()方法即可完成数据的插入,最后返回Item对象。

MySQL

首先确保MySQL已经正确安装并且正常运行。

新建一个数据库,名字还是images360,SQL语句如下所示:

CREATE DATABASE images360 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

新建一个数据表,包含id、url、title、thumb四个字段,SQL语句如下所示:

CREATE TABLE images (id VARCHAR(255) NULL PRIMARY KEY, url VARCHAR(255) NULL , title VARCHAR(255) NULL , thumb VARCHAR(255) NULL)

执行完SQL语句之后,我们就成功创建好了数据表。接下来就可以往表里存储数据了。

接下来我们实现一个MySQLPipeline,代码如下所示:

import pymysql

class MysqlPipeline():
    def __init__(self, host, database, user, password, port):
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            host=crawler.settings.get('MYSQL_HOST'),
            database=crawler.settings.get('MYSQL_DATABASE'),
            user=crawler.settings.get('MYSQL_USER'),
            password=crawler.settings.get('MYSQL_PASSWORD'),
            port=crawler.settings.get('MYSQL_PORT'),
        )

    def open_spider(self, spider):
        self.db = pymysql.connect(self.host, self.user, self.password, self.database, charset='utf8', port=self.port)
        self.cursor = self.db.cursor()

    def close_spider(self, spider):
        self.db.close()

    def process_item(self, item, spider):
        data = dict(item)
        keys = ', '.join(data.keys())
        values = ', '.join(['%s'] * len(data))
        sql = 'insert into %s (%s) values (%s)' % (item.table, keys, values)
        self.cursor.execute(sql, tuple(data.values()))
        self.db.commit()
        return item

如前所述,这里用到的数据插入方法是一个动态构造SQL语句的方法。

这里又需要几个MySQL的配置,我们在settings.py里添加几个变量,如下所示:

MYSQL_HOST = 'localhost'
MYSQL_DATABASE = 'images360'
MYSQL_PORT = 3306
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'

这里分别定义了MySQL的地址、数据库名称、端口、用户名、密码。

这样,MySQL Pipeline就完成了。

Image Pipeline

Scrapy提供了专门处理下载的Pipeline,包括文件下载和图片下载。下载文件和图片的原理与抓取页面的原理一样,因此下载过程支持异步和多线程,下载十分高效。下面我们来看看具体的实现过程。

官方文档地址为:https://doc.scrapy.org/en/latest/topics/media-pipeline.html。

首先定义存储文件的路径,需要定义一个IMAGES_STORE变量,在settings.py中添加如下代码:

IMAGES_STORE = './images'

在这里我们将路径定义为当前路径下的images子文件夹,即下载的图片都会保存到本项目的images文件夹中。

内置的ImagesPipeline会默认读取Item的image_urls字段,并认为该字段是一个列表形式,它会遍历Item的image_urls字段,然后取出每个URL进行图片下载。

但是现在生成的Item的图片链接字段并不是image_urls字段表示的,也不是列表形式,而是单个的URL。所以为了实现下载,我们需要重新定义下载的部分逻辑,即要自定义ImagePipeline,继承内置的ImagesPipeline,重写几个方法。

我们定义ImagePipeline,如下所示:

from scrapy import Request
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline

class ImagePipeline(ImagesPipeline):
    def file_path(self, request, response=None, info=None):
        url = request.url
        file_name = url.split('/')[-1]
        return file_name

    def item_completed(self, results, item, info):
        image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem('Image Downloaded Failed')
        return item

    def get_media_requests(self, item, info):
        yield Request(item['url'])

在这里我们实现了ImagePipeline,继承Scrapy内置的ImagesPipeline,重写下面几个方法。

  • get_media_requests()。它的第一个参数item是爬取生成的Item对象。我们将它的url字段取出来,然后直接生成Request对象。此Request加入到调度队列,等待被调度,执行下载。
  • file_path()。它的第一个参数request就是当前下载对应的Request对象。这个方法用来返回保存的文件名,直接将图片链接的最后一部分当作文件名即可。它利用split()函数分割链接并提取最后一部分,返回结果。这样此图片下载之后保存的名称就是该函数返回的文件名。
  • item_completed(),它是当单个Item完成下载时的处理方法。因为并不是每张图片都会下载成功,所以我们需要分析下载结果并剔除下载失败的图片。如果某张图片下载失败,那么我们就不需保存此Item到数据库。该方法的第一个参数results就是该Item对应的下载结果,它是一个列表形式,列表每一个元素是一个元组,其中包含了下载成功或失败的信息。这里我们遍历下载结果找出所有成功的下载列表。如果列表为空,那么该Item对应的图片下载失败,随即抛出异常DropItem,该Item忽略。否则返回该Item,说明此Item有效。

现在为止,三个Item Pipeline的定义就完成了。最后只需要启用就可以了,修改settings.py,设置ITEM_PIPELINES,如下所示:

ITEM_PIPELINES = {
    'images360.pipelines.ImagePipeline': 300,
    'images360.pipelines.MongoPipeline': 301,
    'images360.pipelines.MysqlPipeline': 302,
}

这里注意调用的顺序。我们需要优先调用ImagePipeline对Item做下载后的筛选,下载失败的Item就直接忽略,它们就不会保存到MongoDB和MySQL里。随后再调用其他两个存储的Pipeline,这样就能确保存入数据库的图片都是下载成功的。

接下来运行程序,执行爬取,如下所示:

scrapy crawl images

爬虫一边爬取一边下载,下载速度非常快,对应的输出日志如下图所示。

查看本地images文件夹,发现图片都已经成功下载,如下图所示。

查看MySQL,下载成功的图片信息也已成功保存,如下图所示。

查看MongoDB,下载成功的图片信息同样已成功保存,如下图所示。

这样我们就可以成功实现图片的下载并把图片的信息存入数据库。

九、本节代码

本节代码地址为:https://github.com/Python3WebSpider/Images360。

十、结语

Item Pipeline是Scrapy非常重要的组件,数据存储几乎都是通过此组件实现的。请读者认真掌握此内容。

原文发布于微信公众号 - 进击的Coder(FightingCoder)

原文发表时间:2018-05-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏walterlv - 吕毅的博客

.NET 使用 XPath 来读写 XML 文件

发布于 2018-06-24 13:10 更新于 2018-09...

1131
来自专栏机器学习从入门到成神

PyCharm下进行Scrapy项目的调试

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

3842
来自专栏老码农专栏

AAA - ActFramework的安全框架II - 应用

1992
来自专栏行者常至

mysql mybatis 批量更新

版权声明:本文为博主原创文章,允许转载,请标明出处。 https://blog.csdn.net/qwdafedv/article/deta...

4321
来自专栏北京马哥教育

爬虫框架Scrapy的第一个爬虫示例入门教程

豌豆贴心提醒,本文阅读时间8分钟 我们使用dmoz.org这个网站来作为小抓抓一展身手的对象。 首先先要回答一个问题。 问:把网站装进爬虫里,总共分几步? ...

3588
来自专栏Java学习网

Java开发之使用Java 8 Streams 对数据库进行 CRUD 操作

Speedment 是一个开放源代码的工具集,它可以被用来生成 Java 实体,并且能将我们同数据库的通信过程管理起来。你可以利用一个图形工具连接到数据库并生成...

1233
来自专栏七夜安全博客

(原创)七夜在线音乐台开发 第三弹 爬虫篇

1462
来自专栏架构师之旅

Web项目接口自动化测试框架搭建

一、原理及特点 参数放在XML文件中进行管理 用httpClient简单封装一个httpUtils工具类 测试用例管理使用了testNg管理,使用了TestNG...

2656
来自专栏进击的程序猿

Laravel之容器1. 背景2. DI3. 依赖反转4. Laravel中的容器参考

顾名思义,容器即存放东西的地方,里面存放的可以是文本、数值,甚至是对象、接口、回调函数。

632
来自专栏大内老A

WCF后续之旅(10): 通过WCF Extension实现以对象池的方式创建Service Instance

我们知道WCF有3种典型的对service instance进行实例化的方式,他们分别与WCF的三种InstanceContextMode相匹配,他们分别是Pe...

2148

扫码关注云+社区

领取腾讯云代金券