Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
Scrapy官方架构图
各部件职能
- Scrapy Engine 控制数据流在系统组件中的流动,并回调相关动作事件
- Scheduler 从引擎接受request并入队,当引擎请求他们时返回request
- Downloader 获取页面数据并提供给引擎,而后提供给spider
- Spiders 用户定义的爬虫模块
- Item Pipeline 处理被spider提取出来的item,包括丢弃、验证和持久化等等
- Downloader middlewares 引擎及下载器之间的hook,处理Downloader传递给引擎的response,同时支持用户挂载自定义逻辑
- Spider middlewares 引擎及Spider之间的hook,处理spider的输入(response)和输出(items及requests),同时支持用户挂载自定义逻辑
数据流转
- 核心引擎从爬虫获取初始url,并生成一个Request任务投入Scheduler调度计划里
- 引擎向调度器请求一个新的Request爬取任务并转发给downloader下载器
- 下载器载入页面并返回一个Response响应给引擎
- 引擎将Response转发给Spider爬虫做 数据提取 和 搜索新的跟进地址
- 处理结果由引擎做分发:提取的数据 -> ItemPipeline管道,新的跟进地址Request -> 调度器
- 流程返回第二步循环执行,直至调度器中的任务被处理完毕
这里我们以爬取马蜂窝问答页面(www.mafengwo.cn/wenda)的文章为例,说明如何构建一个Scrapy爬虫
搭建环境,安装依赖
- #沙箱环境
- virtualenv mafengwoenv
- source mafengwoenv/bin/activate #进入沙箱环境
- #安装两个兼容包
- pip install cryptography ndg-httpsclient
-
- #pip安装本次爬虫项目依赖的包
- pip install scrapy #爬虫框架
- pip install pymongo #mongo引擎的python驱动
创建Scrapy项目
- #创建一个名为 mafengwo 的项目
- scrapy startproject mafengwo
将默认创建如下结构层次的项目
- mafengwo
- ├── mafengwo
- │ ├── __init__.py
- │ ├── items.py #定义抽象数据模型
- │ ├── pipelines.py #定义数据处理管道
- │ ├── settings.py #配置文件
- │ └── spiders #存放项目所有爬虫
- │ └── __init__.py
- └── scrapy.cfg
Item建模
为了结构化数据,我们需要定义爬取数据结构的抽象模型(严格的说,Item不是必须的,你也可以直接在spider中返回dict数据,但是使用Item能获得额外的数据验证机制)
在 mafengwo/mafengwo/items.py 文件中定义我们需要爬取的标题、作者、时间和内容属性:
- # -*- coding: utf-8 -*-
- import scrapy
-
- class WendaItem(scrapy.Item):
- title = scrapy.Field()
- author = scrapy.Field()
- time = scrapy.Field()
- content = scrapy.Field()
编写问答页爬虫主程序解析页面
- #从基础爬虫模板创建我们的爬虫
- scrapy genspider --template basic wenda www.mafengwo.cn/wenda
编辑生成的爬虫程序 mafengwo/mafengwo/spiders/wenda.py,完善我们的数据爬取逻辑:
- # -*- coding: utf-8 -*-
- import scrapy
- from mafengwo import items
-
- class WendaSpider(scrapy.Spider):
- name = "wenda"
- allowed_domains = ["www.mafengwo.cn"] #必须是和start_urls一致的域名,且不能跟上目录
- start_urls = [
- 'http://www.mafengwo.cn/wenda/',
- ]
-
- #框架默认的页面解析器入口,start_urls页面将被传入
- def parse(self, response):
- #遍历文章列表
- for link in response.xpath("//ul[@class='_j_pager_box']/li"):
- url = link.xpath("div[@class='wen']/div[@class='title']/a/@href").extract_first()
- url = response.url + url[7:] #详情页地址
- yield scrapy.Request(url, callback=self.parse_detail) #跟进详情页
- #当前页条目抓取完毕后,跟进下一页
- # next = response.xpath('xxx').extract_first()
- # if next:
- # yield scrapy.Request(next, self.parse)
-
-
- #我们自定义的详情页解析器
- def parse_detail(self, response):
- item = items.WendaItem()
- #抽取页面信息存入模型
- item['author'] = response.xpath("//div[@class='pub-bar fr']/a[@class='name']/text()").extract_first()
- item['title'] = response.xpath("//div[@class='q-title']/h1/text()").extract_first()
- item['time'] = response.xpath("//div[@class='pub-bar fr']/span[@class='time']/span/text()").extract_first()
- item['content'] = response.xpath("//div[@class='q-desc']/text()").extract_first()
-
- yield item
DOM调试
scrapy使用Scrapy Selectors(基于XPath 和 CSS )从网页提取数据
Selector有四个基本方法:
- xpath() 根据xpath规则返回所有节点的selector list
- css() 根据css规则返回所有节点的selector list
- extract() 序列化该节点为unicode字符串并返回list
- re() 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表
xpath简要:
- xpath分为
绝对路径
、相对路径
两种,并由路径表达式
组成:/
根节点//
匹配节点.
当前节点..
父节点
路径表达式
由步进表达式
组成(轴::节点测试[谓语]):- 轴(节点层级的相对关系):
preceding
、preceding-sibling
、self
、following-sibling
、following
、ancestor
、parent
、child
、attribute
... - 节点测试(节点匹配):
节点名
、*
、text()
、node()
... - 谓语(过滤):
[索引数字]
、[last()]
、[@class="hot"]
...
- 轴(节点层级的相对关系):
我们可以通过以下指令进入ScrapyShell来测试xpath规则
- scrapy shell --nolog 'http://www.mafengwo.cn/wenda/' #进入交互式工具
-
- >>>sel.xpath('/div/span') #shell中测试xpath
- >>>fetch("http://www.mafengwo.cn") #切换页面,将会刷新response等对象
我们也可以直接在爬虫解析器中嵌入一个钩子 scrapy.shell.inspect_response(response, self)
,从而在爬取过程中回调到ScrapyShell,并在当时特定场景下进行调试
特别的,对于xpath,我们也可以在浏览器console中通过如下方法来测试xpath
$x('规则')
parse解析器调试
我们可以通过以下命令来调试解析器对页面数据的分析情况
scrapy parse --spider=爬虫名 -c 解析器名 -d 跟进深度 -v 调试地址
数据处理管道Item Pipeline
当Item数据在Spider中被收集之后,它将会被传递到Item Pipeline,并按序执行所有管道 管道接收到Item后可以执行自定义逻辑,同时也决定此Item是否继续通过pipeline,或是被丢弃 管道典型的运用:
- 爬取结果持久化
- 清理HTML数据
- 验证爬取的数据
- 查重(并丢弃)
爬虫数据常用的持久化策略是mongo引擎:
- 它支持海量采集数据的录入
- 有很好的伸缩性拓展性,在后期数据变动调整字段的时候能最小化缩减开发成本。
下面我们通过一个mongo管道做爬虫数据的持久化
当然,你也可以直接将持久化逻辑写入爬虫主程序,但是ItemPipline中的持久化逻辑能避免低配IO对爬虫的阻塞
- # -*- coding: utf-8 -*-
- import pymongo
- from scrapy import exceptions
-
- class MongoPipeline(object):
- #不用事先创建mongo数据库、集合 和 定义文档,即插即用
- mongo_uri = 'localhost'
- mongo_database = 'mafengwo'
- collection_name = 'wenda_pages'
-
- def __init__(self, mongo_uri, mongo_db):
- self.mongo_uri = mongo_uri
- self.mongo_db = mongo_db
-
- #下面这个类方法定义了如何由Crawler对象创建这个管道实例
- #在这里我们可以通过crawler参数类似于 `crawler.settings.get()` 形式访问到诸如settings、signals等所有scrapy框架内核组件
- @classmethod
- def from_crawler(cls, crawler):
- return cls(cls.mongo_uri, cls.mongo_database)
-
- def open_spider(self, spider):
- self.client = pymongo.MongoClient(self.mongo_uri)
- self.collection = self.client[self.mongo_db][self.collection_name]
-
- def close_spider(self, spider):
- self.client.close()
-
- # 管道必须实现的一个方法,在此实现具体的持久化逻辑
- def process_item(self, item, spider):
- if (not item['title']):
- raise exceptions.DropItem('丢弃一个标题不存在页面')
- else:
- self.collection.insert(dict(item))
- return item
启用mongo持久化管道
我们可以在 mafengwo/mafengwo/settings.py 文件中写入配置
- ITEM_PIPELINES = {
- 'mafengwo.pipelines.MongoPipeline': 1, #数字确定了不同管道运行的先后顺序,从低到高
- }
但是为了不污染全局管道配置,我们把setting写入爬虫自配置中,即爬虫主程序的 custom_settings 属性中:
- class WendaSpider(scrapy.Spider):
- custom_settings = {
- 'ITEM_PIPELINES': {'mafengwo.pipelines.MongoPipeline': 1}
- }
运行我们的问答页爬虫
scrapy crawl wenda
当运行 scrapy爬虫模块时,scrapy尝试从中查找Spider的定义,并且在爬取引擎中运行它。 爬取启动后,scrapy首先依据模块的 start_urls 属性创建请求,并将请求的response作为参数传给默认回调函数 parse 。 在回调函数 parse中,我们可以产生(yield)更多的请求,并将响应传递给下一层次的回调函数。
mongo中查看数据
- mongo
- >show dbs
- >use mafengwo #切换数据库
- >show collections
- >db.wenda_pages.findOne()
- >db.wenda_pages.find().limit(3).pretty()
数据库中可以看到,问答页面数据已经成功抓取并录入了