引入
在我们爬取某些网站时会遇到一些问题? 某些网站会定时在原有网页数据的基础上更新一批数据.
例如某电影网站会实时更新一批最近热门的电影. 小说网站会根据作者创作的进度实时更新最新的章节数据等等.
那么遇到类似的场景, 我们就可以采用增量式爬虫了
而增量式爬虫分为两个步骤:
增量爬取
爬取结果去重
增量爬取
一个站点更新也会出现下面两种情况:
1, 单个页面数据更新
当出现这种情况的时候, 我们对此特定页面的内容做哈希, 当然要去除动态变化的那一部分, 比如有的页面有验证码或者日期, 程序定期执行, 在执行的最开始检测此页面的哈希值跟上次抓取是否有变化, 如果有变化就开始抓取.
2, 新增了页面
如果是新增页面呢, 我们会对页面入口内容做哈希, 并且存储分页面的 URL 哈希值, 如果页面入口哈希值发生变化, 获取新增的页面 url 列表, 在这里需要用到 url 的去重, 和数据去重类似, 采用 Redis 集合类型处理.
Redis 集合类型不允许添加重复的数据, 当添加重复的时候时, 返回 0, 并且添加失败. 我们将所有的 url list 存入 Redis 集合, 当页面入口变化时, 进行页面 url 去重, 只抓取新增的页面.
爬取结果去重
结果去重也有以下两种常用的方法:
布隆过滤器
其中布隆过滤器是通过写文件的方式, 多个进程使用需要添加同步和互斥, 较为繁琐, 不推荐多线程 / 进程的时候使用, 另外写文件是磁盘 I/O 操作, 耗费时间长, 可以累积到一定数量再一次写入, 或者利用上下文管理器在程序结束或异常退出时一次性写入.
- class Spider(object):
- def __init():
- # 布容过滤器初始化
- self.burongname = 'test.bl'
- if not os.path.isfile(self.burongname):
- self.bl = BloomFilter(capacity=100000, error_rate=0.000001)
- else:
- with open(self.burongname, 'rb') as f:
- self.bl = BloomFilter.fromfile(f)
- def __enter__(self):
- u"""
- 上下文管理器进入入口
- """
- return self
- def __exit__(self, *args):
- u"""
- 上下文管理器, 退出出口
- """
- if self.conn is not None:
- self.conn.close()
- with open(self.burongname, 'wb') as f:
- self.fingerprints.tofile(f)
- def get_infos(self):
- """
- 抓取主函数
- """
- # 布隆过滤器使用部分, x 为抓取到得数据
- x = JSON.dumps(i)
- if x not in self.bl:
- self.bl.add(x)
- if __name__ == '__main__':
- with Spider() as MSS:
- MSS.get_infos()
上下文管理器, 在主函数执行之前执行 def enter , 在程序运行结束或异常退出时执行 def exit, 上下文管理器还可以用来统计程序执行的时间.
Redis 集合
使用 Redis 集合去重能够支持多线程多进程.
利用 Redis 集合无重复数据的特点, 在 Redis 建立集合, 往其中添加数据的 sha1 值, 添加成功返回 1, 表示无重复, 添加失败返回 0, 表示集合中已经有重复数据
使用步骤:
建立 Redis 连接池
重复检查
下面的例子是接口, 并提供 example.
- [Redis]
- server=192.168.0.100
- pass=123@123
- import sys
- import hashlib
- import os
- import codecs
- import ConfigParser
- import Redis
- """
- 利用 redis 的集合不允许添加重复元素来进行去重
- """
- def example():
- pool, r = redis_init()
- temp_str = "aaaaaaaaa"
- result = check_repeate(r, temp_str, 'test:test')
- if result == 0:
- print ("重复")
- else:
- print ("不重复")
- redis_close(pool)
- def redis_init(parasecname="Redis"):
- """
- 初始化 redis
- :return: redis 连接池
- """
- cur_script_dir = os.path.split(os.path.realpath(__file__))[0]
- cfg_path = os.path.join(cur_script_dir, "db.conf")
- cfg_reder = ConfigParser.ConfigParser()
- secname = parasecname
- cfg_reder.readfp(codecs.open(cfg_path, "r", "utf_8"))
- redis_host = cfg_reder.get(secname, "server")
- redis_pass = cfg_reder.get(secname, "pass")
- # Redis
- pool = Redis.ConnectionPool(host=redis_host, port=6379, db=0, password=redis_pass)
- r = Redis.Redis(connection_pool=pool)
- return pool, r
- def sha1(x):
- sha1obj = hashlib.sha1()
- sha1obj.update(x)
- hash_value = sha1obj.hexdigest()
- return hash_value
- def check_repeate(r, check_str, set_name):
- """
- 向 redis 集合中添加元素, 重复则返回 0, 不重复则添加成功, 并返回 1
- :param r:redis 连接
- :param check_str: 被添加的字符串
- :param set_name: 项目所使用的集合名称, 建议如下格式:"projectname:task_remove_repeate"
- """
- hash_value = sha1(check_str)
- result = r.sadd(set_name, hash_value)
- return result
- def redis_close(pool):
- """
- 释放 redis 连接池
- """
- pool.disconnect()
- if __name__ == '__main__':
- example()
来源: https://www.cnblogs.com/peng104/p/10428394.html