爬虫框架之Scrapy(一)

scrapy简介

scrapy是一个用python实现为了爬取网站数据,提取结构性数据而编写的应用框架,功能非常的强大。

scrapy常应用在包括数据挖掘,信息处理或者储存历史数据的一系列程序中。

scrapy框架图

 

绿线是数据流向

Scrapy Engine(引擎):负责Spiders、Item Pipeline,Downloader、Scheduler中间的通信、信号和数据传递等。

Scheduler(调度器):负责接收引擎传递过来的requests请求,并按照一定的方式整理队列,入队,当引擎需要时,交还给引擎。

Downloader(下载器):负责下载引擎发送过的所有requests请求,将它获得的responses交还给引擎,由引擎交给Spiders处理。

Spiders(爬虫):负责处理所有的responses,从中分析提取数据,获得item字段需要的数据,并将需要跟进的URl提供给引擎,再次进入调度器。

Item Pipline(管道):它负责处理Spiders中获取到的item,并进行后期处理的地方(分析,过滤,储存等)。

Downloader Middlewares(下载中间件):可以当做一个自定义扩展下载功能的组件。

Spiders MIddlewares(爬虫中间件 ):可以理解为一个自定义扩展和操作引擎与Spiders中间通信功能的组件。(比如进入Spiders的responses和从Spiders出去的requests)

Scrapy的核心由引擎控制,其基本流程是这样的:

1 引擎打开一个域名,Spiders处理这个域名,并让Spiders处理第一个爬取获取的url。

2 引擎从Spiders那获取第一个需要爬取的URL,然后作为请求在调度中进行调度。

3 引擎从调度那获取接下来进行爬取的页面。

4 调度将下一个爬取的URL返回给引擎,引擎将他们通过下载中间件发送到下载器。

5 当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎。

6 引擎收到下载器的响应并将它通过Spiders中间件发送到Spiders进行处理。

7 Spiders处理响应并返回爬取到的item,然后给引擎发送新的请求。

8 引擎发送处理后的item到项目管道,然后把处理结果返回给调度器,调度器计划处理下一个请求抓取。

9 系统重复2-9的操作,直到调度中没有请求,然后断开引擎与域之间的联系。

制作Scrapy一般需要4个步骤:

1 新建项目(scrapy startproject xxx):创建一个爬虫项目

2 明确目标(编写items.py):明确要抓取的目标

3 制作爬虫(spiders/xxxspiders.py):制作爬虫开始网页爬取

4 储存内容(pipelines.py):设计管道储存爬取内容

安装创建一个爬虫项目

我使用的pycharm,并且创建了虚拟环境。

查看新环境的package

在新的环境中安装scrapy,在这里安装的是1.52版本

打开terminal,创建新的爬虫项目

scrapy startproject happy

 创建了一个新的爬虫项目,可以看下它的目录结构

这些文件分别是:

scrapy.cfg:项目的主配置信息(真正的配置信息在setting.py中)

items.py:项目的目标文件,设置数据储存模板,用于数据结构化,比较行Django的model

middlewares.py :中间件文件,设计中间件

pipelines.py : 管道文件,数据的持久化处理

setting.py :配置文件,比如递归的层数,并发数,延迟下载等。

spiders:爬虫目录,创建文件,编写爬虫解析规则

新创建一个爬虫程序

1 cd 项目名称

2 scrapy genspider 应用名称 爬取网页的起始url (例如:scrapy genspider qiushi www.qiushibaike.com)

比如按照上面项目要输如的:

1 cd happy

2 scrapy genspider qiushi www.qiushibaike.com

发现项目创建了一个qiushi.py的文件

解读下这段代码:

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


class QiushiSpider(scrapy.Spider):
    # 应用名称
    name = 'qiushi'
    # 允许爬取的域名,如果遇到非该域名的url则爬取不到数据 一般会把它给注释掉
    allowed_domains = ['www.qiushibaike.com']
    # 起始爬取的url
    start_urls = ['http://www.qiushibaike.com/']
    '''
    访问起始的url并获取及结果后的回调函数,该函数的responses参数就是对起始url发送请求后获得的响应对象
    该函数的返回必须为可迭代对象或者null
    '''
    def parse(self, response):
        pass

 3 修改settings.py

19行
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36'
# 伪装请求载体身份
22 行
不遵守robots协议
ROBOTSTXT_OBEY = False

4 执行爬虫程序

第一种方式:

scrapy crawl 应用名称 # 该种执行方式会显示执行的日志

scrapy crawl 应用名称 --nolog # 该种执行方式不会显示执行的日志信息

第二种方式:

在最外层的project_name文件下新建一个py文件,名字随便写,在文件里写入:

从此以后,只要执行start.py文件,就可以让程序跑起来

 初始实例

将糗百首页中段子的内容和标题进行爬取

qiushi.py

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


class QiushiSpider(scrapy.Spider):
    name = 'qiushi'
    allowed_domains = ['www.qiushibaike.com']
    start_urls = ['http://www.qiushibaike.com/']
    def parse(self, response):
        # xpath为response中的方法
        odiv = response.xpath('//div[@class="recommend-article"]//li')
        print(odiv)
        con_lst = []
        for li in odiv:
            # 连续的li里面混有广告,屏蔽掉广告
            try:
                author = li.xpath('.//div[@class="recmd-right"]/a/text()')[0].extract()
                title = li.xpath('.//span[@class="recmd-name"]/text()')[0].extract()
                dic = {'标题':title, '作者':author}
                con_lst.append(dic)
            except:
                continue
        return con_lst

 将其保存为xml格式,运行可以看到

<?xml version="1.0" encoding="utf-8"?>
<items>
<item><标题>尛鑫惢</标题><作者>你是真的皮</作者></item>
<item><标题>金小莲</标题><作者>哥,嫂子说原谅你了,回去吧!</作者></item>
<item><标题>逗太兔</标题><作者>女朋友生气的样子</作者></item>
<item><标题>聊天不撩...</标题><作者>公司车库门口这几天一直大黄蜂在飞,原来是靠近门边有个蜂巢,我让老张去拿个挮子,把蜂巢处理了。老张去搬梯子,我回去拿了二个头盔,然后两人全付武装起来了。梯子架好,</作者></item>
<item><标题>湘雅婷</标题><作者>绳子想挽救你一把,可惜拽的不是地方啊,绳子本想拽你脖子的</作者></item>
<item><标题>我打豆豆...</标题><作者>我驾照是98年。教练车是东风141那种老款大货车。刚学大家离合都用不好老是灭车,教练怕车亏电就让用摇把摇车。女生摇不动都是男生替。有天有个女孩熄火了,一个男生就</作者></item>
<item><标题>许诺的蛋...</标题><作者>智障女友的智障日常非要捉迷藏,我不去找,给我教育了一顿,让我深刻的检讨了下自己的问题。是我错了!</作者></item>
<item><标题>糗百精华...</标题><作者>游着游着就爱了</作者></item>
<item><标题>夜半无人...</标题><作者>就这就是我们要的英伦乡村贵族爱情</作者></item>
<item><标题>零點糗事</标题><作者>真搞不懂,哥们为什么大婚当日就带着老婆去荒岛上了</作者></item>
<item><标题>.遇见更...</标题><作者>爸妈吵架,老爸一怒之下把电视砸了,老妈也把微波炉砸了,没几天就和好了。我问老爸当时干嘛砸电视,老爸说,早想换一台大的了,你妈一直不肯。我后来问老妈,干嘛砸微波炉</作者></item>
<item><标题>有什么呀...</标题><作者>谈谈感受吧</作者></item>
<item><标题>零點糗事</标题><作者>网友盘点世界上最聪明的狗狗,看完你是不是这么觉得</作者></item>
<item><标题>尛鑫惢</标题><作者>校长说经费有限……</作者></item>
<item><标题>欢乐二哈</标题><作者>你们怕老婆也不用这样吧!</作者></item>
<item><标题>砸妳家玻...</标题><作者>宁错怪好人,也不错放坏人</作者></item>
<item><标题>又一盏素...</标题><作者>小丽说“男友一两个月不主动联系我,是几个意思?”她其中一个朋友说“大家回答得都太阴暗,什么劈腿变心从来没爱过你之类的,凡事要往好的方面想。我猜你男朋友应该是死了</作者></item>
<item><标题>蜀南熟男</标题><作者>狗狗:TMD居然会功夫,老子不打了!</作者></item>
<item><标题>包袱侠</标题><作者>在俄罗斯,狮子被分配在可爱动物触摸区</作者></item>
<item><标题>笨小孩(...</标题><作者>中国武侠小说三大宗师陨落殆尽,从此,再无江湖,亦无武侠。飞雪连天射白鹿,笑书神侠倚碧鸳。金老走好!</作者></item>
</items>

 Scrapy中selenium的使用

在通过Scrapy框架进行某些网站的数据爬取的时候,往往会碰到页面数据动态加载的情况发生,如果直接使用Scrapy对其url发送请求,是绝对获取不到动态加载出来的数据,但是浏览器进行url发送会加载出对应的动态数据。

如果我想要在scrapy也获取动态加载出的数据,则必须使用selenium创建浏览器对象,然后通过该浏览器对象进行请求发送,获取动态加载的数据值。

爬取网易新闻国内板块

https://news.163.com/domestic/

分析:当点击国内超链进入国内对应的页面时,会发现当前页面展示的新闻数据是被动态加载出来的,如果直接通过程序对url进行请求,是获取不到动态加载出的新闻数据的。则就需要我们使用selenium实例化一个浏览器对象,在该对象中进行url的请求,获取动态加载的新闻数据。

通过上面的Scra流程可以分析出:

1 当引擎将国内板块url对应的请求提交给下载器后,下载器进行网页数据的下载,然后将下载到的页面数据,封装到response中,提交给引擎,引擎将response在转交给Spiders。

2 Spiders接受到的response对象中存储的页面数据里是没有动态加载的新闻数据的。

3 要想获取动态加载的新闻数据,则需要在下载中间件中对下载器提交给引擎的response响应对象进行拦截,切对其内部存储的页面数据进行篡改,修改成携带了动态加载出的新闻数据,然后将被篡改的response对象最终交给Spiders进行解析操作。

selenium在Scrapy中的使用流程

1 重写爬虫文件的构造方法,在方法中使用selenium实例化一个浏览器对象。(因为浏览器对象只需要被实例化一次)

2 重写爬虫文件的close方法,在其内部关闭浏览器,该方法在爬虫结束时被调用。

3 重写下载中间件的process_response,让该方法对响应进行拦截,并篡改response中存储的页面数据

4 在配置文件中开启下载中间件

代码示例

wangyi.py

class WangyiSpider(scrapy.spider):
    name = 'wangyi'
    #allowed_domains = ['www.xxxx.com']
    start_urls = ['https://news.163.com']
    def __init__(self):
        #实例化一个浏览器对象(实例化一次)
        self.bro = webdriver.Chrome(executable_path='/Users/bobo/Desktop/chromedriver')

    #必须在整个爬虫结束后,关闭浏览器
    def closed(self,spider):
        print('爬虫结束')
        self.bro.quit()

中间件文件:

from scrapy.http import HtmlResponse    
    #拦截到响应对象(下载器传递给Spider的响应对象)
    #request:响应对象对应的请求对象
    #response:拦截到的响应对象
    #spider:爬虫文件中对应的爬虫类的实例
    def process_response(self, request, response, spider):
        #响应对象中存储页面数据的篡改
        if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']:
            spider.bro.get(url=request.url)
            js = 'window.scrollTo(0,document.body.scrollHeight)'
            spider.bro.execute_script(js)
            time.sleep(2)  #一定要给与浏览器一定的缓冲加载数据的时间
            #页面数据就是包含了动态加载出来的新闻数据对应的页面数据
            page_text = spider.bro.page_source
            #篡改响应对象
            return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request)
        else:
            return response 

 settings.py

DOWNLOADER_MIDDLEWARES = {
   'happy1.middlewares.Happy1DownloaderMiddleware': 543,
}

scrapy框架的持久化存储

基于终端指令的持久化存储

条件:保证parse文件中有可迭代类型对象的返回,该返回值可以通过终端指令的方式写入指定格式的文件中进行持久化操作。

指定输出格式进行存储    
scrapy crawl 爬虫名称 -o xxx.json
scrapy crawl 爬虫名称
-o xxx.xml scrapy crawl 爬虫名称 -o xxx.csv
基于管道的持久化存储

主要作用的两个文件:

  1 iems.py:数据结构模板文件,主要定义数据属性。

  2 pipelines.py:管道文件,接收数据,进行持久化操作。

持久化操作流程:

  1 爬虫文件爬取到数据后,需要将数据封装到item对象中

  2 使用yield关键字将items对象提供给pipelines对象进行持久化操作

  3 在管道文件中的process_item方法中接收爬虫文件提供过来的items对象,然后编写代码进行持久化存储

  4 设置settings.py

例子 上面糗事百科 本地存储

qiushi.py

# -*- coding: utf-8 -*-
import scrapy
from happy.items import HappyItem

class QiushiSpider(scrapy.Spider):
    name = 'qiushi'
    allowed_domains = ['www.qiushibaike.com']
    start_urls = ['http://www.qiushibaike.com/']
    def parse(self, response):
        # xpath为response中的方法
        odiv = response.xpath('//div[@class="recommend-article"]//li')
        for li in odiv:
            # 连续的li里面混有广告,屏蔽掉广告
            try:
                title = li.xpath('.//div[@class="recmd-right"]/a/text()')[0].extract()
                author = li.xpath('.//span[@class="recmd-name"]/text()')[0].extract()

            except:
                continue
            # 提供item到管道文件
            item = HappyItem()
            item['title'] = title
            item['author'] = author
            yield item

items.py

import scrapy


class HappyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 储存标题
    title = scrapy.Field()
    # 储存作者
    author = scrapy.Field()

pipelines.py

class HappyPipeline(object):
    # 构造方法
    def __init__(self):
        # 定义一个文件描述符属性
        self.fp = None

    # 下面都是重写父类的方法
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        print('爬虫开始!')
        self.fp = open('data.txt', 'w', encoding='utf-8')

    # 因为该方法会被调用多次,所以文件的开启和关闭都被写在了另外2个只会执行一次的方法中
    def process_item(self, item, spider):
        self.fp.write(item['title'] + ':' + item['author'] + '\n')
        return item

    #爬虫结束时执行一次
    def close_spider(self, spider):
        self.fp.close()
        print('爬虫结束')

settings.py

ITEM_PIPELINES = {
   'happy.pipelines.HappyPipeline': 300,
}

例子 上面糗事百科 mysql存储

新建数据库qiushi和表qiushi,我使用的是navicat

 修改pipelines.py

import pymysql

class QiubaiPipelineByMysql(object):
    # mysql的连接对象声明
    con = None
    # 右表声明
    cursor = None
    # 下面都是重写父类的方法
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        print('爬虫开始!')
        # 连接数据库
        self.con = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123456', db='qiushi')

    # 因为该方法会被调用多次,所以文件的开启和关闭都被写在了另外2个只会执行一次的方法中
    def process_item(self, item, spider):
        # sql语句
        sql = 'insert into qiushi values("%s","%s")'%(item['title'], item['author'])
        # 创建游标
        self.cursor = self.con.cursor()
        # 执行事务
        try:
            self.cursor.execute(sql)
            self.con.commit()
        except Exception as e:
            print(e)
            # 事务回滚
            self.con.rollback()
        return item

    #爬虫结束时执行一次
    def close_spider(self, spider):
        # 关闭连接
        self.cursor.close()
        # 关闭游标
        self.con.close()
        print('爬虫结束')

修改settings.py 把最后的类名字换了

ITEM_PIPELINES = {
   'happy.pipelines.QiubaiPipelineByMysql': 300,
}

得到结果: 

例子 糗事百科 redis存储

我使用的是redis的可视化软件

pipelines.py

import redis

class QiubaiPipelineByRedis(object):
    # redis的连接对象声明
    con = None
    # 下面都是重写父类的方法
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        print('爬虫开始!')
        # 连接数据库
        self.con = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True)

    # 因为该方法会被调用多次,所以文件的开启和关闭都被写在了另外2个只会执行一次的方法中
    def process_item(self, item, spider):
        dic = {'title':item['title'], 'author':item['author']}
        self.con.lpush('data', str(dic))
        return item

    #爬虫结束时执行一次
    def close_spider(self, spider):
        print('爬虫结束')

settings.py

ITEM_PIPELINES = {
   'happy.pipelines.QiubaiPipelineByRedis': 300,
}

得到结果:

 

持久化存储扩展

如果最终需要将爬取到的数据值一份存储到磁盘文件,一份存储到mysql数据库中,一份存储到redis中该,该如何操作?

pipelines.py中的操作:

# redis操作
class QiubaiPipelineByRedis(object):
    pass
#mysql操作
class QiubaiPipelineByMysql(object):
    pass
#本地操作
class HappyPipeline(object):
    pass

 settings.py的操作:

ITEM_PIPELINES = {
   'happy.pipelines.QiubaiPipelineByRedis': 300,
   'happy.pipelines.QiubaiPipelineByMysql': 300,
   'happy.pipelines.HappyPipeline': 300,
}

 

posted @ 2019-04-23 13:09  噼里巴啦  阅读(545)  评论(0编辑  收藏  举报