增量採集中的幾種去重方案

引言

數據採集工做中,不免會遇到增量採集。而在增量採集中,如何去重是一個大問題,由於實際的須要採集的數據也許並很少,但每每要在判斷是否已經採集過這件事上花點時間。好比對於資訊採集,若是發佈網站天天只更新幾條或者根本就不更新,那麼如何讓採集程序每次只採集這更新的幾條(或不採集)是一件很簡單的事,數據庫就是一種實現方式。不過當面臨大量的目標網站時,每次採集前也許就須要先對數據庫進行大量的查詢操做,這是一件費時的事情,不免下降採集程序的性能,使得每次採集耗時變大。本文從資訊採集角度出發,以新浪新聞排行(地址: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

根據 HTTP 緩存機制去重

設若咱們有大量的列表頁中的文章須要採集,而距離上次採集時,有的更新了幾條新聞,有的則沒有更新。那麼如何判斷數據庫去重雖然能解決具體文章的去重,但對於文章索引頁仍須要每次請求,由於 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 緩存機制,咱們有如下幾種方式來判斷頁面是否有更新:數據庫

1. Content-Length

注意以上信息中的 Content-Length 字段,它指明瞭索引頁字符的長度。因爲通常有更新的頁面字符長度也會有所不一樣,所以可使用它來斷定索引頁是否有更新。緩存

2. ETag

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
''

3. Last-Modified

本例並無 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

參考

  1. 使用 HTTP 緩存:Etag, Last-Modified 與 Cache-Control: https://harttle.land/2017/04/04/using-http-cache.html
相關文章
相關標籤/搜索