玩转 Scrapy 框架 (一):Item Pipeline 的使用

Item Pipeline 即项目管道,调用发生在 Spider 产生 Item 之后,当 Spider 解析完 Response,Item 就会被 Engine 传递到 Item Pipeline,被定义的 Item Pipeline 组件会顺次被调用,完成一连串的处理过程,比如数据清洗、存储等。Item Pipeline 的主要功能如下:

  1. 数据清理,主要清理 HTML 数据
  2. 校验抓取的数据,检查抓取的字段数据
  3. 查重并丢弃重复的内容
  4. 数据存储,也就是将抓取的数据保存到数据库中(如:SQLite、MySQL、MongoDB 等) 中。

一、核心方法

Item Pipeline 管道是一个普通的 Python 类,需要在 pipelines.py 文件中定义。与中间件类一样,也需要实现一些方法,大多数方法是可选的,但必须实现 def process_item(self, item, spider) 方法,其他几个可选的方法如下:

def open_spider(self, spider: Spider)
def close_spider(self, spider: Spider)
def from_crawler(cls, crawler)

下面分别对这些方法进行详细的描述。

1、def process_item(self, item, spider)。 process_item 是必须实现的方法,Item Pipeline 管道会默认调用这个方法对 Item 进行处理。例如,在这个方法中可以进行数据清洗或将数据写入数据库等操作。该方法必须返回 Item 类型的值或者抛出一个 DropItem 异常。process_item 方法的参数有如下两个:

item: 当前被处理的 Item 对象
spider: 生成 Item 的 Spider对象

process_item 方法根据不同的返回值类型会有不同的行为,返回值类型如下:

Item 对象: 该 Item 会被低优先级的 Item 管道的 process_item 方法处理,直到所有的 process_item 方法被调用完毕
DropItem 异常: 当前 Item 会被丢弃,不再进行处理

2、def open_spider(self, spider: Spider)。 open_spider 方法是在 Spider 开启时自动调用的。在这个方法中可以做一些初始化操作,如打开数据库连接、初始化变量等。其中 spider 就是被开启的 Spider 对象。

3、def close_spider(self, spider: Spider)。 close_spider 方法是在 Spider 关闭时自动调用的。在该方法中可以做一些收尾工作,如关闭数据库连接、删除临时文件等。其中 spider 就是被关闭的 Spider 对象。

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

二、实战:获取图片(仅学习使用)

2.1 简单分析

本例抓取某个网站中的 1920x1080 壁纸,并将这些壁纸的相关信息(如 URL、标题等) 保存在 MySQL 数据库和 MongoDB。

通过浏览器输入链接 aHR0cHM6Ly93d3cudHVwaWFuemouY29tL2luZGV4Lmh0bWw= 进入网站首页(推荐使用 Chrome 浏览器),然后在导航栏中选择壁纸图,如下图所示:
在这里插入图片描述
进入壁纸图页面之后,向下拉动浏览器滚动条,找到 电脑壁纸分辨率,并选择 1920x1080壁纸,如下图所示:
在这里插入图片描述
进入页面,如下图所示:
在这里插入图片描述然后在页面右键菜单中单击 检查 命令,会打开浏览器开发者工具。切换到 Network 标签,刷新页面,会发现 Network 标签下方显示了很多 URL,如下图所示:
在这里插入图片描述
单击第一个 URL 时,在右侧的 Preview 标签页中显示的返回信息正好是页面上的信息,展开这些信息,会看到下图所示的内容,很明显,里面包含了所有需要的信息,包括图形 URL、标题等。
在这里插入图片描述
对该网站的初步分析已经结束,找到了 URL,下一步就是需要分析 URL 中的参数了,单击上图所示的 Headers 标签,会看到下图所示的 General 部分,其中 Request URL 就是完整URL。
在这里插入图片描述
单击下一页,观察 URL 发生何种变化,如下图所示:
在这里插入图片描述
多试几次发现规律,URL 如下:

第1页url https://www.tupianzj.com/bizhi/1920x1080/list_65_1.html
第2页url https://www.tupianzj.com/bizhi/1920x1080/list_65_2.html
第3页url https://www.tupianzj.com/bizhi/1920x1080/list_65_3.html
第4页url https://www.tupianzj.com/bizhi/1920x1080/list_65_4.html
第5页url https://www.tupianzj.com/bizhi/1920x1080/list_65_5.html
第6页url https://www.tupianzj.com/bizhi/1920x1080/list_65_6.html

到现在为止,已经获得了所有必要的信息,URL 中 list_65_ 前面的是固定不变的,后面的数字代表某页的数据,第1页就是1,第2页就是2,以此类推。现在万事具备,只差写程序完成壁纸的抓取了。

2.2 编码

首先创建一个名为 TupianSpider 的爬虫项目,创建命令如下:
在这里插入图片描述
用 Pycharm 打开创建好的爬虫项目,目录结构如下图所示:
在这里插入图片描述
1、首先在 settings.py 文件中进行如下设置:
在这里插入图片描述
因为我们要将图片信息保存到 MySQL 数据库和 MongoDB 数据库中。要往数据库中写数据,需要连接数据库的信息,如域名或IP、数据库名、用户名、密码等。这些信息也可以直接在 settings.py 文件中进行配置,代码如下:
在这里插入图片描述
2、在 items.py 文件中定义要抓取的字段,示例代码如下:

import scrapy


class TupianspiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 定义MySQL表名和MongoDB数据集合名
    collection = table = "images"  # 表名和集合名
    idx = scrapy.Field()  # 编号
    url = scrapy.Field()  # 链接
    title = scrapy.Field()  # 标题

3、接下来在 picture.py 文件中编写抓取图片的爬虫程序,示例代码如下:

import scrapy
from TupianSpider.items import TupianspiderItem


class PictureSpider(scrapy.Spider):
    name = 'picture'
    # 图片下载地址域名不再该域名下
    # allowed_domains = ['www.tupianzj.com']
    url_template = "https://www.tupianzj.com/bizhi/1920x1080/list_65_{}.html"
    cookies = {
       填写你自己的cookie信息
    }

    def start_requests(self):
        # 通过for-in循环向服务端请求MAX_PAGE参数指定的次数
        # 测试的时候可以先将 MAX_PAGE 改为较小值
        for page in range(1, self.settings.get("MAX_PAGE") + 1):
            # 组成每一页URL并发送请求
            yield scrapy.Request(url=self.url_template.format(page), cookies=self.cookies)

    def parse(self, response, **kwargs):
        # 解析
        # print(response.text)
        li_list = response.xpath("//ul[@class='list_con_box_ul']/li")
        for li in li_list:
            item = TupianspiderItem()
            img_src = li.xpath("./a[1]/img/@src").extract_first()
            item["idx"] = img_src.split("/")[-1].replace(".jpg", "")
            item["title"] = li.xpath("./a[1]/@title").extract_first()
            item["url"] = img_src
            yield item

4、在 pipelines.py 文件中编写3个 Item 管道类,分别用来将图片保存到本地、保存到 MySQL 数据库和保存到 MongoDB 数据库,示例代码如下:

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


# 将图片信息保存到MongoDB数据库
class MongoPipeline:
    def __init__(self, mongo_uri, mongo_db):
        # 传入连接MongoDB数据库必要的信息
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        # 这个cls就是MongoPipeline本身,这里创建了MongoPipeline类的实例
        return cls(mongo_uri=crawler.settings.get("MONGO_URI"),
                   mongo_db=crawler.settings.get("MONGO_DB"))

    # 开启Spider时调用
    def open_spider(self, spider):
        # 连接MongoDB数据库
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    # 处理Item对象
    def process_item(self, item, spider):
        print(item)
        # 获取数据集的名字(本例是images)
        name = item.collection
        # 向images数据集插入文档
        self.db[name].insert_one(dict(item))
        return item

    def close_spider(self, spider):
        # 关闭MongoDB数据库
        self.client.close()


# 将图片信息保存到MySQL数据库中
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):
        # 创建MysqlPipeline类的实例
        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):
        # 连接数据库
        print(1111)
        self.db = pymysql.connect(host=self.host, user=self.user, password=self.password, database=self.database,
                                  # 注意这里的编码有坑,不要写utf-8 否则会报错
                                  charset="utf8", port=self.port)
        self.cursor = self.db.cursor()

    def close_spider(self, spider):
        # 关闭数据库
        self.db.close()

    def process_item(self, item, spider):
        print(item["title"])
        data = dict(item)
        keys = ", ".join(data.keys())
        values = ", ".join(['%s'] * len(data))
        sql = "insert into %s (%s) values (%s)" % (item.table, keys, values)
        # 将与图片相关的数据插入MySQL数据库的images表中
        self.cursor.execute(sql, tuple(data.values()))
        self.db.commit()
        return item


class TupianspiderPipeline(ImagesPipeline):
    # 返回对应本地图像文件的文件名
    # def file_path(self, request, response=None, info=None):
    def file_path(self, request, response=None, info=None, *, item=None):
        url = request.url
        # 注意: split 不要写成了 spilt
        file_name = url.split("/")[-1]
        print(file_name)
        return file_name

    # 过滤不符合条件的图片
    def item_completed(self, results, item, info):
        images_paths = [x['path'] for ok, x in results if ok]
        print(images_paths)
        if not images_paths:
            # 抛出异常,删除当前下载的图片
            raise DropItem("Image Download Failed")
        return item

    def get_media_requests(self, item, info):
        print(item["url"])
        # 根据当前url创建Request对象,并返回该对象,Request对象会加到调度队列中准备下载该图像
        yield Request(item["url"])
        # return item

TupianspiderPipeline 类从 ImagesPipeline 类派生,ImagesPipeline 是 Scrapy 的内建类,用于下载图像文件(FilesPipeline 用于下载文件,如 excel、word等)。ImagesPipeline 类会默认读取 Item 的 image_urls 字段,并认为该字段是一个列表形式,它会遍历 Item 的 image_urls 字段,然后取出每个 URL,并下载该 URL 对应的图片。

在 TupianspiderPipeline 类中重写了 ImagesPipeline 类的一些方法,这些方法的含义如下:

  1. file_path 方法:request 参数就是当前下载对应的 Request 对象,这个方法用来返回保存的文件名,本例直接将图片链接的最后一部分作为文件名,在实际应用中,也可以用随机的方式产生文件名,或使用其他任何方式产生文件名。
  2. item_completed 方法:当单个 Item 对象完成下载后调用该方法。因为不是每张图片都会下载成功,所以需要在该方法中分析下载结果,并剔除下载失败的图片。如果某张图片下载失败,那么就不需要将这样的图片保存到本地和数据库中。results 参数是 Item 对应的下载结果,是一个列表形式的值,每一个列表元素是一个元组,其中包含了下载成功或失败的信息。本例遍历下载结果找出所有下载成功的图片(保存到一个列表中)。如果列表为空,那么该 Item 对应的图片就下载失败,随机抛出 DropItem 异常并忽略该 Item,否则返回该 Item,表明此 Item 有效。
  3. get_media_requests 方法:item 参数是抓取生成的 Item 对象。将它的 url 字段取出来,然后直接生成 Request 对象。这个 Request 对象会加入调度队列,等待被调用,并下载 URL 对应的图像文件。

5、最后在 settings.py 文件中声明前面编写的3个管道类,并为 TupianspiderPipeline 类指定图像下载的本地路径,在默认情况下,ImagesPipeline 类会自动读取 settings.py 文件中名为 IMAGES_STORE 的变量值作为存储图片文件的路径。

ITEM_PIPELINES = {
    'TupianSpider.pipelines.TupianspiderPipeline': 300,
    'TupianSpider.pipelines.MongoPipeline': 301,
    'TupianSpider.pipelines.MysqlPipeline': 302,
}
# 指定存储图片文件的路径
IMAGES_STORE = "./images"

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

现在运行爬虫,会看到 Pycharm 输出类似下图所示的日志信息(前提是注释了 LOG_LEVEL = "ERROR" ),表明爬虫正在抓取图片,并将这些图片保存到本地以及数据库中。
在这里插入图片描述
在抓取图片的过程中,会看到在项目目录下多了一个 images 目录,这就是保存本地图片的目录。我们可以打开该目录,会看到 images 目录中有很多图像文件,如下图所示:
在这里插入图片描述
我们也可以打开 MySQL 数据库的 images 表,如果 images 表中包含类似下图所示的信息,表明图片数据插入成功。
在这里插入图片描述
MongoDB 中查看图片信息:

Scrapy 提供了专门处理下载的 Pipeline,包括文件下载和图片下载。下载文件的原理和图片的原理与抓取页面的原理一样,因此下载过程支持异步和多线程,十分高效。官方文档地址为:https://doc.scrapy.org/en/latest/topics/media-pipeline.html

补充:如果是下载文件,由继承 ImagesPipeline 改为继承 FilesPipeline 即可,并将 IMAGES_STORE 改为 FILES_STORE,其他操作不变。

三、总结

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

至此今天的案例就到此结束了,笔者在这里声明,笔者写文章只是为了学习交流,以及让更多学习 Python 基础的读者少走一些弯路,节省时间,并不用做其他用途,如有侵权,联系博主删除即可。感谢您阅读本篇博文,希望本文能成为您编程路上的领航者。祝您阅读愉快!


在这里插入图片描述

    好书不厌读百回,熟读课思子自知。而我想要成为全场最靓的仔,就必须坚持通过学习来获取更多知识,用知识改变命运,用博客见证成长,用行动证明我在努力。
    如果我的博客对你有帮助、如果你喜欢我的博客内容,请 点赞评论收藏 一键三连哦!听说点赞的人运气不会太差,每一天都会元气满满呦!如果实在要白嫖的话,那祝你开心每一天,欢迎常来我博客看看。
 编码不易,大家的支持就是我坚持下去的动力。点赞后不要忘了 关注 我哦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值