小白進階之Scrapy第六篇Scrapy-Redis詳解(轉)

Scrapy-Redis 詳解

一般咱們在一個站站點進行採集的時候,若是是小站的話 咱們使用scrapy自己就能夠知足。html

可是若是在面對一些比較大型的站點的時候,單個scrapy就顯得力不從心了。ios

要是咱們可以多個Scrapy一塊兒採集該多好啊 人多力量大。git

很遺憾Scrapy官方並不支持多個同時採集一個站點,雖然官方給出一個方法:github

**將一個站點的分割成幾部分 交給不一樣的scrapy去採集**redis

彷佛是個解決辦法,可是很麻煩誒!畢竟分割很麻煩的哇json

下面就改輪到咱們的額主角Scrapy-Redis登場了!cookie

什麼??你這麼就登場了?還沒說爲何呢?

好吧 爲了簡單起見 就用官方圖來簡單說明一下:app

這張圖你們相信你們都很熟悉了。重點看一下SCHEDULERscrapy

1. 先來看看官方對於SCHEDULER的定義:ide

**SCHEDULER接受來自Engine的Requests,並將它們放入隊列(能夠按順序優先級),以便在以後將其提供給Engine**

點我看文檔

2. 如今咱們來看看SCHEDULER都提供了些什麼功能:

根據官方文檔說明 在咱們沒有沒有指定 SCHEDULER 參數時,默認使用:’scrapy.core.scheduler.Scheduler’ 做爲SCHEDULER(調度器)

scrapy.core.scheduler.py:

只挑了一些重點的寫了一些註釋剩下你們本身領會(纔不是我懶哦 )

從上面的代碼 咱們能夠很清楚的知道 SCHEDULER的主要是完成了 push Request pop Request 和 去重的操做。

並且queue 操做是在內存隊列中完成的。

你們看queuelib.queue就會發現基於內存的(deque)

那麼去重呢?

按照正常流程就是你們都會進行重複的採集;咱們都知道進程之間內存中的數據不可共享的,那麼你在開啓多個Scrapy的時候,它們相互之間並不知道對方採集了些什麼那些沒有沒采集。那就你們夥兒本身玩本身的了。徹底沒沒有效率的提高啊!

怎麼解決呢?

這就是咱們Scrapy-Redis解決的問題了,不能協做不就是由於Request 和 去重這兩個 不能共享嗎?

那我把這兩個獨立出來好了。

將Scrapy中的SCHEDULER組件獨立放到你們都能訪問的地方不就OK啦!加上scrapy-redis後流程圖就應該變成這樣了?

So············· 這樣是否是看起來就清楚多了???

下面咱們來看看Scrapy-Redis是怎麼處理的?

scrapy_redis.scheduler.py:

class Scheduler(object):
    """Redis-based scheduler
 
    Settings
    --------
    SCHEDULER_PERSIST : bool (default: False)
        Whether to persist or clear redis queue.
    SCHEDULER_FLUSH_ON_START : bool (default: False)
        Whether to flush redis queue on start.
    SCHEDULER_IDLE_BEFORE_CLOSE : int (default: 0)
        How many seconds to wait before closing if no message is received.
    SCHEDULER_QUEUE_KEY : str
        Scheduler redis key.
    SCHEDULER_QUEUE_CLASS : str
        Scheduler queue class.
    SCHEDULER_DUPEFILTER_KEY : str
        Scheduler dupefilter redis key.
    SCHEDULER_DUPEFILTER_CLASS : str
        Scheduler dupefilter class.
    SCHEDULER_SERIALIZER : str
        Scheduler serializer.
 
    """
 
    def __init__(self, server,
                 persist=False,
                 flush_on_start=False,
                 queue_key=defaults.SCHEDULER_QUEUE_KEY,
                 queue_cls=defaults.SCHEDULER_QUEUE_CLASS,
                 dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY,
                 dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS,
                 idle_before_close=0,
                 serializer=None):
        """Initialize scheduler.
 
        Parameters
        ----------
        server : Redis
            這是Redis實例
        persist : bool
            是否在關閉時清空Requests.默認值是False。
        flush_on_start : bool
            是否在啓動時清空Requests。 默認值是False。
        queue_key : str
            Request隊列的Key名字
        queue_cls : str
            隊列的可導入路徑(就是使用什麼隊列)
        dupefilter_key : str
            去重隊列的Key
        dupefilter_cls : str
            去重類的可導入路徑。
        idle_before_close : int
            等待多久關閉
 
        """
        if idle_before_close < 0:
            raise TypeError("idle_before_close cannot be negative")
 
        self.server = server
        self.persist = persist
        self.flush_on_start = flush_on_start
        self.queue_key = queue_key
        self.queue_cls = queue_cls
        self.dupefilter_cls = dupefilter_cls
        self.dupefilter_key = dupefilter_key
        self.idle_before_close = idle_before_close
        self.serializer = serializer
        self.stats = None
 
    def __len__(self):
        return len(self.queue)
 
    @classmethod
    def from_settings(cls, settings):
        kwargs = {
            'persist': settings.getbool('SCHEDULER_PERSIST'),
            'flush_on_start': settings.getbool('SCHEDULER_FLUSH_ON_START'),
            'idle_before_close': settings.getint('SCHEDULER_IDLE_BEFORE_CLOSE'),
        }
 
        # If these values are missing, it means we want to use the defaults.
        optional = {
            # TODO: Use custom prefixes for this settings to note that are
            # specific to scrapy-redis.
            'queue_key': 'SCHEDULER_QUEUE_KEY',
            'queue_cls': 'SCHEDULER_QUEUE_CLASS',
            'dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',
            # We use the default setting name to keep compatibility.
            'dupefilter_cls': 'DUPEFILTER_CLASS',
            'serializer': 'SCHEDULER_SERIALIZER',
        }
        # 從setting中獲取配置組裝成dict(具體獲取那些配置是optional字典中key)
        for name, setting_name in optional.items():
            val = settings.get(setting_name)
            if val:
                kwargs[name] = val
 
        # Support serializer as a path to a module.
        if isinstance(kwargs.get('serializer'), six.string_types):
            kwargs['serializer'] = importlib.import_module(kwargs['serializer'])
                # 或得一個Redis鏈接
        server = connection.from_settings(settings)
        # Ensure the connection is working.
        server.ping()
 
        return cls(server=server, **kwargs)
 
    @classmethod
    def from_crawler(cls, crawler):
        instance = cls.from_settings(crawler.settings)
        # FIXME: for now, stats are only supported from this constructor
        instance.stats = crawler.stats
        return instance
 
    def open(self, spider):
        self.spider = spider
 
        try:
              # 根據self.queue_cls這個能夠導入的類 實例化一個隊列
            self.queue = load_object(self.queue_cls)(
                server=self.server,
                spider=spider,
                key=self.queue_key % {'spider': spider.name},
                serializer=self.serializer,
            )
        except TypeError as e:
            raise ValueError("Failed to instantiate queue class '%s': %s",
                             self.queue_cls, e)
 
        try:
              # 根據self.dupefilter_cls這個能夠導入的類 實例一個去重集合
            # 默認是集合 能夠實現本身的去重方式 好比 bool 去重
            self.df = load_object(self.dupefilter_cls)(
                server=self.server,
                key=self.dupefilter_key % {'spider': spider.name},
                debug=spider.settings.getbool('DUPEFILTER_DEBUG'),
            )
        except TypeError as e:
            raise ValueError("Failed to instantiate dupefilter class '%s': %s",
                             self.dupefilter_cls, e)
 
        if self.flush_on_start:
            self.flush()
        # notice if there are requests already in the queue to resume the crawl
        if len(self.queue):
            spider.log("Resuming crawl (%d requests scheduled)" % len(self.queue))
 
    def close(self, reason):
        if not self.persist:
            self.flush()
 
    def flush(self):
        self.df.clear()
        self.queue.clear()
 
    def enqueue_request(self, request):
      """這個和Scrapy自己的同樣"""
        if not request.dont_filter and self.df.request_seen(request):
            self.df.log(request, self.spider)
            return False
        if self.stats:
            self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
        # 向隊列裏面添加一個Request
        self.queue.push(request)
        return True
 
    def next_request(self):
      """獲取一個Request"""
        block_pop_timeout = self.idle_before_close
        # block_pop_timeout 是一個等待參數 隊列沒有東西會等待這個時間  超時就會關閉
        request = self.queue.pop(block_pop_timeout)
        if request and self.stats:
            self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
        return request
 
    def has_pending_requests(self):
        return len(self) > 0

 

來先來看看

 

以上就是Scrapy-Redis中的SCHEDULER模塊。下面咱們來看看queue和自己的什麼不一樣:

scrapy_redis.queue.py

以最經常使用的優先級隊列 PriorityQueue 舉例:

 

以上就是SCHEDULER在處理Request的時候作的操做了。

是時候來看看SCHEDULER是怎麼處理去重的了!

只須要注意這個?方法便可:

這樣你們就均可以訪問同一個Redis 獲取同一個spider的Request 在同一個位置去重,就不用擔憂重複啦

大概就像這樣:

  1. spider1:檢查一下這個Request是否在Redis去重,若是在就證實其它的spider採集過啦!若是不在就添加進調度隊列,等待別 人獲取。本身繼續幹活抓取網頁 產生新的Request了 重複以前步驟。
  2. spider2:以相同的邏輯執行

可能有些小夥兒會產生疑問了~~!spider2拿到了別人的Request了 怎麼能正確的執行呢?邏輯不會錯嗎?

這個不用擔憂啦 由於整Request當中包含了,全部的邏輯,回去看看上面那個序列化的字典。

總結一下:

  1. 1. Scrapy-Reids 就是將Scrapy本來在內存中處理的 調度(就是一個隊列Queue)、去重、這兩個操做經過Redis來實現
  2. 多個Scrapy在採集同一個站點時會使用相同的redis key(能夠理解爲隊列)添加Request 獲取Request 去重Request,這樣全部的spider不會進行重複採集。效率天然就嗖嗖的上去了。
  3. 3. Redis是原子性的,好處不言而喻(一個Request要麼被處理 要麼沒被處理,不存在第三可能)

另外Scrapy-Redis自己不支持Redis-Cluster,大量網站去重的話會給單機很大的壓力(就算使用boolfilter 內存也不夠整啊!)

改造方式很簡單:

  1.  使用 **rediscluster** 這個包替換掉自己的Redis鏈接
  2. Redis-Cluster 不支持事務,可使用lua腳本進行代替(lua腳本是原子性的哦)
  3. **注意使用lua腳本 不能寫佔用時間很長的操做**(畢竟一大羣人等着操做Redis 你總不能讓人家等着吧)

以上!完畢

對於懶人小夥伴兒 看看這個我改好的: 集羣版Scrapy-Redis **PS: 支持Python3.6+ 哦 ! 其他的版本沒測試過**

 

轉載請註明:靜覓 » 小白進階之Scrapy第六篇Scrapy-Redis詳解

相關文章
相關標籤/搜索