19--Scarpy05:增量式爬蟲、分散式爬蟲

Edmond辉仔發表於2024-04-25

19--Scarpy05--增量式爬蟲、分散式爬蟲

一. 增量式爬蟲

顧名思義:可以對網站進行反覆抓取,然後發現新東西了就儲存起來,遇到了以前抓取過的內容就自動過濾掉即可

其核心思想:去重,並且可以反覆去重。隨時執行一下,將不同的資料儲存出來,相同的資料去除掉(不儲存)即可

增量爬蟲的核心:去除重複

  1. 去除url的重複

  2. 去除資料的重複

1.1 scrapy排程器去重原始碼分析

### 0 排程器是預設帶去除重複的
用的是python的集合


### 1 scrapy 預設配置
# scrapy/settings/defalut_settings.py 

# 預設的去重類
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'



### 2 排程器的核心原始碼
# scrapy/core/scheduler.py 

class Scheduler:
    ...
    # enqueue v.入隊
    def enqueue_request(self, request):
        """
        解讀:
        如果請求物件request的dont_filter(不過濾)引數 為False  # 就是請求要過濾
        且去重類的request_seen(request)方法為True     # 見過該請求,表示請求重複
        則 返回False  # 表示該請求,不進入請求佇列
        """
        if not request.dont_filter and self.df.request_seen(request):   ### 核心程式碼
            self.df.log(request, self.spider)
            return False
        dqok = self._dqpush(request)
        if dqok:
            self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
        else:
            self._mqpush(request)
            self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
        self.stats.inc_value('scheduler/enqueued', spider=self.spider)
        return True
    
    
    
### 3 預設去重類的原始碼
# scrapy/dupefilters.py 

class RFPDupeFilter(BaseDupeFilter):
    
    def __init__(self, path=None, debug=False):
        self.file = None
        self.fingerprints = set()   # 採用的是集合
        self.logdupes = True
        self.debug = debug
        self.logger = logging.getLogger(__name__)
        if path:
            self.file = open(os.path.join(path, 'requests.seen'), 'a+')
            self.file.seek(0)
            self.fingerprints.update(x.rstrip() for x in self.file)
            
    ...
    
    # 函式名的意思是 request物件 是否見過
    def request_seen(self, request):
        """
        :return為True,就不爬了 若為False,則繼續爬取
        """
        fp = self.request_fingerprint(request)
        if fp in self.fingerprints:  # 請求物件,如果在指紋集合,就返回True 表示見過該請求
            return True
        self.fingerprints.add(fp)
        if self.file:
            self.file.write(fp + '\n')
         
 

            
### 4 指紋生成 原理 request_fingerprint(request)
生成指紋,會把下面兩種地址生成一樣的指紋  # 本質是把?後面的引數排序,再生成指紋
  www.baidu.com?name=lqz&age=18     --指紋--> '4asda232' 長字串
  www.baidu.com?age=18&name=lqz     --指紋--> '4asda232' 長字串
# 上面兩個地址,若是直接放進集合中,會判定成兩個地址,但實質,這兩個是同一個地址請求 

1.2 自定義去重類 實現去重

### 0 原理
自定義去重類,替換掉內建的去重類


### 1 新建檔案 my_filters.py,自定義去重類MyFilter
from scrapy.dupefilters import BaseDupeFilter

from scrapy.utils.request import request_fingerprint


class MyFilter(BaseDupeFilter)   # 繼承BaseDupeFilter
    # 重寫 request_seen()
    def request_seen(self, request):
        fp = request_fingerprint(request)  # 將請求進行生成指紋,一堆長字串
        
        # 根據指紋,進行判斷
        若已經有該指紋,返回True,就不爬了
        若沒有該指紋,返回False,就繼續爬取

        
### 2 配置自定義的去重類  settings.py
DUPEFILTER_CLASS = 'scrapy.my_filters.MyFilter'

1.3 使用redis實現去重

方案是用redis的集合,進行去重,還可以選擇使用資料庫、mongodb進行過濾,原理都一樣

連線redis的兩個方案:

1.在spider中,重寫__init__()方法,在爬蟲類例項化物件時,連線資料庫

2.在pipeline中,open_spider()中,在爬蟲執行前,連線資料庫

去重的兩個方案:

1.url去重 優點: 簡單 缺點:若有新的資料產生,無法提取到最新的資料了

2.資料內容去重 優點: 保證資料的一致性 缺點: 需要每次都把資料從網頁中提取出來

案例:天涯為目標,來嘗試一下增量式爬蟲

  • spider.py:
import scrapy
from redis import Redis
from tianya.items import TianyaItem


class TySpider(scrapy.Spider):
    name = 'ty'
    allowed_domains = ['tianya.cn']
    start_urls = ['http://bbs.tianya.cn/list-worldlook-1.shtml']

    def __init__(self, name=None, **kwargs):
        """
        連線redis的方案1:重寫init方法,連線redis資料庫
        """
        self.red = Redis(password="123456", db=6, decode_responses=True)
        super().__init__(name, **kwargs)

    def parse(self, resp, **kwargs):
        tbodys = resp.css(".tab-bbs-list tbody")[1:]
        for tbody in tbodys:
            hrefs = tbody.xpath("./tr/td[1]/a/@href").extract()
            for h in hrefs:
                ### 去重的兩個方案:
                url = resp.urljoin(h)
                # 判斷在該set集合中是否有url資料
                r = self.red.sismember("tianya:details", url)  
                # 1.url去重. 優點: 簡單  缺點: 如果有人回覆了帖子.就無法提取到最新的資料了
                if not r:
                    yield scrapy.Request(url=resp.urljoin(h), callback=self.parse_details)
                else:
                    print(f"該url已經被抓取過{url}")

        next_href = resp.xpath("//div[@class='short-pages-2 clearfix']/div[@class='links']/a[last()]/@href").extract_first()
        yield scrapy.Request(url=resp.urljoin(next_href), callback=self.parse)

    def parse_details(self, resp, **kwargs):
        title = resp.xpath('//*[@id="post_head"]/h1/span[1]/span/text()').extract_first()
        content = resp.xpath('//*[@id="bd"]/div[4]/div[1]/div/div[2]/div[1]/text()').extract_first()
        item = TianyaItem()
        item['title'] = title
        item['content'] = content
        
        # 提取完資料. 將該url,新增到redis
        self.red.sadd("tianya:details", resp.url)
        
        return item
  • pipelines.py
from itemadapter import ItemAdapter
from redis import Redis
import json

class TianyaPipeline:

    def process_item(self, item, spider):
        # 2.資料內容去重. 優點: 保證資料的一致性. 缺點: 需要每次都把資料從網頁中提取出來
        print(json.dumps(dict(item)))
        
        r = self.red.sadd("tianya:pipelines:items", json.dumps(dict(item)))
        if r:
            # 進入資料庫
            print("存入資料庫", item['title'])
        else:
            print("已經在資料裡了", item['title'])
        return item

    
    def open_spider(self, spider):
        """連線redis的方案2"""
        self.red = Redis(password="123456", db=6)

        
    def close_spider(self, spider):
        self.red.close()

二. 分散式爬蟲

分散式爬蟲:就是搭建一個分散式的叢集,讓其對一組資源進行分佈聯合爬取

# 思考:既然要叢集來抓取,意味著會有好幾個爬蟲同時執行。此時產生一個問題, 如果有重複的url怎麼辦?  
在原來的程式中,scrapy中會由排程器來自動完成這個任務。
但是,此時是多個爬蟲一起跑,不同的機器之間是不能直接共享排程器的,怎麼辦? 


# 解決:
可以採用redis來作為各個爬蟲的排程器
此時引出一個新的模組叫scrapy-redis,在該模組中提供了這樣一組操作
它重寫了scrapy中的排程器,並將排程佇列和去除重複的邏輯,全部引入到了redis中

安裝scrapy-redis

pip install scrapy-redis==0.7.2

2.1 scrapy-redis工作流程

1.某個爬蟲從redis_key獲取到起始url,傳遞給引擎, 到排程器
然後把起始url直接丟到redis的請求佇列裡,開始了scrapy的爬蟲抓取工作

2.若抓取過程中產生了新的請求,不論是哪個節點產生的
最終都會到redis的去重集合中,進行判定是否抓取過

3.若抓取過,直接就放棄該請求
如果沒有抓取過,自動丟到redis請求佇列中 

4.排程器繼續從redis請求佇列裡,獲取要進行抓取的請求,完成爬蟲後續的工作

2.2 scrapy-redis實現分散式爬蟲

接下來用scrapy-redis完成上述流程

1.首先,建立專案 和以前一樣,該怎麼建立還怎麼建立

2.修改Spider. 將start_urls註釋掉. 更換成redis_key

# 修改Spider:在原來的基礎上,爬蟲類繼承RedisSpider

from scrapy_redis.spiders import RedisSpider

class CnblogsSpider(RedisSpider):
    name = 'cnblogs_redis'  # 爬蟲名
    allowed_domains = ['www.cnblogs.com']  # 允許爬取的域
    redis_key = 'myspider:start_urls'  # 設定redis中 存取起始頁的key
    # 以後爬蟲啟動後,並不會開始爬取,因為沒有起始地址
    # 起始地址 需要自己手動寫到redis中: redis-cli; lpush myspider:start_urls http://www.cnblogs.com/

    
    # 注意,版本相容的原因,需要自己手動加上這個方法,因為scrapy新版刪除該方法,但scrapy_redis 還在使用
    def make_requests_from_url(self, url):
        return scrapy.Request(url, dont_filter=True)

3.在配置檔案中,新增 redis、scrapy_redis 配置

### 1 redis配置資訊
REDIS_HOST = "127.0.0.1"  # 主機和埠,預設就是本地(可以不寫)
REDIS_PORT = 6379

# Redis預設資料庫
REDIS_DB = 8  

# Redis資料庫的連線密碼
REDIS_PARAMS = {
    "password":"123456"
}


### 2 scrapy-redis配置資訊  # 固定的

# 使用scrapy_redis的Scheduler,替換掉原來的Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"  

SCHEDULER_PERSIST = True  # 在關閉時,是否自動儲存請求資訊   persist v.存留


# 使用scrapy_redis的RFPDupeFilter,替換原來的去重類
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"


# 將分散式儲存到redis中
ITEM_PIPELINES = {
   'tianya2.pipelines.Tianya2Pipeline': 300,
   'scrapy_redis.pipelines.RedisPipeline': 301  # 配置redis的pipeline
}

4.在不同機器上啟動

# 1.啟動爬蟲
scrapy crawl cnblogs_redis   # 並不會立馬開始,因為redis中還沒有給定起始頁


# 2.在redis中寫入起始頁資料   redis_key
redis-cli lpush myspider:start_urls http://www.cnblogs.com/

2.3 布隆過濾器

2.3.1 資料去重的方案

1.直接用set集合來儲存url     # 最low的方案

2.用set集合儲存hash過的url   # scrapy預設

3.用redis來儲存hash過的請求  # scrapy-redis預設   如果請求非常非常多,redis壓力是很大的    實際已經夠了

4.用布隆過濾器 

2.3.2 布隆過濾器的原理

布隆過濾器:極小記憶體,快速校驗是否重複。其實它裡面就是一個改良版的bitmap,何為bitmap?

假設提前準備好一個陣列,首先把源資料經過hash計算,計算得出一個數字,然後按照該數字,來找到陣列下標對應的位置,設定成1

### 原理:
  BloomFilter 會開闢一個m位的bitArray(位陣列),開始所有資料全部置 0。
  當一個元素過來時,能過多個雜湊函式(h1,h2,h3....)計算不同的在雜湊值,
  並透過雜湊值找到對應的bitArray下標處,將裡面的值 0 置為 1 。
  關於多個雜湊函式,它們計算出來的值必須 [0,m) 之中。 
                             
  當來查詢對應的值時,同樣透過雜湊函式求值,再去尋找陣列的下標,
  如果所有下標都為1時,元素存在。當然也存在錯誤率。
 (如:當陣列存的資料特別多,導致陣列下班全部為1時,那麼查詢什麼都是存在的)
  但是這個錯誤率的大小,取決於陣列的位數和雜湊函式的個數

                       
### 部落格地址:
https://www.cnblogs.com/xiaoyuanqujing/protected/articles/11969224.html
https://developer.aliyun.com/article/773205                  
                       


### 舉例: 預設給定為10個長度陣列
[0],[0],[0],[0],[0],[0],[0],[0],[0],[0] 

## 1.存資料a、b            
a = 李嘉誠
b = 張翠山
....

hash(a) => 3   # 經過單個雜湊函式 計算
hash(b) => 4

# 修改 陣列[雜湊值] 的值  為1               
[0],[0],[0],[1],[1],[0],[0],[0],[0],[0]  


## 2.校驗資料 是否存在於布隆過濾器
找的時候,依然執行該hash演算法. 然後直接去找對應下標的位置看是否為1  # 是1就有, 不是1就沒有

c = '張三'
hash(c) => 6

# 查詢  陣列[雜湊值] 的值
去陣列中找6位置的數字,若是0,則不存在'張三'

但這樣有個不好的現象,容易誤判。若hash演算法選的不夠好,很容易搞錯,那怎麼辦?多選幾個hash演算法

a = 李嘉誠
b = 張翠山

[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]

hash1(a) = 3
hash2(a) = 4

hash1(b) = 2
hash2(b) = 5

[0],[0],[1],[1],[1],[1],[0],[0],[0],[0]

# 查詢的時候:
重新按照hash函式的順序, 再執行一遍,依然會得到多個值,
分別去這多個位置看是否是1
若全是1, 就存在    若有一個是0, 就沒有

2.4 scrapy-redis使用布隆過濾器

在scrapy-redis中,想要使用布隆過濾器是非常簡單的.

可以自定義實現布隆過濾器的邏輯,但建議直接用第三方,就可以了

# 1.安裝布隆過濾器
pip install scrapy_redis_bloomfilter


# 2.settings.py 配置
# 去重類,要使用 BloomFilter 請替換 DUPEFILTER_CLASS
DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"

# 雜湊函式的個數   預設為 6 可自行修改
BLOOMFILTER_HASH_NUMBER = 6

# BloomFilter的bit引數    預設為30  佔用 128MB空間  去重量級1億
BLOOMFILTER_BIT = 30    

相關文章