Scrapy 爬虫框架入门案例详解

欢迎你们关注腾讯云技术社区-博客园官方主页,咱们将持续在博客园为你们推荐技术精品文章哦~css

做者:崔庆才git

Scrapy入门

本篇会经过介绍一个简单的项目,走一遍Scrapy抓取流程,经过这个过程,能够对Scrapy对基本用法和原理有大致的了解,做为入门。程序员

在本篇开始以前,假设已经安装成功了Scrapy,若是还没有安装,请参照上一节安装课程github

本节要完成的任务有:数据库

  • 建立一个Scrapy项目json

  • 建立一个Spider来抓取站点和处理数据数组

  • 经过命令行将抓取的内容导出cookie

建立项目

在抓取以前,你必需要先建立一个Scrapy项目,能够直接用scrapy命令生成,命令以下:数据结构

scrapy startproject tutorial

 

在任意文件夹运行均可以,若是提示权限问题,能够加sudo运行。这个命令将会建立一个名字为tutorial的文件夹,文件夹结构以下:dom

|____scrapy.cfg     # Scrapy部署时的配置文件
|____tutorial         # 项目的模块,引入的时候须要从这里引入
| |______init__.py    
| |______pycache__
| |____items.py     # Items的定义,定义爬取的数据结构
| |____middlewares.py   # Middlewares的定义,定义爬取时的中间件
| |____pipelines.py       # Pipelines的定义,定义数据管道
| |____settings.py       # 配置文件
| |____spiders         # 放置Spiders的文件夹
| | |______init__.py
| | |______pycache__

 

建立Spider

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

建立一个Spider也能够用命令生成,好比要生成Quotes这个Spider,能够执行命令。

cd tutorial
scrapy genspider quotes

首先进入到刚才建立的tutorial文件夹,而后执行genspider这个命令,第一个参数是Spider的名称,第二个参数是网站域名。执行完毕以后,你会发如今spiders文件夹中多了一个quotes.py,这就是你刚刚建立的Spider,内容以下:

# -*- coding: utf-8 -*-
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        pass

能够看到有三个属性,name,allowed_domainsstart_urls,另外还有一个方法parse

  • name,每一个项目里名字是惟一的,用来区分不一样的Spider。

  • allowed_domains容许爬取的域名,若是初始或后续的请求连接不是这个域名下的,就会被过滤掉。

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

  • parse,是Spider的一个方法,默认状况下,被调用时start_urls里面的连接构成的请求完成下载后,返回的response就会做为惟一的参数传递给这个函数,该方法负责解析返回的response,提取数据或者进一步生成要处理的请求。

建立Item

Item是保存爬取数据的容器,它的使用方法和字典相似,虽然你能够用字典来表示,不过Item相比字典多了额外的保护机制,能够避免拼写错误或者为定义字段错误。

建立Item须要继承scrapy.Item类,而且定义类型为scrapy.Field的类属性来定义一个Item。观察目标网站,咱们能够获取到到内容有text, author, tags

因此能够定义以下的Item,修改items.py以下:

import scrapy

class QuoteItem(scrapy.Item):

    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

定义了三个Field,接下来爬取时咱们会使用它。

解析Response

在上文中说明了parse方法的参数resposne是start_urls里面的连接爬取后的结果。因此在parse方法中,咱们能够直接对response包含的内容进行解析,好比看看请求结果的网页源代码,或者进一步分析源代码里面包含什么,或者找出结果中的连接进一步获得下一个请求。

观察网站,咱们能够看到网页中既有咱们想要的结果,又有下一页的连接,因此两部分咱们都要进行处理。

首先看一下网页结构,每一页都有多个class为quote的区块,每一个区块内都包含text,author,tags,因此第一部须要找出全部的quote,而后对每个quote进一步提取其中的内容。

 

提取的方式能够选用CSS选择器或XPath选择器,在这里咱们使用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 .tag::text').extract()

在这里使用了CSS选择器的语法,首先利用选择器选取全部的quote赋值为quotes变量。
而后利用for循环对每一个quote遍历,解析每一个quote的内容。

对text来讲,观察到它的class为text,因此能够用.text来选取,这个结果其实是整个带有标签的元素,要获取它的内容,能够加::text来获得。这时的结果是大小为1的数组,因此还须要用extract_first方法来获取第一个元素,而对于tags来讲,因为咱们要获取全部的标签,因此用extract方法获取便可。

以第一个quote的结果为例,各个选择方法及结果归类以下:

  • 源码
<div class="quote" itemscope="" itemtype="http://schema.org/CreativeWork">
        <span class="text" itemprop="text">“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”</span>
        <span>by <small class="author" itemprop="author">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
        </span>
        <div class="tags">
            Tags:
            <meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world"> 
            <a class="tag" href="/tag/change/page/1/">change</a>
            <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
            <a class="tag" href="/tag/thinking/page/1/">thinking</a>
            <a class="tag" href="/tag/world/page/1/">world</a>
        </div>
    </div>
  • quote.css('.text')
[<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]" data='<span class="text" itemprop="text">“The '>]
  • quote.css('.text::text')
[<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]/text()" data='“The world as we have created it is a pr'>]
  • quote.css('.text').extract()
['<span class="text" itemprop="text">“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”</span>']
  • quote.css('.text::text').extract()
['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”']
  • quote.css('.text::text').extract_first()
“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”

因此,对于text,要获取第一个元素便可,因此使用extract_first()方法,对于tags,要获取全部元素,使用extract()方法。

使用Item

刚才定义了Item,接下来就要轮到使用它了,你能够把它理解为一个字典,不过在声明的时候须要实例化。而后依次对刚才解析的结果赋值,返回便可。

接下来QuotesSpider改写以下:

import scrapy
from tutorial.items import QuoteItem

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ['http://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 .tag::text').extract()
            yield item

如此一来,首页的全部内容就解析出来了,并赋值成了一个个QuoteItem。

后续Request

如上的操做实现了从初始页面抓取内容,不过下一页的内容怎样继续抓取?这就须要咱们从该页面中找到信息来生成下一个请求,而后下一个请求的页面里找到信息再构造下一个请求,这样循环往复迭代,从而实现整站的爬取。

 

观察到刚才的页面拉到最下方,有一个Next按钮,查看一下源代码,能够发现它的连接是/page/2/,实际上全连接就是http://quotes.toscrape.com/page/2,经过这个连接咱们就能够构造下一个请求。

构造请求时须要用到scrapy.Request,在这里咱们传递两个参数,url和callback。

  • url,请求连接

  • callback,回调函数,当这个请求完成以后,获取到response,会将response做为参数传递给这个回调函数,回调函数进行解析或生成下一个请求,如上文的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)操做,经过::attr加属性名称咱们能够获取属性的值。而后再调用extract_first方法获取内容。

第二句是调用了urljoin方法,它能够将相对url构形成一个绝对的url,例如获取到的下一页的地址是/page/2,经过urljoin方法处理后获得的结果就是http://quotes.toscrape.com/page/2/

第三句是经过url和callback构造了一个新的请求,回调函数callback依然使用的parse方法。这样在完成这个请求后,response会从新通过parse方法处理,处理以后,获得第二页的解析结果,而后生成第二页的下一页,也就是第三页的请求。这样就进入了一个循环,直到最后一页。

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

如今改写以后整个Spider类是这样的:

import scrapy
from tutorial.items import QuoteItem

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ['http://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 .tag::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)

接下来让咱们试着运行一下看看结果,进入目录,运行以下命令:

scrapy crawl quotes

就能够看到Scrapy的运行结果了。

2017-02-19 13:37:20 [scrapy.utils.log] INFO: Scrapy 1.3.0 started (bot: tutorial)
2017-02-19 13:37:20 [scrapy.utils.log] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'tutorial.spiders', 'SPIDER_MODULES': ['tutorial.spiders'], 'ROBOTSTXT_OBEY': True, 'BOT_NAME': 'tutorial'}
2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.logstats.LogStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats']
2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2017-02-19 13:37:20 [scrapy.core.engine] INFO: Spider opened
2017-02-19 13:37:20 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2017-02-19 13:37:20 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2017-02-19 13:37:21 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2017-02-19 13:37:21 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/> (referer: None)
2017-02-19 13:37:21 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/>
{'author': u'Albert Einstein',
 'tags': [u'change', u'deep-thoughts', u'thinking', u'world'],
 'text': u'\u201cThe world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.\u201d'}
2017-02-19 13:37:21 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/>
{'author': u'J.K. Rowling',
 'tags': [u'abilities', u'choices'],
 'text': u'\u201cIt is our choices, Harry, that show what we truly are, far more than our abilities.\u201d'}
...
2017-02-19 13:37:27 [scrapy.core.engine] INFO: Closing spider (finished)
2017-02-19 13:37:27 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 2859,
 'downloader/request_count': 11,
 'downloader/request_method_count/GET': 11,
 'downloader/response_bytes': 24871,
 'downloader/response_count': 11,
 'downloader/response_status_count/200': 10,
 'downloader/response_status_count/404': 1,
 'dupefilter/filtered': 1,
 'finish_reason': 'finished',
 'finish_time': datetime.datetime(2017, 2, 19, 5, 37, 27, 227438),
 'item_scraped_count': 100,
 'log_count/DEBUG': 113,
 'log_count/INFO': 7,
 'request_depth_max': 10,
 'response_received_count': 11,
 'scheduler/dequeued': 10,
 'scheduler/dequeued/memory': 10,
 'scheduler/enqueued': 10,
 'scheduler/enqueued/memory': 10,
 'start_time': datetime.datetime(2017, 2, 19, 5, 37, 20, 321557)}
2017-02-19 13:37:27 [scrapy.core.engine] INFO: Spider closed (finished)

在这里贴出部分运行结果,中间的一些抓取结果输出已省略。

首先Scrapy输出了当前的版本号,启动的项目。其次输出了当前在settings.py中的一些重写后的配置。而后输出了当前所应用的middlewares和pipelines,middlewares是默认启用的,若是要修改,咱们能够在settings.py中修改,pipelines默认是空,一样也能够在settings.py中配置,后面会进行讲解。

再接下来就是输出各个页面的抓取结果了,能够看到它一边解析,一边翻页,直至将全部内容抓取完毕,而后终止。

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

这样整个Scrapy程序就成功运行完毕了。

能够发现咱们经过很是简单的一些代码就完成了一个网站内容的爬取,相比以前本身一点点写程序是否是简洁太多了?

保存到文件

刚才运行完Scrapy后,咱们只在控制台看到了输出结果,若是想将结果保存该怎么办呢?

好比最简单的形式,将结果保存成Json文件。

要完成这个其实不须要你写任何额外的代码,Scrapy提供了Feed Exports能够轻松地将抓取结果输出,例如咱们想将上面的结果保存成Json文件,能够执行以下命令:

scrapy crawl quotes -o quotes.json 

运行后发现项目内就会多了一个quotes.json文件,里面包含的就是刚才抓取的全部内容,是一个Json格式,多个项目由中括号包围,是一个合法的Json格式。

另外你还能够每个Item一个Json,最后的结果没有中括号包围,一行对应一个Item,命令以下:

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 Pileline更方便地实现。

使用Item Pipeline

至此,你已经能够成功地完成抓取并将结果保存了,若是你想进行更复杂的操做,如将结果保存到数据库,如MongoDB,或者筛选某些有用的Item,能够定义Item Pileline来实现。

Item Pipeline意为项目管道,当生成Item后,它会自动被送到Item Pipeline进行处理,咱们经常使用它来作以下操做:

  • 清理HTML数据

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

  • 查重并丢弃重复内容

  • 将爬取结果储存到数据库

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

这个方法由两个参数,一个是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,若是你尚未安装,请先安装好MongoDB。

另外还须要安装一个MongoDB开发包pymongo,利用pip安装便可:

pip3 install pymongo

接下来定义另一个Pipeline,一样在pipelines.py中,实现另外一个类MongoPipeline,内容以下:

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

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

在这个类中,实现了API定义的另外几个方法。

  • from_crawler,这是一个类方法,用@classmethod标识,是一种依赖注入的方式,方法的参数就是crawler,经过crawler这个咱们能够拿到全局配置的每一个配置信息,在全局配置settings.py中咱们能够定义MONGO_URIMONGO_DB来指定MongoDB链接须要的地址和数据库名称,拿到配置信息以后返回类对象便可。因此这个方法的定义主要是用来获取settings.py中的配置的。

  • open_spider,当spider被开启时,这个方法被调用。在这里主要进行了一些初始化操做。

  • close_spider,当spider被关闭时,这个方法会调用,在这里将数据库链接关闭。

那么最主要的process_item方法则执行了数据插入操做。

好,定义好这两个类后,咱们须要在settings.py中使用他们,还须要定义MongoDB的链接信息。

settings.py中加入以下内容:

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

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

定义好了以后,再从新执行爬取,命令以下:

scrapy crawl quotes

爬取结束后,能够观察到MongoDB中建立了一个tutorial的数据库,QuoteItem的表。

到如今,咱们就经过抓取quotes完成了整个Scrapy的简单入门,但这只是冰山一角,还有不少内容等待咱们去探索,后面会进行讲解。

源代码

本节代码:https://github.com/Germey/ScrapyTutorial

【有奖讨论】程序员,怎么应对三十岁? 点击查看详情

相关阅读

利用Scrapy爬取全部知乎用户详细信息并存至MongoDB
反爬虫和抗DDOS攻击技术实践
如何轻松爬取网页数据?


此文已由做者受权腾讯云技术社区发布,转载请注明文章出处
原文连接:https://www.qcloud.com/community/article/592498
获取更多腾讯海量技术实践干货,欢迎你们前往腾讯云技术社区

相关文章
相关标签/搜索