用一个非常简单的项目来了解Scrapy完成一遍抓取流程

Posted zhi_neng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用一个非常简单的项目来了解Scrapy完成一遍抓取流程相关的知识,希望对你有一定的参考价值。

本文将通过介绍一个简单的项目,完成一遍Scrapy抓取的流程。通过这个过程,我们可以对Scrapy的基本用法和原理有一个大致的了解,方便以后的使用。

 

1.本文目标

具体需要完成的任务如下:

创建一个Scrapy项目;

创建一个Spider来抓取站点和处理数据;

通过命令行将抓取的内容导出;

将抓取的内容保存到MongoDB数据库。

 

2.准备工作

我们需要安装好Scrapy框架、MongoDB和PyMongo库,如果没有安装,就必须先安装,具体安装方法大家可以看我的安装教程相关文章,如果你使用的是Pycharm软件直接安装也是可以的,具体的这里就不多讲了。

 

3.创建项目

创建一个Scrapy项目,项目文件可以直接用scrapy命令生成,命令如下所示:

scrapy startproject tutorial

这个命可以在任意文件夹运行。如果提示权限有问题,可以加sudo运行该命令。这个命令将会创建一个名为tutorial的文件夹,文件结构如下所示:

 

4.创建Spider

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

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

cd tutorial

scrapy genspider quotes quotes.toscrape.com

进入刚才创建的文件夹tutorial,然后执行genspider命令。第一个参数是Spider的名称,第二个参数是网站域名。执行完毕后,spiders文件夹中多了一个quotes.py,它就是刚刚创建的Spider,内容如下所示:

这里有三个属性——name、allowed_domains、start_urls,还有一个方法parse。

name,它是每一个项目唯一的名字,用来区分不同的Spider。

allowed_domains,它是允许爬取的域名,如果初始或后续的请求链接不是这个域名下的,则请求链接会被过滤掉。

start_urls,它包含Spider在启动时爬取的url列表,初始请求由它来定义的。

parse,它是Spider的一个方法。默认情况下,被调用时start_urls里面的链接构成的请求完成下载执行后,返回的响应就会为唯一的参数传递给这个函数。该方法负责解析返回的响应、提取数据或者进一步生成要处理的请求。

 

5.创建Item

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

创建Item需要继承scrapy.Item类,并且定义类型为scrapy.Field的字段。观察目标网站,我们可以获取到内容有text、author、tags。

定义Item,此时将items.py修改如下:

这里定义了三个字段,接下来爬取时我们会使用到这个Item。

 

6.解析Response

前面我们看到了,parse()方法的参数response是start_urls里面的链接爬取后的结果。所以在parse()方法中,我们可以直接对response变量包含的内容进行解析,比如浏览请求结果的网页源代码,或者进一步分析源代码的内容,或者找到结果中的链接二得到下一个请求。

我们可以看到网页中既有我们想要的结果,又有下一页的链接,这两部分内容我们都要进行处理。首先看看网页结构,如下图所示。每一个页面都有多个class为quote的区块,每一个区块内都包含text、author、tags。那么我们先找出所有的quote,然后提取每一个quote中的内容。

提取的方式可以选择CSS选择器或者Xpath选择器。在这里我使用CSS选择器进行选择,parse()方法的改写如下所示:

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

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

 

7.使用Item

上面定义了Item,接下来就用一用它。Item可以理解为一个字典,不错在声明的时候需要实例化。然后依次用刚才解析的结果赋值Item的每一个字段,最后将Item返回即可。QuotesSpider的具体改写如下:

如此一来,首页的说有内容被解析出来了,并被赋值成了一个个QuoteItem。

 

8.后续Request

上面的操作实现了从初始页面抓取内容。那么,下一页的内容该如何抓取?这就需要我们从当前页面中找到信息来生产下一个请求,然后在下一个请求的页面里找到信息再构造下一个请求。这样循环往复迭代,从而实现整站的爬取。

将刚才的页面来到最底部,我们可以发现有一个Next按钮,如下图。查看它的源码,可以发现它的链接是/page/2/,全连接就是:http://quotes.toscrape.com/page/2,通过这个链接我们就可以构造下一个请求。

构造的时候需要用到scrapy.Request。这里我们传递两个参数——url和callback,这两个参数的说明如下:

url:它是请求链接。

callback:它是回调函数。当指定了该回调函数的请求完成之后,获取到响应,引擎会将该响应作为参数传递给这个回调函数。回调函数进行解析或生成下一个请求,回调函数如上面说的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)

第一句代码首先通过CSS选择器获取下一个页面的链接,即要获取a超链接中的href属性。这里用到了::attr(href)操作。然后再调用extract_first()方法来获取内容。

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

第三句代码通过url和callback变量构造了一个新请求,回调函数callback依然使用parse()方法。这个请求完成后,响应会重新经过parse方法处理,得到第二页的解析结果,然后生成第二页的下一页,也就是第三页的请求。这样爬虫就进入一个循环,直到最后一页。

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

改写后的Spider类如下所示:

 

9.运行代码

接下来,进入目录,执行如下命令:

scrapy crawl quotes 

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

这里是运行的部分结果,中间的抓取结果已经省略了。

首先,Scrapy输出了当前的版本号以及正在启动的项目名称。接着输出了当前settings.py中一些重写后的配置。然后输出了当前所应用的Middlewares和Pipelines。Middlewares是默认启用的,可以在settings.py中修改。Pipelines默认是空,同样也可以在settings.py中配置。

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

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

到此处,整个Scrapy程序成功运行。我们通过非常简单的代码就完成了一个网页内容的爬取,这样比之前一点点的写程序简洁很多。

 

10.保存到文件

运行完Scrapy后,我们只看到控制台结果。如想要保存结果我们应该如何操作呢?

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

scrapy crawl quotes -o quotes.json

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

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

scrapy crawl quotes -o quotes.jl 或者scrapy crawl quotes -o quotes.jsonlines

输出的格式还支持很多种,比如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

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

通过Scrapy提供的Feed Exports,我们可以轻松地输出抓取结果文件。对于一些小型项目来说,这应该足够了。不过如果想要更复杂的输出,比如输出到数据库,我们可以使用Item Pipeline来完成。

 

11.使用Item Pipeline

如果我们事先更复杂的操作,比如保存到MongoDB数据库,或者筛选某些有用的Item,则我们可以通过定义Item Pipeline来实现。

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

清理html数据

验证爬取数据,检查爬取字段

查重并丢弃重复内容

将爬取结果保存到数据库

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

process_iten()方法有两个参数,一个参数是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')

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

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

from scrapy.exceptions import DropItem
import pymongo

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')

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):
        name = item.__class__.__name__
        self.db[name].insert(dict(item))
        return item

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

from_crawler:它是一个类方法,用@classmethod标识,是一种依赖注入的方式。它的参数就是crawler,通过crawler我们可以拿到全局配置的每个配置信息。在全局变量settings.py中,我们可以定义MONGO_URI和MONGO_DB来指定MongoDB链接需要的地址和数据库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取settings.py中的配置的。

open_spider:当Spider开启时,这个方法被调用。上文程序主要进行了一些初始化操作。

close_spider:当Spider关闭时,这个方法会调用。上文程序中将数据库连接关闭。

最主要的process_item()方法则执行了数据库插入操作。

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

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

ITEM_PIPELINES = {
    'tutorial.pipelines.TextPipeline':300,
    'tutorial.pipelines.MongoPipeline':400
}
MONGO_URI = 'localhost'
MONGO_DB = 'tutorial'

赋值ITEM_PIPELINES字典,键名是Pipeline的类名称,键值是调用优先级,是一个数字,数字越小则对应的Pipeline越先被调用。

再重新执行爬取,命令如下所示:

scrapy crawl quotes

爬取结束后,MongoDB中创建了一个tutorial的数据库、QuoteItem的表,长的text已经被处理并追加了省略号,短的text保持不变,author和tags也都相应保存。

本文通过抓取quotes网站完成这个Scrapy的简单入门,更多精彩内容请看下回分解。

以上是关于用一个非常简单的项目来了解Scrapy完成一遍抓取流程的主要内容,如果未能解决你的问题,请参考以下文章

Scrapy 爬虫框架入门案例详解

Scrapy 1.4 文档 03 Scrapy 教程

Python之Scrapy安装

五分钟快速了解Scrapy爬虫框架

一个站点的诞生02--用Scrapy抓取数据

python 利用 scrapy 实现3个爬虫简单入门数据抓取