Python自動化開發學習-分佈式爬蟲(scrapy-redis)

scrapy-redis

講師的博客:https://www.cnblogs.com/wupeiqi/p/6912807.html
scrapy-redis是一個基於redis的scrapy組件,通過它可以快速實現簡單分佈式爬蟲程序,該組件本質上提供了三大功能:

  • scheduler : 調度器
  • dupefilter : URL去重規則(被調度器使用)
  • pipeline : 數據持久化

準備工作

安裝模塊

pip install scrapy-redis

創建爬蟲應用
項目就不重新創建了,直接在之前Scrapy課程的項目裏,再創建一個新的應用:

> cd PeppaScrapy
> scrapy genspider [項目名稱] [起始url]

通過環境變量指定配置文件
之前的課程上,已經對配置文件做了一些設置了。這裏既不想把之前的內容覆蓋掉,也不想受到之前配置的影響。
可以通過命令行的-s參數或者是在類裏寫一個名稱爲custom_settings的字典,一個一個參數的進行設置。這兩個的優先級都很高。但是都不是配置文件的形式,這裏可以單獨再寫一個配置文件,然後通過設置系統環境變量的方式來指定爬蟲應用加載的配置文件。
在原來的settings.py的同級目錄裏創建一個mysettings.py的配置文件,文件可以在任意位置,只要能配python導入。然後設置一個系統環境變量即可:

import os
os.environ.setdefault('SCRAPY_SETTINGS_MODULE', 'PeppaScrapy.mysettings')

連接redis設置

把設置都寫在配置文件中即可:

# redis 配置文件
REDIS_HOST = 'localhost'  # 主機名
REDIS_PORT = 6379  # 端口
# REDIS_URL = 'redis://user:pass@hostname:9001'  # 連接URL,和上面2條一樣,也是連接redis的設置,優先使用這個
# REDIS_PARAMS  = {}  # Redis連接參數,這裏有一些默認值,可以去源碼裏看
# REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'  # 指定連接Redis的Python模塊,默認:redis.StrictRedis
# REDIS_ENCODING = "utf-8"  # redis編碼類型,默認:'utf-8'

連接參數
這裏提供兩種方式連接,一種是前2個設置,指定HOST和PORT。如果還需要用戶名和密碼就沒辦法了。
另一種就是用一個REDIS_URL參數,按照上面的格式把所有的信息填好。另外這裏REDIS_URL參數的優先級高,就是說如果設置了REDIS_URL參數,那麼上面的2個參數就沒有效果了。這個邏輯可以在scrapy_redis.connection.py裏的get_redis函數裏找到。

其他連接參數
REDIS_PARAMS,是連接redis時使用的其他參數。所以其實用戶名,密碼也是可以寫在這裏面的。即使什麼都不寫,scrapy-redis模塊本身也設置了一些默認值,可以在scrapy_redis.defaults.py裏找到:

REDIS_PARAMS = {
    'socket_timeout': 30,
    'socket_connect_timeout': 30,
    'retry_on_timeout': True,
    'encoding': REDIS_ENCODING,  // 這個值默認是'utf-8',也在這個文件裏
}

具體還可以設置哪些參數,就是看連接Redis的那個類的構造函數了,源碼裏就是用這個字典直接**kwargs創建對象了。默認的用來連接Redis的模塊是redis.StrictRedis,下面是這個類的構造函數的參數列表:

    def __init__(self, host='localhost', port=6379,
                 db=0, password=None, socket_timeout=None,
                 socket_connect_timeout=None,
                 socket_keepalive=None, socket_keepalive_options=None,
                 connection_pool=None, unix_socket_path=None,
                 encoding='utf-8', encoding_errors='strict',
                 charset=None, errors=None,
                 decode_responses=False, retry_on_timeout=False,
                 ssl=False, ssl_keyfile=None, ssl_certfile=None,
                 ssl_cert_reqs='required', ssl_ca_certs=None,
                 max_connections=None):

REDIS_ENCODING,是指定編碼類型,默認utf-8沒太多要說的。
用於連接redis的所有參數,除了以下4個是單獨寫的,其他的都寫在REDIS_PARAMS這個字典裏。按照上面構造函數裏的變量名稱寫。這4個可以單獨寫的,其實也就是做了一步映射而已:

# 這個也是 scrapy_redis.connection.py 裏的源碼
SETTINGS_PARAMS_MAP = {
    'REDIS_URL': 'url',
    'REDIS_HOST': 'host',
    'REDIS_PORT': 'port',
    'REDIS_ENCODING': 'encoding',
}

REDIS_PARAMS['redis_cls'],指定連接Redis的Python模塊。按上面說的,處理那4個參數,其他的都只能寫在REDIS_PARAMS這個字典裏。
源碼文件
上面這些主要是翻了3個文件裏的源碼:

  • scrapy_redis.defaults.py
  • scrapy_redis.connection.py
  • redis.client.py

URL去重

模塊提供了一個使用redis做去重的規則,只需要按之前在Scrapy課程裏學的,設置自定的去重規則即可。具體就是加上一條配置:

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

url去重算法
這裏的去重規則,會先把url轉換成唯一標識,然後再存到集合裏。
源碼裏會把url通過hashlib進行哈希運算,用的是SHA1·。把原本的字符串轉成數字簽名。這麼做的好處就是,即使url會很長,但是生成的數字簽名的長度是固定的。這裏可以注意下這個技巧,把url存儲在redis裏的時候,佔用的長度是固定的,關鍵是不會太長。
另外這個轉換過程還有個特點。如果url裏是帶get參數的,如果參數的值是一樣的,但是出現的順序不同,轉換後生成的唯一標識也是一樣的。比如像下面這樣:

url1 = 'https://www.baidu.com/s?wd=sha1&ie=utf-8'
url2 = 'https://www.baidu.com/s?ie=utf-8&wd=sha1'

像這種,只是參數的先後順序不同,兩個請求應該還是同一個請求,應該要去重。這裏在這種情況下生成的唯一標識是一樣的,所以可以做到去重。

這個去重規則實現了去重的算法,是要被調度器使用的。在去重的這個類裏,通過request_seen這個方法來判斷是否有重複。下面的調度器裏就會調用這個方法來判斷避免重複爬取。

調度器

在配置文件裏,加上下面的配置來指定調度器,這個是scrapy讀取的配置:

SCHEDULER = "scrapy_redis.scheduler.Scheduler"

接下來出一些在scrapy-redis模塊裏要讀取的配置。其中部分在scrapy_redis.defaults.py裏有默認設置,還有的在代碼裏也有默認的邏輯,所以配置文件裏可以什麼都不寫,需要改變設置的話也可以自己在配置文件裏指定。

使用的隊列

SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

指定調度器存取請求使用的隊列類型,有3個可選的:

  • PriorityQueue,默認。通過redis的有序集合來實現
  • FifoQueue,隊列。通過redis的List實現的,使用lpush和rpop進行存取
  • LifoQueue,棧。也是通過redis的List實現的,使用lpush和lpop進行存取

請求存放在redis中的key

SCHEDULER_QUEUE_KEY = '%(spider)s:requests'

這樣,不同的爬蟲應用在redis中可以使用不同的key存取請求。

對保存到redis中的數據進行序列化

SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"  

這個設置在defaults裏沒有,如果沒有設置,就用 scrapy_redis.picklecompat 這個。而底層用的還是pickle模塊。這樣可以把對象序列化之後在redis中保存起來。

清空調度器和去重記錄
下面的2個參數都是布爾型的,如果不設置就是False,如果需要開啓,就設置爲True:

SCHEDULER_PERSIST = True  # 是否在關閉時候保留,調度器和去重記錄
SCHEDULER_FLUSH_ON_START = True  # 是否在開始之前清空,調度器和去重記錄

上面兩個參數的效果都是判斷後決定是否要執行調度器的一個flush方法。而這個flush方法裏執行的則是清空調度器和去重記錄。
注意這兩個參數的效果。默認都是False,就是在開始之前不清空調度器和去重記錄,在結束的時候也不保留調度器和去重記錄。測試的話一般不需要保留。如果是上線使用,一般需要保留調度器和去重記錄,那麼就按下面來設置:

# 保留調度器和去重記錄的設置
SCHEDULER_PERSIST = True
SCHEDULER_FLUSH_ON_START = False

獲取請求時等待的時間
調度器獲取請求時,如果數據爲空,進行等待的時間,默認爲0,單位秒:

SCHEDULER_IDLE_BEFORE_CLOSE = 0 

這個實際就是redis的blpop和brpop操作時的timeout參數,默認爲0。如果爲0,就使用lpop和rpop這2個不阻塞的pop方法。如果大於0,就使用阻塞的pop方法。這裏不會用到timeout爲0的阻塞pop,所以不會一直阻塞,總會返回的。無論阻塞還是不阻塞,取不到值就返回None。
另外,調度器默認用的是有序集合,redis的有序集合取值沒有阻塞也沒有timeout,所以這個值是無效的。

去重規則的參數
下面2個是去重規則使用的參數,其中一個在去重的時候講過了。另一個就是在redis裏使用的key:

SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'  # 去重規則在redis中保存時對應的key
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'  # 去重規則對應處理的類

數據持久化

模塊還提供了數據持久化,在scrapy的配置裏指定好數據持久化使用的類:

ITEM_PIPELINES = {
   'scrapy_redis.pipelines.RedisPipeline': 300,
}

# 下面是兩個持久化時可以定製的配置
# PIPELINE_KEY = '%(spider)s:items'
# REDIS_ITEMS_SERIALIZER = "scrapy.utils.serialize.ScrapyJSONEncoder"

先去看看這個類裏具體做了些什麼:

from twisted.internet.threads import deferToThread

class RedisPipeline(object):

    def process_item(self, item, spider):
        return deferToThread(self._process_item, item, spider)

    def _process_item(self, item, spider):
        key = self.item_key(item, spider)
        data = self.serialize(item)
        self.server.rpush(key, data)
        return item

先看process_item方法,返回一個deferToThread,在twisted裏的相當於拿一個線程來做一個事。具體twisted幹了啥就忽略把,主要是處理_process_item這個方法。所以真正做的處理是在_process_item方法裏。
這裏先把item的數據通過self.serialize函數做序列化,然後就是下面的rpush存到redis裏去了。這裏有2個可以定製的參數,一個是序列化的方法,一個是存儲的redis裏使用的Key。
默認使用下面的Key,當然可以在配置文件裏指定:

PIPELINE_KEY = '%(spider)s:items'

默認使用的序列化的方法是一個做了一些定製的json方法,在下面這裏:

from scrapy.utils.serialize import ScrapyJSONEncoder

也是可以通過參數來指定自己的序列化方法的:

REDIS_ITEMS_SERIALIZER = "scrapy.utils.serialize.ScrapyJSONEncoder"

爬蟲和起始url

之前在spiders文件夾下寫爬蟲的時候,都是繼承scrapy.Spider然後寫自己的類。
模塊也提供了一個它自己的類,可以直接繼承模塊提供的這個類來寫爬蟲:

from scrapy_redis.spiders import RedisSpider

class TestSpider(RedisSpider):
    name = 'jandan'
    allowed_domains = ['jandan.net']

用了模塊的爬蟲後,起始url也可以直接從redis裏獲取了,相關的配置有下面2個:

START_URLS_KEY = '%(name)s:start_urls'
START_URLS_AS_SET = False

第一個設置,是在redis裏用來存儲起始url的key,只有通過這個key才能從redis裏獲取到起始的url。
第二個設置,是指定在redis裏使用是否使用集合來存儲起始url,默認不用集合,用的就是列表。
使用模塊提供的這個爬蟲,會一直運行,永遠不會停止。沒有任務的話應該就是一直等着直到獲取到新的任務。可以往redis對應的起始url的key裏添加數據,就會自動的開始爬蟲。適合在線上使用。
而scrapy模塊提供的scrapy.Spider這個爬蟲適合平時自己用,爬完就結束了。

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