Python网络爬虫 第七章 Scrapy框架

一、Scrapy简介

什么是框架?
所谓的框架简单通用解释就是就是一个具有很强通用性并且集成了很多功能的项目模板,该模板可被应用在不同的项目需求中。也可被视为是一个项目的半成品。
如何学习框架?
对于刚接触编程或者初级程序员来讲,对于一个新的框架,只需要掌握该框架的作用及其各个功能的使用和应用即可,对于框架的底层实现和原理,在逐步进阶的过程中在慢慢深入即可。
什么是scrapy?
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍。其内部已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)。对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可。

1、Scrapy环境安装

linux和mac操作系统:

pip install scrapy

windows系统:

pip install wheel

下载twisted,下载地址为http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
安装twisted:找到对应版本的Twisted

pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl
pip install pywin32
pip install scrapy

测试:在终端里录入scrapy指令,没有报错即表示安装成功!

2、Scrapy使用流程

创建工程:

scrapy startproject ProName

进入工程目录:

cd ProName

创建爬虫文件:

scrapy genspider spiderName www.xxx.com 后期可以修改

编写相关操作代码
执行工程:

scrapy crawl spiderName

爬虫文件剖析:

      # -*- coding: utf-8 -*-
      import scrapy
      class QiubaiSpider(scrapy.Spider):
          name = 'qiubai' #应用名称
          #允许爬取的域名(如果遇到非该域名的url则爬取不到数据)一般不用
          allowed_domains = ['https://www.qiushibaike.com/']
          #起始爬取的url
          start_urls = ['https://www.qiushibaike.com/']
          #访问起始URL并获取结果后的回调函数,该函数的response参数就是向起始的url发送请求后,获取的响应对象.该函数返回值必须为可迭代对象或者NUll 
          def parse(self, response):
              print(response.text) #获取字符串类型的响应内容
              print(response.body)#获取字节类型的相应内容

配置文件settings.py修改:

      # 修改内容及其结果如下:
      # 19行:
      USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' #伪装请求载体身份
      # 22行:
      ROBOTSTXT_OBEY = False  #可以忽略或者不遵守robots协议

scrapy基于xpath数据解析操作:
爬取糗事百科的段子数据:https://www.qiushibaike.com/

   # -*- coding: utf-8 -*-
    import scrapy
    class QiubaiSpider(scrapy.Spider):
        name = 'qiubai'
        allowed_domains = ['https://www.qiushibaike.com/']
        start_urls = ['https://www.qiushibaike.com/']

        def parse(self, response):
            # xpath为response中的方法,可以将xpath表达式直接作用于该函数中
            odiv = response.xpath('//*[@id="content"]/div/div[2]/div')
            content_list = []  # 用于存储解析到的数据
            for div in odiv:
                # xpath函数返回的为列表,列表中存放的数据为Selector类型的数据。                # 我们解析到的内容被封装在了Selector对象中,需要调用extract()函数将解析的内容从Selecor中取出。
                author = div.xpath('.//div[@class="author clearfix"]/a/h2/text()')[0].extract()
                content = div.xpath('.//div[@class="content"]/span/text()')[0].extract()
                # 打印展示爬取到的数据
                print(author, content)

二、Scrapy的数据持久化存储

两种持久化存储方式

1、基于终端指令的持久化存储操作

  • 要求:只可以将parse方法的返回值存储到本地的文本文件中
  • 注意:持久化存储对应的文本文件的类型只可以为:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle
  • 指令:scrapy crawl xxx -o filePath
  • 好处:简洁高效便捷
  • 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
   # 基于终端指令持久化存储
    # scrapy crawl qiubai -o ./qiubai.csv
    def parse(self, response):
        #解析:作者的名称+段子内容
        div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
        all_data = [] # 列表 存储所有解析到的数据 存字典
        for div in div_list:
            #xpath返回的是列表,但是列表元素一定是Selector类型的对象
            #extract可以将Selector对象中data参数存储的字符串提取出来
            # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
            #列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
            content = div.xpath('./a[1]/div/span//text()').extract()
            content = ''.join(content)
            # 定义字典
            dic = {
                'author':author,
                'content':content
            }
            all_data.append(dic)
        return all_data

执行指令:

  • 执行输出指定格式进行存储:将爬取到的数据写入不同格式的文件中进行存储
    scrapy crawl 爬虫名称 -o xxx.json
    scrapy crawl 爬虫名称 -o xxx.xml
    scrapy crawl 爬虫名称 -o xxx.csv

2、基于管道的持久化存储操作

scrapy框架中已经为我们专门集成好了高效、便捷的持久化操作功能,我们直接使用即可。要想使用scrapy的持久化操作功能,我们首先来认识如下两个文件:

items.py:数据结构模板文件。定义数据属性。

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

持久化流程:

  1. 爬虫文件爬取到数据后,需要将数据封装到items对象中,在item类中定义相关的属性
  2. 使用yield关键字将items对象提交给pipelines管道进行持久化操作。
  3. 在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
  4. settings.py配置文件中开启管道

爬虫文件:qiubaiDemo.py

import scrapy
from qiubaiPro.items import QiubaiproItem

class QiubaiSpider(scrapy.Spider):
    name = 'qiubai'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.qiushibaike.com/text/']
    def parse(self, response):
        # 解析:作者的名称+段子内容
        div_list = response.xpath('//*[@id="content"]/div/div[2]/div')

        for div in div_list:
            # xpath返回的是列表,但是列表元素一定是Selector类型的对象 取字符串要[0]
            # extract可以将Selector对象中data参数存储的字符串提取出来
            # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
            # 匿名用户的xpath./div[1]/span/h2/text()
            author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()
            # 使用extract_first() 需要保证返回列表中只有一个元素
            # 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
            content = div.xpath('./a[1]/div/span//text()').extract()
            content = ''.join(content)  # 列表转字符串

            item = QiubaiproItem()
            item['author'] = author
            item['content'] = content
            print(item)
            yield item  # 将item提交给了管道

items文件:items.py

    import scrapy
    class SecondbloodItem(scrapy.Item):
      # define the fields for your item here like:
      # name = scrapy.Field()
      author = scrapy.Field() #存储作者
      content = scrapy.Field() #存储段子内容

管道文件:pipelines.py

    class SecondbloodPipeline(object):
      #构造方法
      def __init__(self):
          self.fp = None  #定义一个文件描述符属性
      #下列都是在重写父类的方法:
      #开始爬虫时,执行一次
      def open_spider(self,spider):
          print('爬虫开始')
          self.fp = open('./data.txt', 'w')
      #因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
      def process_item(self, item, spider):
          #将爬虫程序提交的item进行持久化存储
          self.fp.write(item['author'] + ':' + item['content'] + '\n')
          return item
      #结束爬虫时,执行一次
      def close_spider(self,spider):
          self.fp.close()
          print('爬虫结束')

配置文件:settings.py

    #开启管道
    ITEM_PIPELINES = {
    'secondblood.pipelines.SecondbloodPipeline': 300, #300表示为优先级,值越小优先级越高
    }

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

管道文件中的代码为

      #该类为管道类,该类中的process_item方法是用来实现持久化存储操作的。
      class DoublekillPipeline(object):
          def process_item(self, item, spider):
              #持久化操作代码 (方式1:写入磁盘文件)
              return item
      #如果想实现另一种形式的持久化操作,则可以再定制一个管道类:
      class DoublekillPipeline_db(object):
          def process_item(self, item, spider):
              #持久化操作代码 (方式2:写入数据库)
              return item

管道文件中一个管道类对应的是将数据存储到一种平台
爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
process_item中的return item表示将item传递给下一个即将被执行的管道类

在settings.py开启管道操作代码为:

    #下列结构为字典,字典中的键值表示的是即将被启用执行的管道文件和其执行的优先级。数越小优先级越高
    ITEM_PIPELINES = {
     'doublekill.pipelines.DoublekillPipeline': 300,
     'doublekill.pipelines.DoublekillPipeline_db': 200,
    }
    #上述代码中,字典中的两组键值分别表示会执行管道文件中对应的两个管道类中的process_item方法,实现两种不同形式的持久化操作。

三、基于Spider类的全站数据爬取

大部分的网站展示的数据都进行了分页操作,那么将所有页码对应的页面数据进行爬取就是爬虫中的全站数据爬取。
基于scrapy如何进行全站数据爬取呢?

  • 将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。(不推荐)
  • 使用Request方法手动发起请求。(推荐)

需求:将糗事百科所有页码的作者和段子内容数据进行爬取切持久化存储

class QiubaiSpider(scrapy.Spider):
    name = 'qiubai'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.qiushibaike.com/text/']

    # 爬取多页
    pageNum = 1  # 起始页码
    url = 'https://www.qiushibaike.com/text/page/%s/'  # 每页的url

    def parse(self, response):
        # 解析:作者的名称+段子内容
        div_list = response.xpath('//*[@id="content"]/div/div[2]/div')

        for div in div_list:
            # xpath返回的是列表,但是列表元素一定是Selector类型的对象 取字符串要[0]
            # extract可以将Selector对象中data参数存储的字符串提取出来
            # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
            # 匿名用户的xpath./div[1]/span/h2/text()
            author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()
            # 使用extract_first() 需要保证返回列表中只有一个元素
            # 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
            content = div.xpath('./a[1]/div/span//text()').extract()
            content = ''.join(content)  # 列表转字符串

            item = QiubaiproItem()
            item['author'] = author
            item['content'] = content
            print(item)
            yield item  # 将item提交给了管道

        # 爬取所有页码数据
        if self.pageNum <= 13:  # 一共爬取13页(共13页)
            self.pageNum += 1
            url = format(self.url % self.pageNum)
            # 递归爬取数据:callback参数的值为回调函数(将url请求后,得到的相应数据继续进行parse解析),递归调用parse函数
            yield scrapy.Request(url=url, callback=self.parse)
  • 大部分的网站展示的数据都进行了分页操作,那么将所有页码对应的页面数据进行爬取就是爬虫中的全站数据爬取。
  • 基于scrapy如何进行全站数据爬取呢?
    • 将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。(不推荐)
    • 使用Request方法手动发起请求。(推荐)
  • 需求:将糗事百科所有页码的作者和段子内容数据进行爬取切持久化存储

四、Scrapy五大核心组件简介

Scrapy的基本使用我们已经掌握,但是各位心中一定会有些许的疑问,我们在编写scrapy工程的时候,我们只是在定义相关类中的属性或者方法,但是我们并没有手动的对类进行实例化或者手动调用过相关的方法,那么这些操作都是谁做的呢?接下来我们就来看看Scrapy的五大核心组件的工作流程,然后大家就会上述的疑问有基本了解了。

img

  • 引擎(Scrapy)
    • 用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    • 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    • 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    • 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    • 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特\定的次序处理数据。

img

五、请求传参

在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中。这时我们就需要用到请求传参。
请求传参的使用场景:当我们使用爬虫爬取的数据没有存在于同一张页面的时候,则必须使用请求传参
这里以Boss直聘网站为例,python程序员的岗位名称和岗位描述分属两页。

img

    # -*- coding: utf-8 -*-
    import scrapy
    from bossPro.items import BossproItem
    class BossSpider(scrapy.Spider):
        name = 'boss'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position=']
        url = 'https://www.zhipin.com/c101010100/?query=python&page=%d'
        page_num = 2
       #回调函数接受item
        def parse_detail(self,response):
            item = response.meta['item']
            job_desc = response.xpath('//*[@id="main"]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract()
            job_desc = ''.join(job_desc)
            # print(job_desc)
            item['job_desc'] = job_desc
            yield item
        #解析首页中的岗位名称
        def parse(self, response):
            li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li')
            for li in li_list:
                item = BossproItem()
                job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first()
                item['job_name'] = job_name
                # print(job_name)
                detail_url = 'https://www.zhipin.com'+li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first()
                #对详情页发请求获取详情页的页面源码数据
                #手动请求的发送
                #请求传参:meta={},可以将meta字典传递给请求对应的回调函数
                yield scrapy.Request(detail_url,callback=self.parse_detail,meta={'item':item})
            #分页操作
            if self.page_num <= 3:
                new_url = format(self.url%self.page_num)
                self.page_num += 1
                yield scrapy.Request(new_url,callback=self.parse)

六、Scrapy图片数据爬取

在scrapy中我们之前爬取的都是基于字符串类型的数据,那么要是基于图片数据的爬取,那又该如何呢?

其实在scrapy中已经为我们封装好了一个专门基于图片请求和持久化存储的管道类ImagesPipeline,那也就是说如果想要基于scrapy实现图片数据的爬取,则可以直接使用该管道类即可。

ImagesPipeline使用流程

在配置文件中进行如下配置:

# 指定图片存储的目录
IMAGES_STORE = './imgs'

以站长素材网为例

spiders

# -*- coding: utf-8 -*-
import scrapy
from imgsPro.items import ImgsproItem


class ImgSpider(scrapy.Spider):
    name = 'img'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://sc.chinaz.com/tupian/']

    def parse(self, response):
        div_list = response.xpath('//div[@id="container"]/div')
        for div in div_list:
            # 注意:使用伪属性 图片懒加载 动态加载 选择src2
            #
            src = div.xpath('./div/a/img/@src2').extract_first()

            item = ImgsproItem()
            item['src'] = src

            yield item

管道类的编写:

      from scrapy.pipelines.images import ImagesPipeline
      import scrapy
      class ImgproPipeline(object):
          item = None
          def process_item(self, item, spider):
              # print(item)
              return item
      #ImagesPipeline专门用于文件下载的管道类,下载过程支持异步和多线程
      class ImgPipeLine(ImagesPipeline):          # 重写三个方法          # 就是可以根据图片地址进行图片数据的请求
          # 对item中的图片进行请求操作
          def get_media_requests(self, item, info):
              yield scrapy.Request(item['src'])
          #定制图片的名称
          def file_path(self, request, response=None, info=None):
              url = request.url
              file_name = url.split('/')[-1]
              return file_name
          def item_completed(self, results, item, info):
              return item  #该返回值会传递给下一个即将被执行的管道

七、中间件

  • 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件。
  • 作用:我们主要使用下载中间件处理请求,一般会对请求设置随机的User-Agent ,设置随机的代理。目的在于防止爬取网站的反爬虫策略。
    • (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系列处理。比如设置请求的 User-Agent,设置代理等
    • (2)在下载器完成将Response传递给引擎中,下载中间件可以对响应进行一系列处理。比如进行gzip解压等。

1、UA池:User-Agent池

  • 作用:尽可能多的将scrapy工程中的请求伪装成不同类型的浏览器身份。
  • 操作流程:
    • 1.在下载中间件中拦截请求
    • 2.将拦截到的请求的请求头信息中的UA进行篡改伪装
    • 3.在配置文件中开启下载中间件
  • UA池的封装:

2、代理池

  • 作用:尽可能多的将scrapy工程中的请求的IP设置成不同的。
  • 操作流程:
    • 1.在下载中间件中拦截请求
    • 2.将拦截到的请求的IP修改成某一代理IP
    • 3.在配置文件中开启下载中间件
import random


class MiddleproDownloaderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.
    user_agent_list = [
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
        "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
        "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
        "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
        "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
        "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
        "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
        "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
        "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
        "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
        "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
        "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
        "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
        "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
    ]
    # http类型代理
    PROXY_http = [
        '153.180.102.104:80',
        '195.208.131.189:56055',
    ]
    # https类型代理
    PROXY_https = [
        '120.83.49.90:9000',
        '95.189.112.214:35508',
    ]

    # 拦截请求
    def process_request(self, request, spider):
        # UA伪装 随机选择
        request.headers['User-Agent'] = random.choice(self.user_agent_list)

        # 为了验证代理的操作是否生效
        request.meta['proxy'] = 'http://183.146.213.198:80'
        return None

    # 拦截所有的响应
    def process_response(self, request, response, spider):
        # Called with the response returned from the downloader.

        # Must either;
        # - return a Response object
        # - return a Request object
        # - or raise IgnoreRequest
        return response

    # 拦截发生异常的请求
    def process_exception(self, request, exception, spider):
        
        if request.url.split(':')[0] == 'http':
            # 用http代理
            request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
        else:
            # 用https代理
            request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)

        return request  # 将修正之后的请求对象进行重新的请求发送

八、Scrapy中selenium的应用

selenium作为Web应用程序测试工具在前面的文章已经提到过了,https://www.cnblogs.com/wkfvawl/p/14756413.html

这里讲一下selenium与Scrapy框架结合的使用。

1、应用背景

在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值。但是通过观察我们会发现,通过浏览器进行url请求发送则会加载出对应的动态加载出的数据。那么如果我们想要在scrapy也获取动态加载出的数据,则必须使用selenium创建浏览器对象,然后通过该浏览器对象进行请求发送,获取动态加载的数据值。

2、案例分析

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

3、原理分析

当引擎将国内板块url对应的请求提交给下载器后,下载器进行网页数据的下载,然后将下载到的页面数据,封装到response中,提交给引擎,引擎将response在转交给Spiders。Spiders接受到的response对象中存储的页面数据里是没有动态加载的新闻数据的。要想获取动态加载的新闻数据,则需要在下载中间件中对下载器提交给引擎的response响应对象进行拦截,对其内部存储的页面数据进行篡改,修改成携带了动态加载出的新闻数据,然后将被篡改的response对象最终交给Spiders进行解析操作。

selenium在scrapy中的使用流程:

  • 重写爬虫文件的构造方法,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次)
  • 重写爬虫文件的closed(self,spider)方法,在其内部关闭浏览器对象。该方法是在爬虫结束时被调用
  • 重写下载中间件的process_response方法,让该方法对响应对象进行拦截,并篡改response中存储的页面数据
  • 在配置文件中开启下载中间件

爬虫文件:

import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem

class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['www.cccom']
    start_urls = ['https://news.163.com/']
    models_urls = []  # 存储四个板块对应详情页的url

    # 解析四大板块对应详情页的url

    # 实例化一个浏览器对象 
    def __init__(self):
        # spider类中的浏览器对象
        self.bro = webdriver.Chrome(executable_path='D:/anaconda/chromedriver.exe')

    def parse(self, response):
        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
        # 国内 国际 军事 航空
        alist = [3, 4, 6, 7]
        for index in alist:
            # a标签下的href属性值
            model_url = li_list[index].xpath('./a/@href').extract_first()
            self.models_urls.append(model_url)

        # 依次对每一个板块对应的页面进行请求
        for url in self.models_urls:  # 对每一个板块的url进行请求发送
            yield scrapy.Request(url, callback=self.parse_model)

    # 每一个板块对应的新闻标题相关的内容都是动态加载

    def parse_model(self, response):  # 解析每一个板块页面中对应新闻的标题和新闻详情页的url
        # response.xpath()
        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
        for div in div_list:
            # 新闻标题
            title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
            # 详情页链接
            new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()

            item = WangyiproItem()
            item['title'] = title

            # 对新闻详情页的url发起请求
            yield scrapy.Request(url=new_detail_url, callback=self.parse_detail, meta={'item': item})

    def parse_detail(self, response):  # 解析新闻内容
        content = response.xpath('//*[@id="content"]/div[2]//text()').extract()
        content = ''.join(content)  # 列表转字符串
        # 接受item
        item = response.meta['item']
        item['content'] = content
        # 提交管道 持久化存储
        yield item

    def closed(self, spider):
        self.bro.quit()

中间件文件:

from scrapy.http import HtmlResponse
from time import sleep


class WangyiproDownloaderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.

    def process_request(self, request, spider):
        # Called for each request that goes through the downloader
        # middleware.

        # Must either:
        # - return None: continue processing this request
        # - or return a Response object
        # - or return a Request object
        # - or raise IgnoreRequest: process_exception() methods of
        #   installed downloader middleware will be called
        return None

    # 该方法拦截四大板块对应的响应对象,进行篡改
    def process_response(self, request, response, spider):  # spider爬虫对象

        bro = spider.bro  # 获取了在爬虫类中定义的浏览器对象

        # 挑选出指定的响应对象进行篡改
        # 通过url指定request
        # 通过request指定response
        # spider爬虫对象

        # 定位指定的request
        if request.url in spider.models_urls:
            bro.get(request.url)  # 新闻四个板块对应的url进行请求
            sleep(3)
            page_text = bro.page_source  # 包含了动态加载的新闻数据

            # response #四大板块对应的响应对象
            # 针对定位到的这些response进行篡改
            # 实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象
            # 如何获取动态加载出的新闻数据?
            # 基于selenium便捷的获取动态加载数据
            # 响应对象所对应请求的对象的url
            # 旧的响应对象的响应数据中不包含动态加载数据的

            # new_response 新的响应对象
            # page_text做新的响应对象的响应数据
            # url 响应对象所对应请求对象的url
            new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)

            return new_response
        else:
            # response #其他请求对应的响应对象
            return response

    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.

        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain
        # - return a Request object: stops process_exception() chain
        pass

配置文件:

# 开启下载中间件
DOWNLOADER_MIDDLEWARES = {
    'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}

九、提升Scrapy的爬取效率

  • 增加并发:
    • 默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。
  • 降低日志级别:
    • 在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’
  • 禁止cookie:
    • 如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
  • 禁止重试:
    • 对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
  • 减少下载超时:
    • 如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s
posted @ 2021-05-25 20:07  王陸  阅读(173)  评论(0编辑  收藏  举报