學習爬蟲之Scrapy框架學習(七)---Scrapy框架裏使用分佈式爬蟲(Scrapy_redis)。分佈式實戰豆瓣電影信息爬取;使用scrapyd實現項目部署。(thirteen day)

第一部分:分佈式爬蟲(使用Scrapy_redis):

(官方文檔:點我!!!
1.簡單介紹:
scrapy_redis是一個基於Redis的Scrapy組件,用於scrapy項目的分佈式部署和開發。

特點:
分佈式爬取

你可以啓動多個spider對象,互相之間共享有一個redis的request隊列。最適合多個域名的廣泛內容的爬取。

分佈式數據處理

爬取到的item數據被推送到redis中,這意味着你可以啓動儘可能多的item處理程序。

scrapy即插即用

scrapy調度程序+過濾器,項目管道,base spider,使用簡單。

2.Scrapy_redis的安裝:
一般通過pip安裝Scrapy-redis:

pip install scrapy-redis
scrapy-redis依賴:

Python 2.7, 3.4 or 3.5以上
Redis >= 2.8
Scrapy >= 1.1
redis-py >= 2.10

scrapy-redis的使用非常簡單,幾乎可以並不改變原本scrapy項目的代碼,只用做少量設置。

在這裏插入圖片描述注意:使用分佈式和以往直接使用Scrapy的區別在於,增加了一個redis數據庫,也就是增加了一步:在引擎將請求交給調度器scheduler後,調度器不是直接將請求排隊後就再交給引擎,而是要先交給redis數據庫,可想而知,如果有多個spider對象,那每個裏面的步驟都是這樣的話,這個redis數據庫就是所有spider對象的公共區域,所有的請求都要扔進redis數據庫中,如果有個spider對象是空閒的,那麼redis就會直接將請求給它,而源碼中有過濾去重功能,所以,這就實現了多個人共同做一件事,即分佈式爬取。

1.簡單使用分佈式爬取豆瓣電影信息:

(目標:在本機上使用兩個完全一模一樣的豆瓣項目,去使用分佈式下載豆瓣電影top250首頁電影信息!)
在這裏插入圖片描述其實,我們要進行修改的就只有settings.py文件以及咱的爬蟲文件,別的文件都不需要進行改動。

(1)settings.py文件中的配置:

#設置scrapy-redis
#1.啓用調度將請求存儲進redis
# from scrapy_redis.scheduler import Scheduler
SCHEDULER="scrapy_redis.scheduler.Scheduler"

#2.確保所有spider通過redis共享相同的重複過濾
# from scrapy_redis.dupefilter import RFPDupeFilter
DUPEFILTER_CLASS="scrapy_redis.duperfilter.RFDuperFilter"

#3.指定連接到Redis時要使用的主機和端口     目的是連接上redis數據庫
REDIS_HOST="localhost"
REDIS_PORT=6379

(2)spider文件的更改

**共四步:
#1.導出RedisSpider類:(既然要使用它,肯定首先要導入!)
from scrapy_redis.spiders import RedisSpider

#2.繼承使用RedisSpider類:(既然要使用它,就要繼承去使用這個類)
class DbSpider(RedisSpider):

#3.既然將請求都放進了Redis裏,那爬蟲文件中就不再需要start_urls這個初始請求了:
# start_urls = [‘https://movie.douban.com/top250’]

#4.設置一個鍵,尋找起始的url:(這個鍵就會在redis中尋找初始的url,所以後面我們只需往redis裏放請求即可!)
redis_key=“db:start_urls”
**

完整版爬蟲文件:

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

from ..items import DoubanItem      #因爲我們要使用包含定義字段名的類,所以需要導入
from scrapy_redis.spiders import RedisSpider        #1.導出RedisSpider類
class DbSpider(RedisSpider):                        #2.使用RedisSpider類
    name = 'db'
    allowed_domains = ['movie.douban.com']
    # start_urls = ['https://movie.douban.com/top250']  #3.將請求放在的redis裏面

    redis_key="db:start_urls"                          #4.設置一個鍵,尋找起始的url

    page_num=0   #類變量
    def parse(self, response):  #解析和提取數據
        # 獲取電影信息數據
        # films_name=response.xpath('//div[@class="info"]/div/a/span[1]/text()').extract()
        node_list=response.xpath('//div[@class="info"]')    #25個
        if node_list:       #此判斷的作用:在爬取到10頁之後,就獲取不到了!判斷每次是否獲取到數據,如果沒有則返回空(即停止了)
            self.logger.info("你好帥啊!")
            for node in node_list:
                # 電影名字
                film_name=node.xpath('./div/a/span[1]/text()').extract()[0]
                # 主演   拿標籤內容,再正則表達式匹配
                con_star_name=node.xpath('./div/p[1]/text()').extract()[0]
                if "主" in con_star_name:
                    star_name=re.findall("主演?:? ?(.*)",con_star_name)[0]
                else:
                    star_name="空"
                #評分
                score=node_list.xpath('./div/div/span[@property="v:average"]/text()').extract()[0]

                # 使用字段名  收集數據
                item=DoubanItem()
                item["film_name"]=film_name
                item["star_name"]=star_name
                item["score"]=score
                # 形式:{"film_name":"肖申克的救贖","star_name":"蒂姆","score":"9.7"}
                #yield item          #不使用return的原因是,return返回的是parse這個方法(會造成只會傳給管道第一個電影信息的bug)。而我們所需要的是返回每次解析後的數據
                # 獲取電影簡介詳情的url:
                detail_url=node.xpath('./div/a/@href').extract()[0]
                yield scrapy.Request(detail_url,callback=self.get_detail,meta={"info":item})

            self.page_num+=1
            print("page_num:",self.page_num)
            if self.page_num == 1:
                return
            page_url="https://movie.douban.com/top250?start={}&filter=".format(self.page_num*25)
            yield scrapy.Request(page_url, callback=self.parse)
            # 注意:各個模塊的請求都會交給引擎,然後經過引擎的一系列操作;但是,切記:引擎最後要把得到的數據再來給到
            # spider爬蟲文件讓它解析並獲取到真正想要的數據(callback=self.parse)這樣就可以再給到自身。
        else:
            return

    def get_detail(self,response):
        item=DoubanItem()
        #獲取電影簡介信息
        #1.meta會跟隨response一塊返回  2.可以通過response.meta接收   3.通過updata可以添加到新的item對象
        info = response.meta["info"]        #接收電影的基本信息
        item.update(info)                   #把電影基本信息的字段加進去

        #將電影簡介信息加入相應的字段裏
        description=response.xpath('//div[@id="link-report"]//span[@property="v:summary"]/text()').extract()[0].strip()
        item['description']=description
        yield item


(3)items.py文件:

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class DoubanItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    #需要定義字段名  就像數據庫那樣,有字段名,才能插入數據(即存儲數據)
    # films_name=scrapy.Field()   #定義字段名
    film_name=scrapy.Field()
    star_name=scrapy.Field()
    score=scrapy.Field()

    description=scrapy.Field()

(4)pipelines.py文件:

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html

import json
class DoubanPipeline(object):
    def open_spider(self,spider):   #爬蟲文件開啓,此方法就開啓
        self.f=open("films.txt","w",encoding="utf-8")       #打開文件

    def process_item(self, item, spider):        #會來25次,就會調用25次這個方法  如果按常規來寫,文件就會被操作25次打開關閉
        #爲了能寫進text  json.dumps將dic數據轉換爲str
        json_str=json.dumps(dict(item),ensure_ascii=False)+"\n"
        self.f.write(json_str)                              #爬蟲文件開啓時,文件就已經打開,在此直接寫入數據即可!
        return item

    def close_spider(self,spider):  #爬蟲文件關閉,此方法就開啓
        self.f.close()                                      #爬蟲文件關閉時,引擎已經將全部數據交給管道,關閉文件


分佈式實現效果:(第一個項目在project1目錄下;第二個項目在project2目錄下)

(1)直接運行項目,發現在等待,進行一些便於觀察的簡單設置後。

分別在兩個終端中開啓兩個scrapy項目:(注意:之前要開啓咱的redis數據庫)

在這裏插入圖片描述會發現,這倆項目都在等待,不會繼續執行。這是因爲咱沒有給redis這個公共區域一個初始的請求,這倆項目都在週而復始的向redis要初始url,結果一直要不到!


{
爲了觀察更加明顯,在project1目錄下的爬蟲文件做如下操作:
在這裏插入圖片描述在這裏插入圖片描述在project2目錄下的爬蟲文件中做類似操作:
在這裏插入圖片描述
在這裏插入圖片描述同時在兩個項目的settings.py文件中設置兩個的日誌不顯示在控制檯,而是存儲到.log文件中。爲了便於觀察:

LOG_FILE="db.log"
LOG_ENABLED=False

}


(2)分別運行兩個項目,發現都在等待,再開一個終端,做如下操作:

在這裏插入圖片描述
我們會發現我們的兩個項目都會成功的跑起來:

在這裏插入圖片描述
在這裏插入圖片描述

總結:

會發現,在project2目錄下的項目運行會顯示使用了parse函數,這也就說明在redis這個公共區域的start_urls請求被project2目錄下的項目搶到了,然後就會運行這個項目,
但是,在這個項目的爬蟲文件代碼執行的過程中會在25次循環中給引擎發送共25次url請求,引擎得到這25個request請求後會將它們都交給scheduler調度器,再通過調度器交給redis數據庫這個公共區域。
最後,兩個項目的scheduler調度器就會一起搶這公共區域裏的請求,這就實現了咱爬蟲的分佈式爬取數據!!!

在這裏插入圖片描述

效果:

(1)在project1目錄下的項目中會爬取到9部電影的信息;

在這裏插入圖片描述

(2)在project2目錄下的項目中會爬取到16部電影的信息。

在這裏插入圖片描述

共計剛好我們所要爬取的所有目標數據:25部電影的信息。

解決一些零碎的小問題:

1.解決爬空問題:(在兩個項目中都進行以下操作!)

(1)使用拓展程序(這個文件就是爲了解決爬空而生的):

(文件名:extensions.py)

# -*- coding: utf-8 -*-

# Define here the models for your scraped Extensions
import logging

from scrapy import signals
from scrapy.exceptions import NotConfigured

logging = logging.getLogger(__name__)


class RedisSpiderSmartIdleClosedExensions(object):

    def __init__(self, idle_number, crawler):
        self.crawler = crawler
        self.idle_number = idle_number
        self.idle_list = []
        self.idle_count = 0

    @classmethod
    def from_crawler(cls, crawler):
        # first check if the extension should be enabled and raise

        # NotConfigured otherwise

        if not crawler.settings.getbool('MYEXT_ENABLED'):
            raise NotConfigured

        if not 'redis_key' in crawler.spidercls.__dict__.keys():
            raise NotConfigured('Only supports RedisSpider')

        # get the number of items from settings

        idle_number = crawler.settings.getint('IDLE_NUMBER', 360)

        # instantiate the extension object

        ext = cls(idle_number, crawler)

        # connect the extension object to signals

        crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)

        crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)

        crawler.signals.connect(ext.spider_idle, signal=signals.spider_idle)

        return ext

    def spider_opened(self, spider):
        spider.logger.info("opened spider {}, Allow waiting time:{} second".format(spider.name, self.idle_number * 5))

    def spider_closed(self, spider):
        spider.logger.info(
            "closed spider {}, Waiting time exceeded {} second".format(spider.name, self.idle_number * 5))

    def spider_idle(self, spider):
        # 程序啓動的時候會調用這個方法一次,之後每隔5秒再請求一次
        # 當持續半個小時都沒有spider.redis_key,就關閉爬蟲
        # 判斷是否存在 redis_key
        if not spider.server.exists(spider.redis_key):
            self.idle_count += 1
        else:
            self.idle_count = 0

        if self.idle_count > self.idle_number:
            # 執行關閉爬蟲操作
            self.crawler.engine.close_spider(spider, 'Waiting time exceeded')

(2)在settings.py文件中設置這個拓展程序:

# Enable or disable extensions                  #擴展程序
# See https://docs.scrapy.org/en/latest/topics/extensions.html
EXTENSIONS = {
   # 'scrapy.extensions.telnet.TelnetConsole': None,
    'douban.extensions.RedisSpiderSmartIdleClosedExensions':500,					#開啓extensions.py這個拓展程序
}
MYEXT_ENABLED = True      # 開啓擴展
IDLE_NUMBER = 10           # 配置空閒持續時間單位爲 10個 ,一個時間單位爲5s


(注意:redis中存儲的數據:
spidername:items

list類型,保存爬蟲獲取到的數據item內容是json字符串。
spidername:dupefilter

set類型,用於爬蟲訪問的URL去重內容是40個字符的url的hash字符串
spidername:start_urls

list類型,用於接收redisspider啓動時的第一個url
spidername:requests

zset類型,用於存放requests等待調度。內容是requests對象的序列化字符串。

關於分佈式(Scrapy_redis)的總結:

()分佈式爬蟲
一.settings裏的配置
# 啓用調度將請求存儲進redis
# 1.必須
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 確保所有spider通過redis共享相同的重複過濾。
#2. 必須
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 指定連接到Redis時要使用的主機和端口。
# 3.必須
REDIS_HOST = 'localhost'
REDIS_PORT = 6379

二.spider文件更改

from scrapy_redis.spiders import  RedisSpider #1 導出 RedisSpider

class DbSpider(RedisSpider):  #2使用RedisSpider類

    # start_urls = ['https://movie.douban.com/top250/']#3將要請求放在  公共區域 redis裏面
    redis_key = "db:start_urls"#4  設置一個鍵  尋找起始url.redis數據庫中 寫入  start_urls      
lpush  db:start_urls   https://movie.douban.com/top250/.解決爬空的問題
1.解決爬空的文件    extensions.py  主要是RedisSpiderSmartIdleClosedExensions
2.設置
MYEXT_ENABLED = True      # 開啓擴展
IDLE_NUMBER = 10           # 配置空閒持續時間單位爲 10個 ,一個時間單位爲5s

第二部分:項目部署

(scarpy是一個爬蟲框架, 而scrapyd是一個網頁版管理scrapy的工具, scrapy爬蟲寫好後,可以用命令行運行,但是如果能在網頁上操作就比較方便. scrapyd就是爲了解決這個問題,能夠在網頁端查看正在執行的任務,也能新建爬蟲任務,和終止爬蟲任務,功能比較強大.。要將spider部署到Scrapyd,可以使用由Scrapyd客戶端包提供的Scrapyd -deploy工具。)
注意以下幾個問題:

1、scrapy是什麼?
一個爬蟲框架,你可以創建一個scrapy項目

2、scrapyd是什麼?
相當於一個組件,能夠將scrapy項目進行遠程部署,調度使用等

因此scrapyd可以看作一個cs(client-server)程序,因此毫無疑問我們需要安裝和配置scrapyd(server)和連接的scrapy-client(client)。

(1)實現項目部署前的準備(安裝相關庫)

1.scrapyd庫的安裝:
命令:pip install scrapyd
安裝成功的現象:輸入scrapyd,scrapyd帶有一個最小的Web界面,啓動後,通過訪問http://localhost:6800。如下圖:
在這裏插入圖片描述

2.scrapyd-client庫的安裝:
命令:pip install scrapyd-client
安裝成功現象:到scrapy項目下面 輸入scrapyd-deploy 出現:Unknown target: default

(2)修改scrapy.cfg文件:

# Automatically created by: scrapy startproject
#
# For more information about the [deploy] section see:
# https://scrapyd.readthedocs.io/en/latest/deploy.html

[deploy:spider55]                   #1.部署名稱,可寫可不寫!
url = http://localhost:6800/      #2.url必須有,可以是遠程服務器
project = douban                  #3.項目名稱(不能刪除)
username=cool					  #訪問服務器所需的用戶名和密碼(可不寫)
password=123456

執行命令發現設置成功:
命令:scrapyd-deploy -l
作用:查看設置的部署名稱和url
在這裏插入圖片描述

(3)執行打包命令:(在有scrapy.cfg的目錄下)

{部署操作會打包你當前項目。從返回的結果裏面,可以看到部署的狀態,項目名稱,版本號和爬蟲個數,以及當前主機名稱。}

#				部署名稱     項目名稱
scrapyd-deploy spider55 -p douban

#效果:
'''
出現  {"node_name": "YNRBYA8RP4AT92A", "status": "ok", "project": "douban", "version": "1588083324", "spiders": 2}
'''

(4)啓動爬蟲:

#啓動爬蟲命令:								     項目名稱 		 爬蟲名稱
curl http://localhost:6800/schedule.json -d project=douban   -d spider=db   

#注意:啓動爬蟲之後一定要立馬往數據庫裏寫入起始url: 
lpush db:start_urls https://movie.com/top250

#啓動爬蟲之後控制檯輸出:
{"node_name": "YNRBYA8RP4AT92A", "status": "ok", "jobid": "16903afa895b11ea886298fa9b72ce54"}

現在就可以在網頁中查看想要看到的一些日誌信息等

scrapyd的web界面比較簡單,主要用於監控,所有的調度工作全部依靠接口實現.
官方文檔:
https://scrapyd.readthedocs.io/en/latest/api.html

(5)關閉爬蟲:

#關閉爬蟲命令:								項目名稱 					在網頁中的job裏的job的id值,如下圖:
curl http://localhost:6800/cancel.json -d project=douban -d job=4fc26e4209da11e9b344000c292b8398

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章