scrapy過濾器
1. 過濾器
當我們在爬取網頁的時候可能會遇到一個調轉連線會在不同頁面出現,這個時候如果我們的爬蟲程式不能識別出
該連結是已經爬取過的話,就會造成一種重複不必要的爬取。所以我們要對我們即將要爬取的網頁進行過濾,把重
復的網頁連結過濾掉。
2. 指紋過濾器
去重處理可以避免將重複性的資料儲存到資料庫中以造成大量的冗餘性資料。不要在獲得爬蟲的結果後進行內容過
濾,這樣做只不過是避免後端資料庫出現重複資料。
去重處理對於一次性爬取是有效的,但對於增量式爬網則恰恰相反。對於持續性長的增量式爬網,應該進行"前置過
濾",這樣可以有效地減少爬蟲出動的次數。在發出請求之前檢查詢爬蟲是否曾爬取過該URL,如果已爬取過,則讓爬
蟲直接跳過該請求以避免重複出動爬蟲。
Scrapy 提供了一個很好的請求指紋過濾器(Request Fingerprint duplicates filter)
scrapy.dupefilters.ReppupeFilter ,當它被啟用後,會自動記錄所有成功返回響應的請求的URL,並將其以檔案
(requests.seen)
方式儲存在專案目錄中。請求指紋過濾器的原理是為每個URL生成一個指紋並記錄下來,一旦
當前請求的URL在指紋庫中有記錄,就自動跳過該請求。
預設情況下這個過濾器是自動啟用的,當然也可以根據自身的需求編寫自定義的過濾器。
預設過濾器開啟的地方:
def start_requests(self):
for url in self.start_urls:
# 每一個url封裝成Request物件,交給排程器
# 這裡的dont_filter=True 就是預設開啟scrapy自帶的過濾器
yield Request(url, dont_filter=True)
2.1 過濾器原始碼
2.1.1 基本過濾器
這是一個基類,scrapy自帶的指紋過濾器就是繼承這個類,然後重寫這些方法實現的。
如果你經常看原始碼的話,你話發現很多功能的實現都有一個最基礎的基類,然後實現功能的那個類,繼承它
並對他的方法進行重寫。scrapy框架中就是這樣的。
class BaseDupeFilter:
# 基本的過濾器
@classmethod
def from_settings(cls, settings):
"""這個方法可以從settings.py中獲取資料"""
return cls()
def request_seen(self, request):
# 對 request 去重的方法
return False
def open(self): # can return deferred 爬蟲開啟的時候
pass
def close(self, reason): # can return a deferred 爬蟲關閉的時候
pass
def log(self, request, spider): # log that a request has been filtered 爬蟲的日誌
pass
2.2.2 預設的過濾器
繼承BaseDupeFilter類,然後對內部重寫。
class RFPDupeFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter 指紋過濾器 對整個request的去重"""
# 預設的話是一個指紋過濾器, 會對整個request物件進行過濾 (url/method/params...)
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) # 更新檔案
@classmethod
def from_settings(cls, settings):
# 從配置檔案中取到要應用的過濾器。
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(job_dir(settings), debug)
def request_seen(self, request):
# 拿到request 傳到 request_fingerprint方法 摘要出來的指紋 fp
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')
def request_fingerprint(self, request):
# 返回請求生成的指紋
return request_fingerprint(request)
def close(self, reason):
# 爬蟲結束關閉存放指紋的檔案
if self.file:
self.file.close()
def log(self, request, spider):
# 爬蟲日誌
if self.debug:
msg = "Filtered duplicate request: %(request)s (referer: %(referer)s)"
args = {'request': request, 'referer': referer_str(request)}
self.logger.debug(msg, args, extra={'spider': spider})
elif self.logdupes:
msg = ("Filtered duplicate request: %(request)s"
" - no more duplicates will be shown"
" (see DUPEFILTER_DEBUG to show all duplicates)")
self.logger.debug(msg, {'request': request}, extra={'spider': spider})
self.logdupes = False
spider.crawler.stats.inc_value('dupefilter/filtered', spider=spider)
執行流程:
細心的小夥伴可能發現了,這個和pipeline類原始碼的執行流程差不多,對沒錯就是差不多。
- 執行 from_settings(cls, settings): 這個類方法
- 執行 _ _init _ _ (self, path=None, debug=False): 例項化物件
- 執行 request_seen(self, request): 拿到由請求生產的指紋
- 執行 log(self, request, spider): 記錄日誌
- 執行 close(self, reason): 關閉記錄指紋的檔案
由於 scrapy.dupefilters.RFPDupeFilter 採用檔案方式儲存指紋庫,對於增量爬取且只用於短期執行的專案還能
應對。一旦遇到爬取量巨大的場景時,這個過濾器就顯得不太適用了,因為指紋庫檔案會變得越來越大,過濾器在啟動時會一次性將指紋庫中所有的URL讀入,導致消耗大量記憶體。
所以我們情況下,在使用scrapy過濾器的時候,都是自己重新自定義。
3. 自定義過濾器
雖然自帶的過濾器不好用,但是我們可以用Scrapy提供的 request_fingerprint
函式為請求生成指紋,然後將
指紋寫入記憶體,這樣會從記憶體中存取資料會很快。然後寫好的這個類,位置可以隨便放,但是一定要在settings.py
檔案中從新指定過濾器。
# 這裡是放在了當前專案的中介軟體裡面了.
DUPEFILTER_CLASS = 'qd_04_english.middlewares.URLFilter'
# 過濾器先啟動,再執行爬蟲
import hashlib
from scrapy.dupefilters import BaseDupeFilter
class URLFilter(BaseDupeFilter):
"""根據URL過濾"""
@classmethod
def from_settings(cls, settings):
# 從settings裡面取到配置檔案
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 過濾url的集合
self.url_set = set()
def request_seen(self, request):
"""對每一個請求進行過濾"""
url = self.request_fingerprint(request)
if url in self.url_set:
# 返回True就代表這個url已經被請求過了
return True
else:
self.url_set.add(request.url)
def request_fingerprint(self, request):
# 返回由url摘要後的字串
return hashlib.md5(request.url.encode()).hexdigest()
注意:start_urls 中的請求,預設是不過濾的。
4. 總結
之前我們在管道中,講到的資料去重,是對結果的去重,這裡我們講的過濾是對請求的去重。
一定一定要會看原始碼,會自定義一些元件。因為自帶的公司一般都不會用的,因為不好用。