# -*- coding: utf-8 -*- """ Created on 2018-12-13 21:08 --------- @summary: --------- @author: Boris @email: boris_liu@foxmail.com """ import copy from typing import Any, List, Union, Optional, Tuple, Callable from feapder.utils.tools import get_md5 from .bloomfilter import BloomFilter, ScalableBloomFilter from .expirefilter import ExpireFilter class Dedup: BloomFilter = 1 MemoryFilter = 2 ExpireFilter = 3 def __init__(self, filter_type: int = BloomFilter, to_md5: bool = True, **kwargs): """ 去重过滤器 集成BloomFilter、MemoryFilter、ExpireFilter Args: filter_type: 过滤器类型 BloomFilter name: 过滤器名称 该名称会默认以dedup作为前缀 dedup:expire_set:[name]/dedup:bloomfilter:[name]。 默认ExpireFilter name=过期时间; BloomFilter name=dedup:bloomfilter:bloomfilter absolute_name: 过滤器绝对名称 不会加dedup前缀,当此值不为空时name参数无效 expire_time: ExpireFilter的过期时间 单位为秒,其他两种过滤器不用指定 error_rate: BloomFilter/MemoryFilter的误判率 默认为0.00001 to_md5: 去重前是否将数据转为MD5,默认是 redis_url: redis://[[username]:[password]]@localhost:6379/0 BloomFilter 与 ExpireFilter 使用 默认会读取setting中的redis配置,若无setting,则需要专递redis_url initial_capacity: 单个布隆过滤器去重容量 默认100000000,当布隆过滤器容量满时会扩展下一个布隆过滤器 error_rate:布隆过滤器的误判率 默认0.00001 **kwargs: """ if filter_type == Dedup.ExpireFilter: try: expire_time = kwargs["expire_time"] except: raise ValueError("需传参数 expire_time") name = kwargs.get("absolute_name") or "dedup:expire_set:%s" % kwargs.get( "name", expire_time ) expire_time_record_key = "dedup:expire_set:expire_time" self.dedup = ExpireFilter( name=name, expire_time=expire_time, expire_time_record_key=expire_time_record_key, redis_url=kwargs.get("redis_url"), ) else: initial_capacity = kwargs.get("initial_capacity", 100000000) error_rate = kwargs.get("error_rate", 0.00001) name = kwargs.get("absolute_name") or "dedup:bloomfilter:" + kwargs.get( "name", "bloomfilter" ) if filter_type == Dedup.BloomFilter: self.dedup = ScalableBloomFilter( name=name, initial_capacity=initial_capacity, error_rate=error_rate, bitarray_type=ScalableBloomFilter.BASE_REDIS, redis_url=kwargs.get("redis_url"), ) elif filter_type == Dedup.MemoryFilter: self.dedup = ScalableBloomFilter( name=name, initial_capacity=initial_capacity, error_rate=error_rate, bitarray_type=ScalableBloomFilter.BASE_MEMORY, ) else: raise ValueError( "filter_type 类型错误,仅支持 Dedup.BloomFilter、Dedup.MemoryFilter、Dedup.ExpireFilter" ) self._to_md5 = to_md5 def __repr__(self): return str(self.dedup) def _deal_datas(self, datas): if self._to_md5: if isinstance(datas, list): keys = [get_md5(data) for data in datas] else: keys = get_md5(datas) else: keys = copy.deepcopy(datas) return keys def add( self, datas: Union[List[Any], Any], skip_check: bool = False ) -> Union[List[Any], Any]: """ 添加数据 @param datas: list / 单个值 @param skip_check: 是否直接添加,不检查是否存在 适用于bloomfilter,加快add速度 @return: list / 单个值 (如果数据已存在 返回 0 否则返回 1, 可以理解为是否添加成功) """ keys = self._deal_datas(datas) is_added = self.dedup.add(keys, skip_check) return is_added def get(self, datas: Union[List[Any], Any]) -> Union[List[Any], Any]: """ 检查数据是否存在 @param datas: list / 单个值 @return: list / 单个值 (存在返回1 不存在返回0) """ keys = self._deal_datas(datas) is_exists = self.dedup.get(keys) return is_exists def filter_exist_data( self, datas: List[Any], *, datas_fingerprints: Optional[List] = None, callback: Callable[[Any], None] = None ) -> Union[Tuple[List[Any], List[Any]], List[Any]]: """ 过滤掉已存在的数据 *** 直接修改原来的数据 使用完此方法后 datas, datas_fingerprints 里面的值为去重后的数据 @param datas_fingerprints: 数据的唯一指纹 列表 @param datas: 数据 列表 @param callback: 数据已存在时的回调 callback(data) @return: None """ is_exists = self.get(datas_fingerprints or datas) dedup_datas = [] if datas_fingerprints: dedup_datas_fingerprints = [] while is_exists: data = datas.pop(0) is_exist = is_exists.pop(0) data_fingerprint = datas_fingerprints.pop(0) if not is_exist: dedup_datas.append(data) dedup_datas_fingerprints.append(data_fingerprint) else: if callback: callback(data) datas_fingerprints.extend(dedup_datas_fingerprints) datas.extend(dedup_datas) return datas, datas_fingerprints else: while is_exists: data = datas.pop(0) is_exist = is_exists.pop(0) if not is_exist: dedup_datas.append(data) else: if callback: callback(data) datas.extend(dedup_datas) return datas