請求去重
這是爬蟲崗一道高頻出現(xiàn)的面試題:
Q:對于重復的請求浊闪,scrapy是如何去重的删铃?去重原理是什么?請求是如何計算唯一性的攒射?
帶著這個問題醋旦,進入今天的主題。
DUPEFILTER_CLASS
在scrapy項目配置中会放,DUPEFILTER_CLASS
是框架對請求去重規(guī)則的設(shè)置項饲齐。默認的類路徑:scrapy.dupefilters.RFPDupeFilter
。
進入到文件中咧最,觀察到類RFPDupeFilter繼承自BaseDupeFilter捂人,而BaseDupeFilter似乎什么都沒做,只是定義了一些方法矢沿。所以滥搭,真正的去重核心代碼都在RFPDupeFilter類中。逐行分析下其原理咨察。
RFPDupeFilter
class RFPDupeFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter"""
def __init__(self, path=None, debug=False):
self.file = None
# 用python內(nèi)置set()作為請求的指紋
# set的特性:無序不重復元素集
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论熙,就會持久化文件
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(job_dir(settings), debug)
def request_seen(self, request):
# !I阌脓诡!核心,用于檢測指紋是否存在媒役。
# 使用request_fingerprint來獲取請求的指紋
fp = self.request_fingerprint(request)
# 指紋在集合中祝谚,返回True
if fp in self.fingerprints:
return True
# 不在集合中,追加到集合里
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + '\n')
def request_fingerprint(self, request):
# 調(diào)用scrapy的request_fingerprint來進行指紋計算
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)
上述代碼非常簡單酣衷,簡單到任何人都可以自己輕松寫一個交惯。其中request_seen
方法用于檢測請求是否重復,返回True則重復,否則通過席爽。其中核心的是調(diào)用了request_fingerprint
來計算指紋意荤。進去看看。
request_fingerprint
The request fingerprint is a hash that uniquely identifies the resource the request points to
請求指紋是唯一標識請求指向的資源的哈希值
def request_fingerprint(request, include_headers=None, keep_fragments=False):
# 是否計算headers
if include_headers:
include_headers = tuple(to_bytes(h.lower()) for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
cache_key = (include_headers, keep_fragments)
if cache_key not in cache:
# 開始計算只锻,加密算法sha1
fp = hashlib.sha1()
# 將請求方式和請求url玖像,請求的body加入計算,
# 此處的url如果指向同一個資源齐饮,同樣認為一樣捐寥,比如:
# http://www.example.com/query?id=111&cat=222
# http://www.example.com/query?cat=222&id=111
# 這兩個url指向同一目標,我們也認為是重復的request.url
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url, keep_fragments=keep_fragments)))
fp.update(request.body or b'')
# headers加入計算
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[cache_key] = fp.hexdigest()
return cache[cache_key]
調(diào)度器的執(zhí)行流程
在scrapy的調(diào)度器代碼中Scheduler祖驱,通過類方法from_crawler
讀取配置項中DUPEFILTER_CLASS
的類路徑握恳,使用load_object
加載并通過create_instance
實例化對象。賦給屬性self.df
class Scheduler:
def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,
logunser=False, stats=None, pqclass=None, crawler=None):
self.df = dupefilter
……
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
dupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])
dupefilter = create_instance(dupefilter_cls, settings, crawler)
……
return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,
stats=crawler.stats, pqclass=pqclass, dqclass=dqclass,
mqclass=mqclass, crawler=crawler)
def open(self, spider):
……
return self.df.open()
def close(self, reason):
……
return self.df.close(reason)
def enqueue_request(self, request):
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False
……
return True
調(diào)度器被打開open捺僻、關(guān)閉close乡洼、請求入列enqueue_request的時候
分別觸發(fā)過濾器的打開open、關(guān)閉close陵像、計算指紋request_seen就珠。
當構(gòu)造請求時,參數(shù)dont_filter
為False的時候醒颖,才會進入去重計算妻怎。
新手經(jīng)常犯的錯。dont_filter
=True認為是去重泞歉。實際上國外人思維和我們直接表達不同逼侦。可能我們做參數(shù)就filter
=True是過濾腰耙,filter
=False就不過濾榛丢。加了dont,dont_filter=True 翻譯過來就是:不過濾挺庞?是的晰赞。
總結(jié)
現(xiàn)在再來回答面試官的問題:
Q:對于重復的請求,scrapy是如何去重的选侨?去重原理是什么掖鱼?請求是如何計算唯一性的?
A:scrapy是通過配置文件中DUPEFILTER_CLASS屬性來選擇去重的方法援制。默認情況下戏挡,是調(diào)用scrapy.dupefilters.RFPDupeFilter。
scrapy請求是通過Python內(nèi)置set不重復集合的特性來做本地去重的晨仑。
其加密算法是sha1褐墅。默認情況針對請求的方式拆檬、url、body來做唯一性計算妥凳。
核心兩點:set 指紋去重竟贯,sha1加密計算指紋。