scrapy框架的使用

scrapy框架介绍

Scrapy基于Twisted的异步处理框架,是纯python实现的框架。结构清晰,模块间的耦合程度低,可扩展性强,灵活完成各种需求。

1.架构

Engine:引擎,处理整个系统的数据流处理,触发事务,框架的核心。

Item:项目,它定义了爬取结果的数据结构,爬取的数据会被赋值成该Item对象。

Scheduler:调度器,接受引擎发过来的请求并将其加入其中,在引擎再次请求的时候将请求提供给引擎。

Downlaoder:下载器,下载网页内容,并将其网页内容返回给蜘蛛。

Spiders:蜘蛛,定义了爬取的逻辑和网页的解析规则,主要负责解析响应并生成提取结果和新的请求。

Item Pipeline:项目管道,负责处理由蜘蛛从网页中抽取的项目,它的主要任务是清洗,验证和存数据。

Downloader Middlewares:下载器中间件,位于引擎和下载器之间的钩子框架,主要处理引擎和下载器之间的请求及响应。

Spider Middlewares:蜘蛛中间件,位于引擎和蜘蛛之间的钩子框架,处理向蜘蛛输入的响应和输出结果及新的请求。

2.数据流

Scrapy的数据流由引擎控制,数据流的过程如下:

(1) Engine 首先打开一个网站,找到处理该网站的spider,并向该spider请求第一个要爬取的url

(2) Engine从spider中获取到第一个要爬取的url,并通过scheduler以request形式调度。

(3) Engine向scheduler请求下一个要爬取的url

(4)Scheduler返回下一个要爬取的url给Engine,Engine将url通过Downloader Middlewares转发给Downloader下载。

(5)一旦页面下载完毕,Downloader生成该页面Response,并将其通过downloader middlewares发送给Engine

(6)Engine从下载器接收到Response,并将其通过Spider Middlewares发给spider处理

(7)Spider处理Response,并返回提取到的Item及新的Request给Engine

(8)Engine将spider返回的Item给Item Pipeline,将新的request 给Scheduler

(9)重复(2)-(8),直到Scheduler中没有更多的Request,Engine关闭该网站,爬取结束。

3.项目结构

scrapy不同于pyspider,它通过命令行来创建项目,代码的编写还是需要IDE。项目创建之后,项目文件结构为:

scrapy.cfg
projectname/
    __init__.py
    items.py
    pipelines.py
    settings.py
    middlewares.py
    spiders/
        __init__.py
        spider1.py
        spider2.py
    ...

各个文件的功能:

scrapy.cfg:scrapy项目的配置文件,其内定义了项目的配置文件路径,部署相关信息等内容。

items.py:它定义Item数据结构,所有的Item的定义都可以放到这里。

pipelines.py:它定义Item Pipilines的实现,所有的Item pipelines的实现都可以放到这里。

settings.py:项目的全局配置。

middlewares.py:定义spider middlewares和download middlewares的实现。

spiders:包含一个个spider的实现,灭个spider都有一个文件。

scrapy入门

创建一个scrapy项目

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

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

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

所需环境:

scrapy框架,MongoDB和pymongo

目标网站: http://quotes.toscrape.com/

1.创建项目

项目文件可以直接用命令生成,命令在个目录执行,项目就在此目录生成。

scrapy startproject tutorial

在此目录创建一个tutorial的文件夹,文件夹结构如:

scrapy.cfg
tutorial
    __init__.py
    items.py items的定义,爬取的数据结构
    middlewares.py 定义爬取的中间件
    pipelines.py 定义数据管道
    settings.py 配置文件
    spiders 放置spider的文件夹
        __init__.py
2.创建spider

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

命令行创建一个spider,生成quotes这个spider,如下

cd tutorial
scrapy genspider quotes quotes.toscrape.com

进入刚刚创建的文件夹,执行genspider命令,第一个参数是spider的名称,第二个参数是网站域名。执行成功后,spiders文件夹多了一个quotes.py文件。

其内容为:

# -*- 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 每个项目唯一名字,用来区分不同的spider

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

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

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

3.创建Item

item是保存爬取数据的容器,使用方法和字典类似,而且字典更加全面,新增了额外的保护机制,避免拼写错误或者定义字段错误。

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

修改items.py内容为

import scrapy
class QuoteItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    text=scrapy.Field()
    author=scrapy.Field()
    tag=scrapy.Field()
    #定义了三个字段,类的名称修改为QuoteItem,以供后续的使用
4.解析Response

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

此次目标网站每页都有10个div,每个div的class都为quote,这个div中包含text,author,tag。首先,找出所有quote,然后获取每个div的内容。

提取的方法可以是css选择器或者Xpath选择器。

使用css选择器,改写parse()方法为:

def parse(self, response):
    quotes=response.css('.quote')
    for quote in quotes:
        text=quote.css('.text::text').extract_first()
        #.text获取的是整个标签的节点,如果只获取正文内容,用::text来获取,这是的结果是长度为1的列表,用extract_first()方法直接获取
        author=quote.css('.author::text').extract_first()
        #类似.text
        tags=quote.css('.tags .tag::text').extract()
        #tags要获取所有标签,所以用extract()获取整个列表
5.使用Item

上文定义了Item,简单理解为一个字典。声明的时候需要实例化,将解析的结果赋值item的每一个字段,最后将item返回。

QuotesSpider改写后的内容为:

# -*- coding: utf-8 -*-
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()
            #.text获取的是整个标签的节点,如果只获取正文内容,用::text来获取,这是的结果是长度为1的列表,用extract_first()方法直接获取
            item['author']=quote.css('.author::text').extract_first()
            #类似.text
            item['tags']=quote.css('.tags .tag::text').extract()
            #tags要获取所有标签,所以用extract()获取整个列表
            yield item
6.后续Request

到目前为止,实现了初始页面的抓取。接下来,就要分析源码,找到下一页的信息构造请求,然后在下一页的信息

中再构造再下一页的请求。

分析源码,可以看到,下一页的链接有/page/2,全链接就是http://quotes.tostrape.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()
#通过css选择器获取下一个页面的链接,此时用到了::attr(href)操作。然后调用extract_first()方法获取内容。
url=response.urljoin(next)
#调用urljoin()方法,urljoin()方法可以将相对url构造成一个绝对url,
#下一页的地址是/page/2,urljoin()处理后的结果就是http://quotes.toscrape.com/page/2
yield scrapy.Request(url=url,callback=self.parse)
#通过url和callback构造了一个新的请求,回调函数依然用parse()方法。

这个请求完成之后,响应会重新经过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()
            #.text获取的是整个标签的节点,如果只获取正文内容,用::text来获取,这是的结果是长度为1的列表,用extract_first()方法直接获取
            item['author']=quote.css('.author::text').extract_first()
            #类似.text
            item['tags']=quote.css('.tags .tag::text').extract()
            #tags要获取所有标签,所以用extract()获取整个列表
            yield item
        next=response.css('.pager .next a::attr(href)').extract_first()
        #通过css选择器获取下一个页面的链接,此时用到了::attr(href)操作。然后调用extract_first()方法获取内容。
        url=response.urljoin(next)
        #调用urljoin()方法,urljoin()方法可以将相对url构造成一个绝对url,
        #下一页的地址是/page/2,urljoin()处理后的结果就是http://quotes.toscrape.com/page/2
        yield scrapy.Request(url=url,callback=self.parse)
        #通过url和callback构造了一个新的请求,回调函数依然用parse()方法。
7.运行

进入目录,运行命令

scrapy crawl quotes

控制台得到输出结果

...
2020-01-21 15:06:11 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/9/>
quotes.toscrape.com/page/8/)
2020-01-21 15:06:11 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/9/>
{'author': 'Albert Einstein',
 'tags': ['mistakes'],
 'text': '“Anyone who has never made a mistake has never tried anything new.”'}
2020-01-21 15:06:11 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/9/>
{'author': 'Jane Austen',
 'tags': ['humor', 'love', 'romantic', 'women'],
 'text': "“A lady's imagination is very rapid; it jumps from admiration to "
         'love, from love to matrimony in a moment.”'}
2020-01-21 15:06:11 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/9/>
{'author': 'J.K. Rowling',
 'tags': ['integrity'],
 'text': '“Remember, if the time should come when you have to make a choice '
         'between what is right and what is easy, remember what happened to a '
         'boy who was good, and kind, and brave, because he strayed across the '
         'path of Lord Voldemort. Remember Cedric Diggory.”'}
2020-01-21 15:06:11 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/9/>
{'author': 'Jane Austen',
...
8.保存到文件

scrapy提供的Feed Exports可以较为方便的将抓取结果输出。

如果将爬取结果保存为json文件,可执行如下命令:

scrapy crawl quotes -o quotes.json

运行之后项目中多了一个quotes.json文件,包含了抓取的所有内容。

另外也可以灭一个Item输出一行json,输出后缀为jl,为jsonline的缩写,命令为:

scrapy crawl quotes -o quotes.jl

输出格式有很多,像csv,xml,pickl等,还支持ftp,s3等远程输出。也可通过自定义来实现其他输出。

将结果输出到文件,对一些小型项目来说,已经足够了。但若想要更加复杂的输出,如输出到数据库,可以使用Item Pipeline来完成。

9.使用Item Pipeline

Item pipeline 为项目管道,当Item生成后,它会自动被送到Item Pipiline进行处理,常用Item pipeline实现如下操作。

清理HTML数据

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

查重并对齐重复内容

将爬取结果保存到数据库

要实现Item pipeline,只需定义一个类并实现process_item()方法即可。启用Item pipeline后,Item pipeline会自动调用这个方法。process_item()方法必须返回包含数据的字典或Item对象。

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): #构造方法里定义限制长度为50
        self.limit=50
    def process_item(self,item,spider):
        if item['text']:#判断item的text属性书是否存在
            if (len(item['text'])>self.limit):#大于50,截断后拼接省略号,返回item即可
                item['text']=item['text'][0:self.limit].rstrip()+'...'
            return item
        else:#text属性不存在,就抛出异常
            return DropItem('Missing Text')

将处理逗得item存入mongoDB,定义另外一个类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')
        )
    #crawler()类方法,用 @classmethod标识,参数为crawler,通过crawler可以得到全局配置的每个配置信息。
    #在全局配置settings.py中,定义了MONGO_URI和MONGO_DB来指定mongoDB连接需要的地址和数据库名称,拿到配置信息之后,返回类对象即可

    def open_spider(self,spider):#当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):#当spider关闭时,被调用。将数据库的连接关闭
        self.client.close()

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

在settings.py中加入下列信息:

ITEM_PIPELINES={
    'tutorial.pipelines.TextPipeline': 300,
    'tutorial.pipelines.MongoPipeline': 400
}
#赋值ITEM_PIPELINES字典,键名是Pipeline的类名称,键值是调用优先级,值越小,越优先被调用
MONGO_URI='localhost'
MONGO_DB='tutorial

在执行爬取,命令

scrapy crawl quotes

爬取结束,MongoDB建立了一个tutorial的数据库,QuoteItem的表。

查看数据库数据

import pymongo
client=pymongo.MongoClient(host='localhost',port=27017)

db=client.tutorial
collection=db.QuoteItem
results=collection.find()
for res in results:
    print(res)

输出结果:

...
{'_id': ObjectId('5e26c23d847b30492259733a'), 'text': "“You may say I'm a dreamer, but I'm not the only o...", 'author': 'John Lennon', 'tags': ['beatles', 'connection', 'dreamers', 'dreaming', 'dreams', 'hope', 'inspirational', 'peace']}
{'_id': ObjectId('5e26c23d847b30492259733b'), 'text': '“I am free of all prejudice. I hate everyone equal...', 'author': 'W.C. Fields', 'tags': ['humor', 'sinister']}
{'_id': ObjectId('5e26c23d847b30492259733c'), 'text': "“The question isn't who is going to let me; it's w...", 'author': 'Ayn Rand', 'tags': []}
{'_id': ObjectId('5e26c23d847b30492259733d'), 'text': "“′Classic′ - a book which people praise and don't...", 'author': 'Mark Twain', 'tags': ['books', 'classic', 'reading']}
{'_id': ObjectId('5e26c23e847b30492259733e'), 'text': '“Anyone who has never made a mistake has never tri...', 'author': 'Albert Einstein', 'tags': ['mistakes']}
{'_id': ObjectId('5e26c23e847b30492259733f'), 'text': "“A lady's imagination is very rapid; it jumps from...", 'author': 'Jane Austen', 'tags': ['humor', 'love', 'romantic', 'women']}
...

   转载规则


《scrapy框架的使用》 White Spider 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录