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