增量采集中的几种去重方案

引言

数据采集工做中,不免会遇到增量采集。而在增量采集中,如何去重是一个大问题,由于实际的须要采集的数据也许并很少,但每每要在判断是否已经采集过这件事上花点时间。好比对于资讯采集,若是发布网站天天只更新几条或者根本就不更新,那么如何让采集程序每次只采集这更新的几条(或不采集)是一件很简单的事,数据库就是一种实现方式。不过当面临大量的目标网站时,每次采集前也许就须要先对数据库进行大量的查询操做,这是一件费时的事情,不免下降采集程序的性能,使得每次采集耗时变大。本文从资讯采集角度出发,以新浪新闻排行(地址: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
相关文章
相关标签/搜索