19--Scarpy05--增量式爬蟲、分散式爬蟲
一. 增量式爬蟲
顧名思義:可以對網站進行反覆抓取,然後發現新東西了就儲存起來,遇到了以前抓取過的內容就自動過濾掉即可
其核心思想:去重,並且可以反覆去重。隨時執行一下,將不同的資料儲存出來,相同的資料去除掉(不儲存)即可
增量爬蟲的核心:去除重複
-
去除url的重複
-
去除資料的重複
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