• Scrapy入门


    Scrapy入门

    上一节我们介绍了Scrapy 框架的基本架构、数据流过程和项目架构,对Scrapy有了初步的认识。接下来我们用 Scrapy 实现一个简单的项目,完成一遍 Scrapy 抓取流程。通过这个过程,我们可以对Scrapy的基本用法和原理有大体了解。

    1.目标

    本节要完成的目标如下。

    • 创建一个 Scrapy 项目,熟悉 Scrapy项目的创建流程。
    • 编写一个 Spider 来抓取站点和处理数据,了解 Spider的基本用法。
    • 初步了解Item Pipeline 的功能,将抓取的内容保存到MongoDB数据库。
    • 运行Scrapy 爬虫项目,了解 Scrapy 项目的运行流程。

    这里我们以 Scrapy 推荐的官方练习项目为例进行实战演练,抓取的目标站点为 https://quotes.toscrape.com/,页面如图所示。

    image-20221107193538824

    这个站点包含了一系列名人名言、作者和标签,我们需要使用Scrapy将其中的内容爬取并保存下来。

    2.准备工作

    在开始之前,我们需要安装好Scrapy框架、MongoDB和PyMongo库,具体的安装参考流程如下:

    • Scrapy:使用pip命令进行安装
    • MongoDB:https://cuiqingcai.com/31070.html
    • PyMongo:使用pip命令进行安装

    安装好这三部分之后,我们就可以正常使用 Scrapy 命令了,同时也可以使用PyMongo连接MongoDB数据库并写入数据了。

    3.创建项目

    首先我们需要创建一个Scrapy项目,可以直接用命令生成,项目名称可以叫作 scrapytutorial,创建命令如下:

    scrapy startproject scrapytutorial
    
    • 1

    image-20221107223040585

    运行完毕后,当前文件夹下会生成一个名为scrapytutorial的文件夹:

    image-20221107223130728

    使用Pycharm打开文件,文件夹结构如下所示:

    image-20221107223917521

    4.创建 Spider

    Spider是自己定义的类,Scrapy 用它来从网页里抓取内容,并解析抓取的结果。不过这个类必须继承 Scrapy 提供的 Spider 类 scrapy.Spider,还要定义 Spider 的名称和起始 Request,以及怎样处理爬取后的结果的方法。

    也可以使用命令行创建一个 Spider。比如要生成 Quotes 这个 Spider,可以执行如下命令:

    cd scrapytutorial
    scrapy genspider quotes quotes.toscrape.com
    
    • 1
    • 2

    image-20221107233526324

    进人刚才创建的scrapytutorial 文件夹,然后执行genspider 命令。第一个参数是 Spider的名称,第二个参数是网站域名。执行完毕后,spiders 文件夹中多了一个 quotes.py,它就是刚刚创建的Spider,我们再把start_urls中的http协议改成https,最终代码如下所示:

    import scrapy
    
    
    class QuotesSpider(scrapy.Spider):
        name = 'quotes'
        allowed_domains = ['quotes.toscrape.com']
        start_urls = ['https://quotes.toscrape.com/']
    
        def parse(self, response):
            pass
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个QuotesSpider 就是刚才命令行自动创建的 Spider,它继承了 scrapy 的Spider类,QuotesSpitder有3个属性,分别为name、allowed_domains 和 start_urls,还有一个方法parse。

    • name是每个项目唯一的名字,用来区分不同的Spider。
    • allowed_domains是允许爬取的域名,如果初始或后续的请求链接不是这个域名下的,则请求链接会被过滤掉。
    • start_urls包含了 Spider 在启动时爬取的URL列表,初始请求是由它来定义的。
    • parse是 Spider的一个方法。在默认情况下,start_urls里面的链接构成的请求完成下载后,parse 方法就会被调用,返回的响应就会作为唯一的参数传递给 parse方法。该方法负责解析返回的响应、提取数据或者进一步生成要处理的请求。

    5.创建 Item

    Item是保存爬取数据的容器,定义了爬取结果的数据结构。它的使用方法和字典类似。不过相比字典,Item多了额外的保护机制,可以避免拼写错误或者定义字段错误。

    创建Item 需要继承scrapy的Item类,并且定义类型为Field的字段,这个字段就是我们要爬取的字段。

    那我们需要爬哪些字段呢?观察目标网站,我们可以获取到的内容有下面几项。

    • text:文本,即每条名言的内容,是一个字符串。
    • author:作者,即每条名言的作者,是一个字符串。
    • tags:标签,即每条名言的标签,是字符串组成的列表。

    这样的话,每条爬取数据就包含这3个字段,那么我们就可以定义对应的tem,此时将items.py修改如下:

    import scrapy
    
    class QuoteItem(scrapy.Item):
        text = scrapy.Field()
        author = scrapy.Field()
        tags = scrapy.Field()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里我们声明了QuoteItem,继承了Item类,然后使用 Field定义了3个字段,接下来爬取时我们会使用到这个Item。

    6.解析 Response

    前面我们看到,parse 方法的参数 response 是 start_urls 里面的链接爬取后的结果,即页面请求后得到的 Response,Scrapy 将其转化为了一个数据对象,里面包含了页面请求后得到的Response Status、Body 等内容。所以在parse方法中,我们可以直接对response 变量包含的内容进行解析,比如浏览请求结果的网页源代码,进一步分析源代码内容,或者找出结果中的链接而得到下一个请求。

    我们可以看到网页中既有我们想要的结果,又有下一页的链接,这两部分内容我们都要进行处理。

    首先看看网页结构,如图所示。每一页都有多个class为 quote的区块,每个区块内都包含text、author、tags。那么我们先找出所有的quote,然后提取每个quote 中的内容。
    image-20221107234939326

    我们可以使用CSS选择器或XPath选择器进行提取,这个过程我们可以直接借助response的css或 xpath 方法实现,这都是 Scrapy给我们封装好的方法,直接调用即可。

    在这里我们使用CSS选择器进行选择,可以将parse方法的内容进行如下改写:

     def parse(self, response):
            quotes=response.css('.quote')
            for quote in quotes:
                text=quote.css('.text::text').extract_first()
                author=quote.css('.author::text').extract_first()
                tags=quote.css('.tags .tags::text').extract()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里首先利用CSS 选择器选取所有的quote 并将其赋值为quotes变量,然后利用for 循环遍历每个quote,解析每个quote的内容。

    对text来说,观察到它的class为text,所以可以用.text选择器来选取,这个结果实际上是整个带有标签的节点,要获取它的正文内容,可以加 ::text。这时的结果是长度为1的列表,所以还需要用extract_first方法来获取第一个元素。而对于tags来说,由于我们要获取所有的标签,所以用extract 方法获取整个列表即可。

    7.使用Item

    上文我们已经定义了QuoteItem,接下来就要使用它了。

    我们可以把 ltem理解为一个字典,和字典还不太相同,其本质是一个类,所以在使用的时候需要实例化。实例化之后,我们依次用刚才解析的结果赋值 Item的每一个字段,最后将Item 返回。

    QuotesSpider的改写如下:

    import scrapy
    from scrapytutorial.items import QuoteItem
    
    
    class QuotesSpider(scrapy.Spider):
        name = 'quotes'
        allowed_domains = ['quotes.toscrape.com']
        start_urls = ['https://quotes.toscrape.com/']
    
        def parse(self, response):
            quotes = response.css('.quote')
            for quote in quotes:
                item = QuoteItem()
                item['text'] = quote.css('.text::text').extract_first()
                item['author'] = quote.css('.author::text').extract_first()
                item['tags'] = quote.css('.tags .tags::text').extract()
                yield item
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    如此一来,首页的所有内容就被解析出来并被赋值成了一个个QuoteItem了,每个Quoteltem就代表一条名言,包含名言的内容、作者和标签。

    8.后续Request

    上面的操作实现了从首页抓取内容,如果运行它,我们其实已经可以从首页提取到所有quote信息并将其转化为一个个QuoteItem 对象了。

    但是,这样还不够,下一页的内容该如何抓取呢?这就需要我们从当前页面中找到信息来生成下一个Request,利用同样的方式进行请求并解析就好了。那再下一页呢?也是一样的原理,我们可以在下一个页面里找到信息再构造再下一个Request。这样循环往复迭代,从而实现整站的爬取。

    我们将刚才的页面拉到最底部,如图所示。

    image-20221108234216407

    这里我们发现有一个Next按钮,查看一下源代码,可以看到它的链接是/page/2/,实际上全链接就是 https://quotes.toscrape.com/page/2,通过这个链接我们就可以构造下一个Request了。

    构造Request时需要用到scrapy的Request 类。这里我们传递两个参数,分别是url和 callback,这两个参数的说明如下。

    • url:目标页面的链接。
    • callback:回调方法,当指定了该回调方法的Request完成下载之后,获取Response,Engine会将该Response 作为参数传递给这个回调方法。回调方法进行Response的解析生成一个或多个Item 或 Request,比如上文的parse方法就是回调方法。

    由于刚才所定义的parse 方法就是用来提取名言text、author、tags的方法,而下一页的结构和刚才已经解析的页面结构是一样的,所以我们可以再次使用parse方法来做页面解析。

    接下来我们要做的就是利用选择器得到下一页链接并生成请求,在parse方法后追加如下的代码:

    next = response.css('.pager .next a::attr(href)').extract_first()
    url = response.urljoin(next)
    yield scrapy.Request(url=url, callback=self.parse())
    
    • 1
    • 2
    • 3

    第一行代码首先通过CSS选择器获取下一个页面的链接,即要获取超链接a中的href 属性,这里用到了::attr(href)进行提取,其中 attr 代表提取节点的属性,href 则为要提取的属性名,然后再下一步调用extract_first方法获取内容。

    第二行代码调用了urljoin方法,urljoin方法可以将相对URL构造成一个绝对URL。例如,获取到的下一页地址是/page/2/,urljoin方法处理后得到的结果就是 https://quotes.toscrape.com/page/2/。

    第三行代码通过url和callback变量构造了一个新的Request,回调方法callback依然使用parse方法。这个 Request 执行完成后,其对应的Response 会重新经过parse方法处理,得到第二页的解析结果,然后以此类推,生成第二页的下一页,也就是第三页的请求。这样爬虫就进入了一个循环,直到最后一页。

    通过几行代码,我们就轻松实现了一个抓取循环,将每个页面的结果抓取下来了。

    现在,改写后的整个Spider类如下所示:

    import scrapy
    from scrapytutorial.items import QuoteItem
    
    
    class QuotesSpider(scrapy.Spider):
        name = 'quotes'
        allowed_domains = ['quotes.toscrape.com']
        start_urls = ['https://quotes.toscrape.com/']
    
        def parse(self, response):
            quotes = response.css('.quote')
            for quote in quotes:
                item = QuoteItem()
                item['text'] = quote.css('.text::text').extract_first()
                item['author'] = quote.css('.author::text').extract_first()
                item['tags'] = quote.css('.tags .tags::text').extract()
                yield item
            next = response.css('.pager .next a::attr(href)').extract_first()
            url = response.urljoin(next)
            yield scrapy.Request(url=url, callback=self.parse())
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看到整个站点的抓取逻辑就轻松完成了,不需要再去编写怎样发送Request,不需要去关心异常处理,因为这些工作Scrapy都帮我们完成了,我们只需要关注Spider本身的抓取和提取逻辑即可。

    9.运行

    接下来就是运行项目了,点击终端,输入命令:

    scrapy crawl quotes
    
    • 1

    image-20221109113745605

    就可以看到Scrapy的运行结果了:

    image-20221109115404682

    image-20221109115439332

    这里只是部分运行结果,省略了一些中间的抓取结果。

    首先,Scrapy 输出了当前的版本号以及正在启动的项目名称。然后输出了当前 settings.py 中一些重写后的配置。接着输出了当前所应用的Middlewares和Item Pipelines。 Middlewares和 Item Pipelines都沿用了Scrapy的默认配置,我们可以在 settings.py中配置它们的开启和关闭,后文会对它们的用法进行讲解。

    接下来就是输出各个页面的抓取结果了,可以看到爬虫一边解析,一边翻页,直到将所有内容抓取完毕,然后终止。

    最后,Scrapy输出了整个抓取过程的统计信息,如请求的字节数、请求次数、响应次数、完成原因等。

    整个Scapy程序成功运行。我们通过非常简单的代码就完成了一个站点内容的爬取,所有的名言都被我们抓取下来了。

    10.保存到文件

    运行完Scrapy后,我们只在控制台上看到了输出结果。如果想保存结果该怎么办呢?

    要完成这个任务其实不需要任何额外的代码,Scrapy 提供的Feed Exports 可以轻松将抓取结果出。例如,如果我们想将上面的结果保存成JSON文件,那么可以执行如下命令:

    scrapy crawl quotes -o quotes.json
    
    • 1

    命令运行后,项目内多了一个quotesjson 文件,文件包含了刚才抓取的所有内容,内容是JSON格式。

    另外我们还可以让每一个Item 输出一行JSON,输出后缀为j1,为jsonline的缩写,命令如下所示:

    scrapy crawl quotes -o quotes.jl
    或
    scrapy crawl quotes -o quotes.jsonlines
    
    • 1
    • 2
    • 3

    Feed Exports支持从输出格式还有很多,例如csv、xml、pickle、marshal等,同时它支持ftp、s3等远程输出,另外还可以通过自定义ItemExporter 来实现其他的输出。

    例如,下面命令对应的输出分别为csv、xml、pickle、marshal格式以及ftp远程输出:

    scrapy crawl quotes -o quotes.csv 
    scrapy crawl quotes -o quotes.xml 
    scrapy crawl quotes -o quotes.pickle 
    scrapy crawl quotes -o quotes.marshal
    scrapy crawl quotes -o ftp://user:pass@ftp.example.com/path/to/quotes.csv
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其中,ftp 输出需要正确配置用户名、密码、地址、输出路径,否则会报错。

    通过Scrapy 提供的Feed Exports,我们可以轻松地将抓取结果到输出到文件中。对于一些小型项目来说,这应该足够了。

    如果想要更复杂的输出,如输出到数据库等,我们可以使用Item Pileline来完成。

    11.使用Item Pipeline

    如果想进行更复杂的操作,如将结果保存到 MongoDB数据库中或者筛选某些有用的Item,那么我们可以定义Item Pipeline 来实现。

    ltem Pipeline 为项目管道。当 Item生成后,它会自动被送到 Item Pipeline 处进行处理,我们可以用Item Pipeline来做如下操作:

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

    要实现 Item Pipeline 很简单,只需要定义一个类并实现process_item方法即可。启用Item Pipeline后,Item Pipeline 会自动调用这个方法。process_item方法必须返回包含数据的字典或Item对象,或者抛出 DropItem异常。

    process_item 方法有两个参数。一个参数是item,每次 Spider 生成的Item都会作为参数传递过来。另一个参数是spider,就是 Spider 的实例。

    接下来,我们实现一个 Item Pipeline,筛掉 text 长度大于 50的Item,并将结果保存到 MongoDB。修改项目里的 pipelines.py 文件,之前用命令行自动生成的文件内容可以删掉,增加一个TextPipeline 类,内容如下所示:

    from scrapy.exceptions import DropItem
    
    
    class Textpipeline(object):
        def _init_(self):
            self.limit = 50
    
        def process_item(self, item, spider):
            if item['text']:
                if len(item['text']) > self.limit:
                    item['text'] = item['text'][0:self.limit].rstrip() + '...'
                    return item
            else:
                return DropItem('Missing Text')
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这段代码在构造方法里定义了限制长度为50,实现了process_item方法,其参数是 item 和 spider。首先该方法判断item的text属性是否存在,如果不存在,则抛出DropItem异常。如果存在,再判断长度是否大于50,如果大于,那就截断然后拼接省略号,再将item返回。

    接下来,我们将处理后的item 存入 MongoDB,定义另外一个 Pipeline。同样在 pipelines.py 中,我们实现另一个类MongoPipeline,内容如下所示:

    import pymongo
    
    
    class MongoDBPipeline(object):
        def _init_(self, connection_string, database):
            self.connection_string = connection_string
            self.database = database
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(
                connection_string=crawler.settings.get('MONGODB_CONNECTION_STRING'),
                database=crawler.settings.get('MONCODB_DATABASE')
            )
    
        def open_spider(self, spider):
            self.client = pymongo.MongoClient(self.connection_string)
            self.db = self.client[self.database]
    
        def process_item(self, item, spider):
            name = item._class_._name_
            self.db[name].insert_one(dict(item))
            return item
    
        def close_spider(self, spider):
            self.client.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    MongoPipeline类实现了另外几个API定义的方法。

    • from_crawler:一个类方法,用@classmethod标识,这个方法是以依赖注入的方式实现的,方法的参数就是crawler。通过crawler,我们能拿到全局配置的每个配置信息,在全局配置settings-py 中,可以通过定义 MONGO_URI和 MONGO_DB来指定 MongoDB连接需要的地址和数据库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取 settings.py中的配置的。
    • open_spider:当 Spider 被开启时,这个方法被调用,主要进行了一些初始化操作。
    • close_spider:当 Spider 被关闭时,这个方法被调用,将数据库连接关闭。

    最主要的process_item方法则执行了数据插入操作,这里直接调用 insert_one 方法传入item对象即可将数据存储到 MongoDB。

    定义好 TextPipeline 和 MongoDBPipeline 这两个类后,我们需要在 settings.py 中使用它们。MongoDB的连接信息还需要定义。

    我们在 setings.py 中加入如下内容:

    ITEM_PIPELINES = {
        'scrapytutorial.pipelines.TextPipeline': 300,
        'scrapytutorial.pipelines.MongoDBPipeline': 400,
    }
    MONGODB_CONNECTION_STRING = 'localhost'
    MONGODB_DATABASE = 'scrapytutorial'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里我们声明了 ITEM_PIPELINES 字典,键名是 Pipeline 的类名称,健值是调用优先级,是一个数字,数字越小则对应的Pipeline 越先被调用,另外我们声明了MongoDB的连接字符串和存储的数据库名称。

    再重新执行爬取,命令还是一样的:

    scrapy crawl quotes
    
    • 1

    爬取结束后,我们可以看到MongoDB中创建了一个 scrapytutorial的数据库和QuoteItem的表,内容如图所示。

    image-20221109192010015

  • 相关阅读:
    Spring监听器-spring源码详解(五)
    Docker - Docker启动的MySql修改密码
    macOS鼠标管理操作增强BetterMouse简体中文
    车内静谧性超越埃尔法?走进腾势D9身价上亿的NVH实验室
    计算机视觉40例之案例02人脸打码与解码
    Vue/JS中定时器模拟随机指定范围、位数的小数并更新innerHTML以及页面被销毁时监听事件中销毁定时器
    群晖-使用docker套件部署Prometheus+Grafana
    长短期记忆神经网络
    Unity-UGUI-曲线绘制
    利用Aspose.Word对Word文件添加印章处理以及实现业务数据的替换处理
  • 原文地址:https://blog.csdn.net/W_chuanqi/article/details/127854585