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']}
...