8. scrapy框架

scrapy框架

版权声明:本博客来自路飞学城Python全栈开发培训课件,仅用于学习之用,严禁用于商业用途。
欢迎访问路飞学城官网:https://www.luffycity.com/

本节重点

  • scrapy基本使用
  • scrapy的数据持久化存储
  • scrapy基于spider类的全站数据爬取
  • 请求传参与五大核心组件
  • scrapy图片数据爬取
  • scrapy中间件
  • scrapy中selenium的应用
  • scrapy基于CrawlSpider类的全站数据爬取
  • scrapy分布式

1. scrapy基本使用

1.1 scrapy初识

1.1.1 什么是框架?

所谓的框架简单通用解释就是就是一个具有很强通用性并且集成了很多功能的项目模板,该模板可被应用在不同的项目需求中。也可被视为是一个项目的半成品。

1.1.2 如何学习框架?

对于刚接触编程或者初级程序员来讲,对于一个新的框架,只需要掌握该框架的作用及其各个功能的使用和应用即可,对于框架的底层实现和原理,在逐步进阶的过程中在慢慢深入即可。

1.1.3 什么是scrapy?

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍。其内部已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)。对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可。

1.2 scrapy基本使用

1.2.1 环境安装

linux和mac操作系统:

pip install scrapy

windows系统:

  • 查看python版本

    在这里插入图片描述

  • 下载twisted

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

    在这里插入图片描述

  • 安装twisted

    在下载目录下执行如下命令:

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

    在这里插入图片描述

  • 安装pywin32和scrapy

    pip install pywin32
    pip install scrapy
    

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

1.2.2 scrapy使用流程
  • 创建工程

    在终端里执行如下命令:

    scrapy startproject ProName
    
  • 进入工程目录

    cd ProName
    
  • 创建爬虫文件

    在spiders子目录中创建一个爬虫文件:

    scrapy genspider spiderName www.xxx.com
    
  • 编写相关操作代码

  • 执行工程

    scrapy crawl spiderName
    

在这里插入图片描述

1.2.3 爬虫文件剖析

我们按照以上步骤生成一个qiubaiPro工程,爬虫文件注释如下:

 # -*- 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协议
1.2.4 scrapy基于xpath数据解析

我们以爬虫糗事百科为例,数据解析如下:

# -*- 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, **kwargs):
        """
        页面解析
        :param response: 爬虫爬取页面后的响应体
        :param kwargs: 默认生成的爬虫文件没有该参数,需要手动增加该参数(可能因版本原因)
        :return: 
        """
        # xpath为response中的方法,可以将xpath表达式直接作用于该函数中
        # 定位方式见下图示例
        odiv = response.xpath('//div[@id="content-left"]/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)

在这里插入图片描述

2. scrapy的数据持久化存储

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

基于终端指令的持久化存储要保证爬虫文件的parse方法中有可迭代类型对象(通常为列表or字典)的返回,该返回值可以通过终端指令的形式写入指定格式的文件中进行持久化操作。

实例:

import scrapy


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

    def parse(self, response, **kwargs):
        odiv = response.xpath('//div[@id="content-left"]/div')
        content_list = []
        for div in odiv:
            author = div.xpath('.//div[@class="author clearfix"]/a/h2/text()')[0].extract()
            content = div.xpath('.//div[@class="content"]/span/text()')[0].extract()
            # 将解析到的内容封装到字典中
            dic = {
                '作者': author,
                '内容': content
            }
            # 将数据存储到content_list这个列表中
            content_list.append(dic)
        return content_list

该方式特点:

  • 要求:只可以将parse方法的返回值存储到本地的文本文件中

  • 注意:持久化存储对应的文本文件的类型只可以为:‘json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, 'pickle

  • 指令:scrapy crawl xxx -o filePath

    # 不同格式文件指令示例:
    scrapy crawl 爬虫名称 -o xxx.json
    scrapy crawl 爬虫名称 -o xxx.xml
    scrapy crawl 爬虫名称 -o xxx.csv
    
  • 好处:简介高效便捷

  • 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)

2.2 基于管道的持久化存储

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

  • items.py:数据结构模板文件。定义数据属性。
  • pipelines.py:管道文件。接收数据(items),进行持久化操作。

持久化流程:

  • 爬虫文件爬取到数据后,需要将数据封装到items对象中。
  • 使用yield关键字将items对象提交给pipelines管道进行持久化操作。
  • 在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
  • 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, **kwargs):
        # 解析:作者的名称+段子内容
        div_list = response.xpath('//div[@id="content-left"]/div')
        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() | ./div[1]/span/h2/text()').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

            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表示为优先级,值越小优先级越高
}

2.3 持久化存储进阶

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

解决方案:

  • 管道文件中一个管道类对应的是将数据存储到一种平台

  • 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受

  • process_item中的return item表示将item传递给下一个即将被执行的管道类

实例:

管道文件中的代码为:

注意:以下数据库持久化存储代码需要提前建好数据库和数据表,并按照数据库驱动程序。

import pymysql


# 该类为管道类,该类中的process_item方法是用来实现持久化存储操作的。
class QiubaiproPipeline(object):
    fp = None

    # 重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
    def open_spider(self, spider):
        print('开始爬虫......')
        self.fp = open('./qiubai.txt', 'w', encoding='utf-8')

    # 专门用来处理item类型对象
    # 该方法可以接收爬虫文件提交过来的item对象
    # 该方法没接收到一个item就会被调用一次
    # #持久化操作代码 (方式1:写入磁盘文件)
    def process_item(self, item, spider):
        author = item['author']
        content = item['content']

        self.fp.write(author + ':' + content + '\n')

        return item  # 就会传递给下一个即将被执行的管道类

    def close_spider(self, spider):
        print('结束爬虫!')
        self.fp.close()


# 管道文件中一个管道类对应将一组数据存储到一个平台或者载体中
# 如果想实现另一种形式的持久化操作,则可以再定制一个管道类:
class mysqlPileLine(object):
    conn = None
    cursor = None

    def open_spider(self, spider):
        self.conn = pymysql.Connect(host='127.0.0.1', 
                                    port=3306, user='root', 
                                    password='123456', 
                                    db='qiubai',
                                    charset='utf8')

    def process_item(self, item, spider):
        self.cursor = self.conn.cursor()
        try:
            self.cursor.execute(
                'insert into qiubai values("%s","%s")' % (item["author"], item["content"])
            )
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

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

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

3. scrapy基于spider类的全站数据爬取

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

基于scrapy如何进行全站数据爬取呢?

  • 将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。(不推荐)
  • 使用Request方法手动发起请求。(推荐)
    • 手动请求发送:yield scrapy.Request(url,callback) # callback专门用做于数据解析

实例1:

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

import scrapy
from qiushibaike.items import QiushibaikeItem


# scrapy.http import Request
class QiushiSpider(scrapy.Spider):
    name = 'qiushi'
    # allowed_domains = ['www.qiushibaike.com']
    start_urls = ['https://www.qiushibaike.com/text/']
    # 爬取多页
    pageNum = 1  # 起始页码
    url = 'https://www.qiushibaike.com/text/page/%s/'  # 每页的url

    def parse(self, response, **kwargs):
        div_list = response.xpath('//*[@id="content-left"]/div')
        for div in div_list:
            # //*[@id="qiushi_tag_120996995"]/div[1]/a[2]/h2
            author = div.xpath('.//div[@class="author clearfix"]//h2/text()').extract_first()
            author = author.strip('\n')
            content = div.xpath('.//div[@class="content"]/span/text()').extract_first()
            content = content.strip('\n')
            item = QiushibaikeItem()
            item['author'] = author
            item['content'] = content
            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)

实例2:

需求:爬取校花网中的照片的名称

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


class XiaohuaSpider(scrapy.Spider):
    name = 'xiaohua'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://www.521609.com/meinvxiaohua/']

    #生成一个通用的url模板(不可变)
    url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
    page_num = 2

    def parse(self, response):
        li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
        for li in li_list:
            img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
            print(img_name)

        if self.page_num <= 11:
            new_url = format(self.url%self.page_num)
            self.page_num += 1
            #手动请求发送:callback回调函数是专门用作于数据解析
            yield scrapy.Request(url=new_url,callback=self.parse)

4. 请求传参

使用场景:

在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中。这时我们就需要用到请求传参。

实例:

需求:爬取BOSS直聘网的职位信息和职位详情

boss.py文件:

# -*- 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)

5. 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

6. scrapy图片数据爬取

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

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

基于文件下载的管道类ImagesPipeline:

只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。

使用流程:

  • 数据解析(图片的地址)
  • 将存储图片地址的item提交到制定的管道类
  • 在管道文件中自定制一个基于ImagesPipeLine的一个管道类
    • get_media_request方法:获取请求路径
    • file_path方法:指定文件名称
    • item_completed方法:返回给下一个即将被执行的管道类
  • 在配置文件中:
    • 指定图片存储的目录:IMAGES_STORE = ‘./imgs_bobo’
    • 指定开启的管道:自定制的管道类

使用实例:

需求:爬取站长素材上的图片

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

    ...
    BOT_NAME = 'imgsPro'
    
    SPIDER_MODULES = ['imgsPro.spiders']
    NEWSPIDER_MODULE = 'imgsPro.spiders'
    
    LOG_LEVEL = 'ERROR'
    # Crawl responsibly by identifying yourself (and your website) on the user-agent
    USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
    
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    
    # 开启管道类
    ITEM_PIPELINES = {
       'imgsPro.pipelines.imgsPileLine': 300,
    }
    # 指定图片存储的目录
    IMAGES_STORE = './imgs'
    ...
    

    配置文件中,增加了图片存储目录的配置。

  • 编写爬虫文件img.py,获取到图片的路径:

    # -*- 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:
                #注意:使用伪属性
                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 imgsPileLine(ImagesPipeline):
    
        # 就是可以根据图片地址进行图片数据的请求
        def get_media_requests(self, item, info):
            yield scrapy.Request(item['src'])
    
        # 指定图片存储的名称
        def file_path(self, request, response=None, info=None, *, item=None):
            imgName = request.url.split('/')[-1]
            return imgName
    
        # 返回给下一个即将被执行的管道类
        def item_completed(self, results, item, info):
            return item
    

    在该管道类中,指定图片地址、图片存储名称即可。

7. scrapy五大核心组件

7.1 五大核心组件

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

在这里插入图片描述

scrapy的五大核心组件:

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

7.2 下载中间件

下载器中间件(Downloader Middleware) 用于处理scrapy的request和response的钩子框架,可以全局的修改一些参数,如UA伪装、代理ip、header等。

UA伪装:User-Agent池

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

代理池:

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

使用下载器中间件时必须激活这个中间件,方法是在settings.py文件中设置DOWNLOADER_MIDDLEWARES这个字典,格式类似如下:

DOWNLOADERMIDDLEWARES = {
    'myproject.middlewares.Custom_A_DownloaderMiddleware': 543,
    'myproject.middlewares.Custom_B_DownloaderMiddleware': 643,
    'myproject.middlewares.Custom_B_DownloaderMiddleware': None,
}

数字越小,越靠近引擎,数字越大越靠近下载器,所以数字越小的,processrequest()优先处理;数字越大的,process_response()优先处理;若需要关闭某个中间件直接设为None即可。

自定义下载器中间件

有时我们需要编写自己的一些下载器中间件,如使用代理,更换user-agent等,对于请求的中间件实现 process_request(request, spider);对于处理回复中间件实现process_response(request, response, spider);以及异常处理实现 process_exception(request, exception, spider)

  • process_request(*request*, *spider*)

每当scrapy进行一个request请求时,这个方法被调用。通常它可以返回1.None 2.Response对象 3.Request对象 4.抛出IgnoreRequest对象。通常返回None较常见,它会继续执行爬虫下去。

利用下载中间件更换user-agent和代理ip的实例:

中间件文件middlewares.py:

from scrapy import signals

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"
    ]
    PROXY_http = [
        '153.180.102.104:80',
        '195.208.131.189:56055',
    ]
    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':
            # 代理
            request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
        else:
            request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)

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

配置文件:

...
# 打开中间件
DOWNLOADER_MIDDLEWARES = {
   'middlePro.middlewares.MiddleproDownloaderMiddleware': 543,
}
...

8. scrapy中selenium的应用

8.1 引子

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

8.2 案例分析

需求: 爬取网易新闻的国内板块下的新闻数据

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

selenium在scrapy中使用的原理分析:

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

selenium在scrapy中的使用流程:

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

8.3 案例实战:

  • 创建爬虫工程

    在终端里执行如下命令:

    scrapy startproject wangyiPro
    
  • 进入工程目录

    cd wangyiPro
    
  • 创建爬虫应用

    在spiders子目录中创建一个爬虫应用:

    scrapy genspider wangyi www.xxx.com
    
  • 编写相关操作代码

    items.py:

    import scrapy
    
    
    class WangyiproItem(scrapy.Item):
        # define the fields for your item here like:
        title = scrapy.Field()
        content = scrapy.Field()
    

    爬虫文件wangyi.py:

    import scrapy
    from selenium import webdriver
    from wangyiPro.items import WangyiproItem
    
    
    class WangyiSpider(scrapy.Spider):
        name = 'wangyi'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['https://news.163.com/']
    
        models_urls = []  # 存储五个板块对应详情页的url
    
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.bro = webdriver.Chrome()
    
        # 解析五大板块对应详情页的url
        def parse(self, response, **kwargs):
            li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
            alist = [3, 4, 6]
            for index in alist:
                li = li_list[index]
                model_url = li.xpath('./a/@href').extract_first()
                self.models_urls.append(model_url)
    
            # 依次对每一个板块对应的页面进行请求
            for url in self.models_urls:
                yield scrapy.Request(url=url, callback=self.parse_model)
    
        # 每一个板块对应的新闻标题相关的内容都是动态加载
        def parse_model(self, response):
            div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
            for div in div_list:
                title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
                title = title.replace('\n', '').replace('\r', '')
                detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
                item = WangyiproItem()
                item['title'] = title
                # 对每一个详情页进行请求
                yield scrapy.Request(url=detail_url, 
                                     callback=self.parse_detail, 
                                     meta={'item': item})
    
        def parse_detail(self, response):
            item = response.meta['item']
            content = response.xpath('//*[@id="content"]/div[2]//text()').extract()
            content = ''.join(content)
            content.replace('\n', '').replace('\r', '')
            item['content'] = content
            yield item
    
        def closed(self, spider):
            self.bro.quit()
    

    管道类middlewares.py:

    特别说明:管道类中其他没有用到的类和方法要全部注释或删除。

    from scrapy.http import HtmlResponse
    from scrapy import signals
    import time
    
    # useful for handling different item types with a single interface
    from itemadapter import is_item, ItemAdapter
    
    
    class WangyiproDownloaderMiddleware:
        # 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):
            return None
    
        def process_response(self, request, response, spider):
            bro = spider.bro
            # 挑选出指定的响应对象进行篡改
            # 通过url指定request
            # 通过request指定response
            if request.url in spider.models_urls:
                bro.get(request.url)
                time.sleep(3)
                page_text = bro.page_source  # 包含了动态加载的新闻数据
                # response #五大板块对应的响应对象
                # 针对定位到的这些response进行篡改
                # 实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象
                # 如何获取动态加载出的新闻数据?
                # 基于selenium便捷的获取动态加载数据
                new_response = HtmlResponse(url=request.url, 
                                            body=page_text, 
                                            encoding='utf-8', 
                                            request=request)
    
                return new_response
            else:
                return response
    
        def process_exception(self, request, exception, spider):
            pass
    

    配置文件settinds.py

    BOT_NAME = 'wangyiPro'
    
    SPIDER_MODULES = ['wangyiPro.spiders']
    NEWSPIDER_MODULE = 'wangyiPro.spiders'
    
    
    # UA伪装
    USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'
    # 设置终端打印日志级别
    LOG_LEVEL = 'ERROR'
    # 设置不遵从爬取协议
    ROBOTSTXT_OBEY = False
    
    # 开启下载中间件
    DOWNLOADER_MIDDLEWARES = {
       'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
    }
    
    # 开启管道
    ITEM_PIPELINES = {
       'wangyiPro.pipelines.WangyiproPipeline': 300,
    }
    
  • 执行工程

    scrapy crawl wangyi
    

9. scrapy基于CrawlSpider类的全站数据爬取

9.1 引子

回顾之前的代码中,我们有很大一部分时间在寻找下一页的url地址或者是内容的url地址上面,这个过程能更简单一些么?

实现思路:

  • 从response中提取所有的满足规则的url地址
  • 自动的构造自己requests请求,发送给引擎

对应的crawlspider就可以实现上述需求,能够匹配满足条件的url地址,组装成Reuqest对象后自动发送给引擎,同时能够指定callback函数**,**即:crawlspider爬虫可以按照规则自动获取连接

提问:如果想要通过爬虫程序去爬取”糗百“全站数据新闻数据的话,有几种实现方法?

方法一:基于Scrapy框架中的Spider的递归爬取进行实现(Request模块递归回调parse方法)。

方法二:基于CrawlSpider的自动爬取进行实现(更加简洁和高效)。

9.2 CrawlSpider简介

CrawlSpider其实是Spider的一个子类,除了继承到Spider的特性和功能外,还派生除了其自己独有的更加强大的特性和功能。其中最显著的功能就是”LinkExtractors链接提取器“。Spider是所有爬虫的基类,其设计原则只是为了爬取start_url列表中网页,而从爬取到的网页中提取出的url进行继续的爬取工作使用CrawlSpider更合适。

9.3 CrawlSpider使用

9.3.1 CrawlSpider初识

创建scrapy工程:

scrapy startproject projectName

创建爬虫文件:

scrapy genspider -t crawl spiderName www.xxx.com

注意:此指令对比以前的指令多了 “-t crawl”,表示创建的爬虫文件是基于CrawlSpider这个类的,而不再是Spider这个基类。

观察生成的爬虫文件:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class ChoutidemoSpider(CrawlSpider):
    name = 'choutiDemo'
    #allowed_domains = ['www.chouti.com']
    start_urls = ['http://www.chouti.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        i = {}
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return i

观察CrawlSpider跟普通的spider的区别:

  • 在crawlspider爬虫中,没有parse函数
  • CrawlSpider多了一个rules属性,其作用是定义”提取动作“。在rules中可以包含一个或多个Rule对象,在Rule对象中包含了LinkExtractor对象
9.3.2 CrawlSpider详解

CrawlSpider的重点是rules

  • rules是一个元组或者是列表,包含的是Rule对象
  • 一个Rule对象表示一种提取规则
  • Rule表示规则,其中包含LinkExtractor,callback和follow等参数
  • LinkExtractor:连接提取器,可以通过正则或者是xpath来进行url地址的匹配
  • callback :表示经过连接提取器提取出来的url地址响应的回调函数,可以没有,没有表示响应不会进行回调函数的处理
  • follow:连接提取器提取的url地址对应的响应是否还会继续被rules中的规则进行提取,True表示会,Flase表示不会

LinkExtractor:顾名思义,链接提取器

作用:根据指定的规则(allow)进行指定链接的提取。

LinkExtractor(
         allow=r'Items/'# 满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。
         deny=xxx,  # 满足正则表达式的则不会被提取。
         restrict_xpaths=xxx, # 满足xpath表达式的值会被提取
         restrict_css=xxx, # 满足css表达式的值会被提取
         deny_domains=xxx, # 不会被提取的链接的domains。 
    )

Rule : 规则解析器

作用:根据链接提取器中提取到的链接,根据指定规则(callback)提取解析器链接网页中的内容。

Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True)

参数介绍:

  • 参数1:指定链接提取器
  • 参数2:指定规则解析器解析数据的规则(回调函数)
  • 参数3:是否将链接提取器继续作用到链接提取器提取出的链接网页中。当callback为None,参数3的默认值为true。
9.3.3 CrawlSpider爬取流程

CrawlSpider整体爬取流程:

  • 爬虫文件首先根据起始url,获取该url的网页内容

  • 链接提取器会根据指定提取规则将步骤a中网页内容中的链接进行提取

  • 规则解析器会根据指定解析规则将链接提取器中提取到的链接中的网页内容根据指定的规则进行解析

  • 将解析数据封装到item中,然后提交给管道进行持久化存储

9.4 CrawlSpider实例

需求: 爬取58同城二手房信息

实例:

items.py:

import scrapy


class HourseproItem(scrapy.Item):
    # define the fields for your item here like:
    title = scrapy.Field()
    detail = scrapy.Field()
    price_total = scrapy.Field()
    price_average = scrapy.Field()

爬虫文件hourse.py:

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from hoursePro.items import HourseproItem


class HourseSpider(CrawlSpider):
    name = 'hourse'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://bj.58.com/ershoufang/p1']

    rules = (
        Rule(LinkExtractor(allow=r'ershoufang/p\d+/'), callback='parse_item', follow=True),
    )
    
    def parse_item(self, response, **kwargs):
        div_list = response.xpath('//*[@id="__layout"]/div/section/section[3]/section[1]/section[2]/div')
        for div in div_list:
            title = div.xpath('./a/div[2]/div[1]/div[1]/h3/text()').extract_first()
            detail = div.xpath('./a/div[2]/div[1]/section//text()').extract()
            # 去除爬取文本中的空格和换行
            detail = [info.replace(' ','').replace('\n',' ').replace('\r',' ') for info in detail]
            detail = ''.join(detail)
            price_total = div.xpath('./a/div[2]/div[2]/p[1]//text()').extract()
            price_total = ''.join(price_total)
            price_average = div.xpath('./a/div[2]/div[2]/p[2]/text()').extract_first()

            item = HourseproItem()
            item['title'] = title
            item['detail'] = detail
            item['price_total'] = price_total
            item['price_average'] = price_average
            yield item

管道文件pipelines.py:

class HourseproPipeline:
    fp = None

    def open_spider(self, spider):
        self.fp = open('./hourse.txt', 'w', encoding='utf-8')
        print('开始写入...')

    def process_item(self, item, spider):
        title = item['title']
        detail = item['detail']
        price_total = item['price_total']
        price_average = item['price_average']
        info = '{}\n{}\n{}\n{}'.format(title,price_total,price_average,detail)
        self.fp.write(info)

    def close_spider(self, spider):
        self.fp.close()
        print('写入完成!')

配置文件settings.py:

BOT_NAME = 'hoursePro'

SPIDER_MODULES = ['hoursePro.spiders']
NEWSPIDER_MODULE = 'hoursePro.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'


LOG_LEVEL = 'ERROR'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
   'hoursePro.pipelines.HourseproPipeline': 300,
}

爬取结果hourse.txt:

西城厂桥胡同私产平房出售 含土地出让金 过户周期短 随时看房
298 万
150506元/㎡
1室0厅0卫 19.8㎡ 西 共1层  2006年建造 厂桥胡同小区西城什刹海厂桥胡同满五唯一新上文化路小区   一室一厅一卫 纯南向小一居校园房
146 万
37883元/㎡
1室1厅1卫 38.5㎡ 南 共3层  1979年建造 文化路小区房山良乡良乡文化路满五唯一新上霸州北站北 首付2万 宜居养老 温泉现房  门口公交直达北京
52 万
6934元/㎡
2室2厅1卫 75㎡ 南北 中层(共18层)  2018年建造 牛驼温泉小镇北京周边廊坊平安路新上推荐新房大兴黄村中心 中楼层 三居室 529万
529 万
50252元/㎡
3室1厅1卫 105.3㎡ 南北 中层(共6层)  2003年建造 怡兴园大兴黄村兴华南路5号满五唯一新上随时看!崇文门东大街 2室1厅1卫!高层无遮挡
615 万
92454元/㎡
2室1厅1卫 66.5㎡ 南北 高层(共12层)  1980年建造 崇文门东大街社区东城崇文门崇文门东大街满五唯一新上居间费百一过户支付北街家园六区  高楼层 采光视野好 只契税
280 万
38830元/㎡

10. scrapy分布式

我们在前面已经实现了Scrapy单机爬虫,虽然爬虫是异步加多线程的,但是我们只能在一台主机上运行,所以爬取效率还是有限的,分布式爬虫则是将多台主机组合起来,共同完成一个爬取任务,这将大大提高爬取的效率。

10.1 分布式爬虫架构

在了解分布式爬虫架构之前,首先回顾一下Scrapy的架构,如下图所示。

在这里插入图片描述

Scrapy单机爬虫中有一个本地爬取队列Queue,这个队列是利用deque模块实现的。如果新的Request生成就会放到队列里面,随后Request被Scheduler调度。之后,Request交给Downloader执行爬取,简单的调度架构如下图所示。

在这里插入图片描述

如果两个Scheduler同时从队列里面取Request,每个Scheduler都有其对应的Downloader,那么在带宽足够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错,爬取效率会翻倍。

这样,Scheduler可以扩展多个,Downloader也可以扩展多个。而爬取队列Queue必须始终为一个,也就是所谓的共享爬取队列。这样才能保证Scheduer从队列里调度某个Request之后,其他Scheduler不会重复调度此Request,就可以做到多个Schduler同步爬取。这就是分布式爬虫的基本雏形,简单调度架构如下图所示。

在这里插入图片描述

我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不需要各自维护爬取队列,而是从共享爬取队列存取Request。但是各台主机还是有各自的Scheduler和Downloader,所以调度和下载功能分别完成。如果不考虑队列存取性能消耗,爬取效率还是会成倍提高。

10.2 scrapy-redis组件简介

通过上一节的知识,我们发现原生的scarapy是不可以实现分布式爬虫,原因如下:

  • 调度器不可以被分布式机群共享
  • 管道不可以被分布式机群共享

所以实现分布式爬取的关键就是,找一台专门的主机上运行一个共享的队列比如Redis,然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求,所以总结下来,实现分布式的关键就是三点:

  • 共享队列
  • 重写Scheduler,让其无论是去重还是任务都去访问共享队列
  • 为Scheduler定制去重规则(利用redis的集合类型)

以上三点便是scrapy-redis组件的核心功能。

必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫,可以给原生的scrapy框架提供可以被共享的管道和调度器。

在这里插入图片描述

10.3 环境安装

  • 安装scrapy-redis的组件

    pip install scrapy-redis
    
  • 安装Redis

    在 Windows 下,Redis 可以直接到 GitHub 的发行版本里面下载,具体下载地址是 https://github.com/MSOpenTech/redis/releases。

    可以下载 Redis-x64-3.2.100.msi 安装即可。

    安装过程比较简单,直接点击 Next 按钮安装即可。安装完成后,Redis 便会启动。

    在系统服务页面里,可以观察到多了一个正在运行到 Redis 服务,如图所示:

    在这里插入图片描述

10.4 分布式爬取实例

需求:scrapy+redis分布式爬取58同城北京全站二手房数据

实现流程:

  • 创建一个工程

  • 创建一个基于CrawlSpider的爬虫文件

  • 修改当前的爬虫文件

    • 导包:from scrapy_redis.spiders import RedisCrawlSpider
    • 将start_urls和allowed_domains进行注释
    • 添加一个新属性:redis_key = ‘sun’ 可以被共享的调度器队列的名称
    • 编写数据解析相关的操作
    • 将当前爬虫类的父类修改成RedisCrawlSpider
    # 爬虫文件hourse.py
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from hoursePro.items import HourseproItem
    from scrapy_redis.spiders import RedisCrawlSpider
    
    
    class HourseSpider(RedisCrawlSpider):
        name = 'hourse'
        # allowed_domains = ['www.xxx.com']
        # start_urls = ['https://bj.58.com/ershoufang/p1']
    
        redis_key = 'hourse'
        rules = (
            Rule(LinkExtractor(allow=r'ershoufang/p\d+/'), callback='parse_item', follow=True),
        )
    
        def parse_item(self, response, **kwargs):
            print(response)
    
            div_list = response.xpath('//*[@id="__layout"]/div/section/section[3]/section[1]/section[2]/div')
            for div in div_list:
                title = div.xpath('./a/div[2]/div[1]/div[1]/h3/text()').extract_first()
                detail = div.xpath('./a/div[2]/div[1]/section//text()').extract()
                # 去除爬取文本中的空格和换行
                detail = [info.replace(' ', '').replace('\n', ' ').replace('\r', ' ') for info in detail]
                detail = ''.join(detail)
                price_total = div.xpath('./a/div[2]/div[2]/p[1]//text()').extract()
                price_total = ''.join(price_total)
                price_average = div.xpath('./a/div[2]/div[2]/p[2]/text()').extract_first()
    
                item = HourseproItem()
                item['title'] = title
                item['detail'] = detail
                item['price_total'] = price_total
                item['price_average'] = price_average
                yield item
    
  • 修改配置文件settings

    • 指定使用可以被共享的管道
    • 指定调度器
    • 指定redis服务器
    BOT_NAME = 'hoursePro'
    
    SPIDER_MODULES = ['hoursePro.spiders']
    NEWSPIDER_MODULE = 'hoursePro.spiders'
    
    # Crawl responsibly by identifying yourself (and your website) on the user-agent
    USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'
    
    # LOG_LEVEL = 'ERROR'
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    
    # 分布式相关配置
    #指定管道
    ITEM_PIPELINES = {
        'scrapy_redis.pipelines.RedisPipeline': 400
    }
    #指定调度器
    # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 使用scrapy-redis组件自己的调度器
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。
    # 如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
    SCHEDULER_PERSIST = True
    
    #指定redis
    REDIS_HOST = '127.0.0.1' #redis远程服务器的ip(修改)
    REDIS_PORT = 6379
    
  • redis相关操作配置

    打开配置文件修改(windwos配置文件为redis.windows-service.conf)

    • 将bind 127.0.0.1进行删除(配置文件第56行)
    • 关闭保护模式:protected-mode yes改为no(配置文件第75行)

    在这里插入图片描述

    在这里插入图片描述

    • 结合着配置文件开启redis服务
    • 启动客户端( 终端执行命令:redis-cli )
  • 执行工程

    scrapy runspider hourse.py
    
  • 向调度器的队列中放入一个起始的url

    • 调度器的队列在redis的客户端中,执行如下命令:

      lpush hourse https://bj.58.com/ershoufang/p1
      
  • 爬取到的数据存储在了redis的proName:items这个数据结构中。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值