scrapy自定義擴展(extensions)實現實時監控scrapy爬蟲的運行狀態

效果圖:
在這裏插入圖片描述

廢話

如何知道你寫的爬蟲有沒有正常運行,運行了多長時間,請求了多少個網頁,抓到了多少條數據呢?官方其實就提供了一個字典就包含一些抓取的相關信息:crawler.stats.get_stats(),crawler是scrapy中的一個組件。你可以在很多組件中訪問他,比如包含from_crawler(cls, crawler)方法的所有組件。

既然能得到scrapy的運行狀態,想要實時顯示出來應該也很簡單吧。同樣是使用上一篇博客用到的influxdb+grafana來展示數據,我們只需要將scrapy的一些運行信息實時同步到influxdb這個數據庫,就能通過grafana以圖的形式來展示出數據庫裏的內容了。

寫數據庫

如何實時將字典同步到數據庫呢?這裏肯定要設定一個同步的時間間隔,假設是5秒。那麼我們的需求就是讓scrapy每5秒寫一次爬蟲運行狀態的信息到數據庫,上面提到能訪問到crawler.stats.get_stats()這個的組件有很多,比如中間件、管道、爬蟲。我們應該在哪個組件中同步信息?

這個我們可以先看一些內置的組件分別實現了什麼功能,然後看和需求最相似的功能。很明顯,功能最爲合適的是extensions這個組件了,有很多人可能都沒有用過這個組件,我在看很多博客都很少有提到這個組件的,因爲這個組件能做的事,其他也能做,用它只是爲了讓分工更明確而已。所以一些額外的功能一般寫入extensions,我們先看看內置的幾個都實現了什麼樣的功能

  • 日誌統計信息擴展(scrapy.extensions.logstats.LogStats):記錄基本統計信息,例如抓取的頁面和已抓取的項目
  • 核心統計信息擴展(scrapy.extensions.corestats.CoreStats): 如果啓用了統計信息收集,請啓用核心統計信息的收集
  • Telnet控制檯擴展(scrapy.extensions.telnet.TelnetConsole): 提供一個telnet控制檯,以進入當前正在運行的Scrapy進程內的Python解釋器,這對於調試非常有用
  • 內存使用擴展(scrapy.extensions.memusage.MemoryUsage): 此擴展名在Windows中不起作用

其中日誌統計信息擴展就是把crawler.stats.get_stats()這個字典信息寫入到日誌,這和我要實現的功能基本類似。所以代碼可以參考參考。直接看我的代碼:

import logging
from scrapy import signals
import datetime
from threading import Timer
from influxdb import InfluxDBClient


logger = logging.getLogger(__name__)

class SpiderStatLogging:

    def __init__(self, crawler, dbparams, interval):
        self.exit_code = False
        self.interval = interval
        self.crawler = crawler
        self.client = InfluxDBClient(**dbparams)
        self.stats_keys = set()
        self.cur_d = {
            'log_info': 0, 
            'log_warning': 0,
            'requested': 0,
            'request_bytes': 0,
            'response': 0,
            'response_bytes': 0,
            'response_200': 0,
            'response_301': 0,
            'response_404': 0,
            'responsed': 0,
            'item': 0,
            'filtered': 0,
        }

    @classmethod
    def from_crawler(cls, crawler):
        dbparams = crawler.settings.get('INFLUXDB_PARAMS')
        interval = crawler.settings.get('INTERVAL', 60)
        ext = cls(crawler, dbparams, interval)
        crawler.signals.connect(ext.engine_started, signal=signals.engine_started)
        crawler.signals.connect(ext.engine_stopped, signal=signals.engine_stopped)
        crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
        crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
        return ext

    def spider_closed(self, spider, reason):
        logger.info(self.stats_keys)
        influxdb_d = {
            "measurement": "spider_closed",
            "time": datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
            "tags": {
                'spider_name': spider.name
            },
            "fields": {
                        'end_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 
                        'reason': reason,
                        'spider_name':spider.name
                    }
        }
        if not self.client.write_points([influxdb_d]):
            raise Exception('寫入influxdb失敗!')
        
    def spider_opened(self, spider):
        influxdb_d = {
            "measurement": "spider_opened",
            "time": datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
            "tags": {
                'spider_name': spider.name
            },
            "fields": {
                        'start_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                        'spider_name':spider.name
                    }
        }
        if not self.client.write_points([influxdb_d]):
            raise Exception('寫入influxdb失敗!')

    def engine_started(self):
        Timer(self.interval, self.handle_stat).start()
    
    def engine_stopped(self):
        self.exit_code = True

    def handle_stat(self):
        stats = self.crawler.stats.get_stats()
        d = {
            'log_info': stats.get('log_count/INFO', 0), 
            'dequeued': stats.get('scheduler/dequeued/redis', 0),
            'log_warning': stats.get('log_count/WARNING', 0),
            'requested': stats.get('downloader/request_count', 0),
            'request_bytes': stats.get('downloader/request_bytes', 0),
            'response': stats.get('downloader/response_count', 0),
            'response_bytes': stats.get('downloader/response_bytes', 0),
            'response_200': stats.get('downloader/response_status_count/200', 0),
            'response_301': stats.get('downloader/response_status_count/301', 0),
            'response_404': stats.get('downloader/response_status_count/404', 0),
            'responsed': stats.get('response_received_count', 0),
            'item': stats.get('item_scraped_count', 0),
            'depth': stats.get('request_depth_max', 0),
            'filtered': stats.get('bloomfilter/filtered', 0),
            'enqueued': stats.get('scheduler/enqueued/redis', 0),
            'spider_name': self.crawler.spider.name
        }
        for key in self.cur_d:
            d[key], self.cur_d[key] = d[key] - self.cur_d[key], d[key]
        influxdb_d = {
            "measurement": "newspider",
            "time": datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
            "tags": {
                'spider_name': self.crawler.spider.name
            },
            "fields": d
        }
        if not self.client.write_points([influxdb_d]):
            raise Exception('寫入influxdb失敗!')
        self.stats_keys.update(stats.keys())
        if not self.exit_code:
            Timer(self.interval, self.handle_stat).start()

代碼應該不難理解,從settings.py中讀取兩個變量’INFLUXDB_PARAMS’、‘INTERVAL’,然後在引擎開始的時候開啓一個定時器,每隔INTERVAL秒執行一次handle_stat函數,handle_stat函數的功能就是把crawler.stats.get_stats()這個字典寫入到influxdb數據庫裏。接着只需要在配置文件中啓用這個擴展即可,

EXTENSIONS = {
    '項目名稱.文件名稱.SpiderStatLogging': 1,
    # 假設上面的代碼都保存在extensions.py中,放在和settings.py同級目錄,
    # 則可以寫成:項目名稱.extensions..SpiderStatLogging
}

展示數據庫

granfana我就不多說了,不懂的請百度,或者看一下我的上一篇博客再百度。

圖表json:https://lanzous.com/icrr5kb(太長就直接放網盤了,複製到grafana導入即可)

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