數據採集工做中,不免會遇到增量採集。而在增量採集中,如何去重是一個大問題,由於實際的須要採集的數據也許並很少,但每每要在判斷是否已經採集過這件事上花點時間。好比對於資訊採集,若是發佈網站天天只更新幾條或者根本就不更新,那麼如何讓採集程序每次只採集這更新的幾條(或不採集)是一件很簡單的事,數據庫就是一種實現方式。不過當面臨大量的目標網站時,每次採集前也許就須要先對數據庫進行大量的查詢操做,這是一件費時的事情,不免下降採集程序的性能,使得每次採集耗時變大。本文從資訊採集角度出發,以新浪新聞排行(地址:http://news.sina.com.cn/hotnews/)資訊採集爲例,針對增量採集提出了去重方案。css
考慮到數據庫查詢亦需耗費時間,所以去重字段可單獨存放到一張表上,或使用 redis 等查詢耗時較少的數據庫。通常來講,能夠將文章詳情頁源地址做爲去重字段。考慮到某些鏈接字符過長,可使用 md5 將其轉化爲統一長度的字符。然後在每次採集時,先抓取列表頁,解析其中的文章信息,將連接 md5 化,而後再將獲得的結果到數據庫中查詢,若是存在則略過,不然深刻採集。以 redis 爲例,實現代碼以下:html
import hashlib import redis import requests import parsel class NewsSpider(object): start_url = 'http://news.sina.com.cn/hotnews/' def __init__(self): self.db = RedisClient('news') def start(self): r = requests.get(self.start_url) for url in self.parse_article(r): fingerprint = get_url_fingerprint(url) if self.db.is_existed(fingerprint): continue try: self.parse_detail(requests.get(url)) except Exception as e: print(e) else: # 若是解析正常則將地址放入數據庫 self.db.add(fingerprint) def parse_article(self, response): """解析文章列表頁並返回文章地址""" selector = parsel.Selector(text=response.text) # 獲取全部文章地址 return selector.css('.ConsTi a::attr(href)').getall() def parse_detail(self, response): """詳情頁解析邏輯省略""" pass class RedisClient(object): """Redis 客戶端""" def __init__(self, key): self._db = redis.Redis( host='localhost', port=6379, decode_responses=True ) self.key = key def is_existed(self, value): """檢測 value 是否已經存在 若是存在則返回 True 不然 False """ return self._db.sismember(self.key, value) def add(self, value): """存入數據庫""" return self._db.sadd(self.key, value) def get_url_fingerprint(url): """對 url 進行 md5 加密並返回加密後的字符串""" md5 = hashlib.md5() md5.update(url.encode('utf-8')) return md5.hexdigest()
注意以上代碼中,對請求的 md5 並無考慮對 POST 請求的 md5 加密,若有須要可自行實現。nginx
設若咱們有大量的列表頁中的文章須要採集,而距離上次採集時,有的更新了幾條新聞,有的則沒有更新。那麼如何判斷數據庫去重雖然能解決具體文章的去重,但對於文章索引頁仍須要每次請求,由於 HEAD 請求所花費的時間要比 GET/POST 請求要少,因此可用 HEAD 請求獲取索引頁的相關信息,從而判斷索引頁是否有更新。經過對目標地址請求能夠查看具體信息:redis
>>> r = requests.head('http://news.sina.com.cn/hotnews/') >>> for k, v in r.headers.items(): ... print(k, v) ... Server: nginx Date: Fri, 31 Jan 2020 09:33:22 GMT Content-Type: text/html Content-Length: 37360 Connection: keep-alive Vary: Accept-Encoding ETag: "5e33f39e-28e01"V=CCD0B746 X-Powered-By: shci_v1.03 Expires: Fri, 31 Jan 2020 09:34:16 GMT Cache-Control: max-age=60 Content-Encoding: gzip Age: 6 Via: http/1.1 ctc.guangzhou.union.182 (ApacheTrafficServer/6.2.1 [cSsNfU]), http/1.1 ctc.chongqing.union.138 (ApacheTrafficServer/6.2.1 [cHs f ]) X-Via-Edge: 158046320209969a5527d9b2299db18a555d7 X-Cache: HIT.138 X-Via-CDN: f=edge,s=ctc.chongqing.union.144.nb.sinaedge.com,c=125.82.165.105;f=Edge,s=ctc.chongqing.union.138,c=219.153.34.144
在此,基於 HTTP 緩存機制,咱們有如下幾種方式來判斷頁面是否有更新:數據庫
注意以上信息中的 Content-Length
字段,它指明瞭索引頁字符的長度。因爲通常有更新的頁面字符長度也會有所不一樣,所以可使用它來斷定索引頁是否有更新。緩存
Etag
響應頭字段表示資源的版本,在發送請求時帶上 If-None-Match
頭字段,來詢問服務器該版本是否仍然可用。若是服務器發現該版本仍然是最新的,就能夠返回 304
狀態碼指示 UA
繼續使用緩存,那麼就不用再採集具體的頁面了。本例中亦可以使用:bash
# ETag 完整字段爲 "5e33f39e-28e01"V=CCD0B746 # 實際須要的則是 5e33f39e-28e01 >>> r = requests.get('http://news.sina.com.cn/hotnews/', headers={'If-None-Match': '5e33f39e-28e01'}) >>> r.status_code 304 >>> r.text ''
本例並無 Last-Modified
字段,不過考慮到其餘網站可能會有該字段,所以亦可考慮做爲去重方式之一。該字段與 Etag
相似,Last-Modified
HTTP 響應頭也用來標識資源的有效性。不一樣的是使用修改時間而不是實體標籤。對應的請求頭字段爲 If-Modified-Since
。服務器
以上三種均可以將上次請求獲得的信息存入到數據庫中,再次請求時則取出相應信息併發送相應請求,若是與本地一致(或響應碼爲 304)則判斷爲未更新,否則則繼續請求。鑑於有的網站並無實現 HTTP 緩存機制,有的則只實現某一種。所以能夠考慮在採集程序中將以上三種機制所有實現,從而保證最大化的減小無效請求。具體實現思路爲:併發
請求目標網站 - 以 Content-Length 判斷 -> HEAD 請求 與本地存儲長度對比 一致 -> 忽略 不一致 -> 發送 GET/POST 請求 同時更新本地數據 - 以 ETag/Last-Modified 判斷 -> GET/POST 請求 並在請求頭中帶上相應信息 判斷響應碼 304 -> 忽略 200 -> 解析並更新本地數據 - 無相應字段 -> GET/POST 請求
根據 HTTP 緩存機制並不能完美的適配全部網站,所以,能夠記錄各個目標網站的更新頻率,並將更新較爲頻繁的網站做爲優先採集對象。ide