Scrapy + Mongo 构建一个网页爬虫

左心房为你撑大大i 提交于 2019-12-06 05:57:57

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),同时支持用户挂载自定义逻辑

数据流转

  1. 核心引擎从爬虫获取初始url,并生成一个Request任务投入Scheduler调度计划里
  2. 引擎向调度器请求一个新的Request爬取任务并转发给downloader下载器
  3. 下载器载入页面并返回一个Response响应给引擎
  4. 引擎将Response转发给Spider爬虫做 数据提取 和 搜索新的跟进地址
  5. 处理结果由引擎做分发:提取的数据 -> ItemPipeline管道,新的跟进地址Request -> 调度器
  6. 流程返回第二步循环执行,直至调度器中的任务被处理完毕

这里我们以爬取马蜂窝问答页面(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有四个基本方法

  1. xpath() 根据xpath规则返回所有节点的selector list
  2. css() 根据css规则返回所有节点的selector list
  3. extract() 序列化该节点为unicode字符串并返回list
  4. re() 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表

xpath简要

  • xpath分为 绝对路径相对路径 两种,并由 路径表达式 组成:
    • /根节点
    • //匹配节点
    • .当前节点
    • ..父节点
  • 路径表达式步进表达式 组成(轴::节点测试[谓语]):
    • 轴(节点层级的相对关系):precedingpreceding-siblingselffollowing-siblingfollowingancestorparentchildattribute...
    • 节点测试(节点匹配):节点名*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引擎:

  1. 它支持海量采集数据的录入
  2. 有很好的伸缩性拓展性,在后期数据变动调整字段的时候能最小化缩减开发成本。

下面我们通过一个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()

数据库中可以看到,问答页面数据已经成功抓取并录入了

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!