python爬虫中的去重处理

python爬虫中的去重处理

爬虫进阶课程笔记。

1、去重应用场景以及基本原理

1.1、 爬虫中什么业务需要使用去重

  • 防止发出重复的请求
  • 防止存储重复的数据

1.2、去重实现的基本原理

根据给定的判断依据和给定的去重容器,将原始数据逐一进行判断,判断去重容器中是否有该数据。如果没有那就把该数据对应的判断依据添加去重容器中,同时标记该数据是不重复数据;如果有就不添加,同时标记该数据是重复数据。

  • 判断依据(原始数据、原始数据特征值)
  • 去重容器(存储判断数据) set()

2、基于信息摘要算法的去重

  • 普通内存版本
  • Redis持久化版本
  • MySQL持久化版本

信息摘要hash算法指可以将任意长度的文本、字节数据,通过一个算法得到一个固定长度的文本。 如MD5(128位)、SHA1(160位)等。

特征:只要源文本不同,计算得到的结果,必然不同(摘要)。

摘要:摘要算法主要用于比对信息源是否一致,因为只要源发生变化,得到的摘要必然不同;而且通常结果要比源短很多,所以称为“摘要”。

正因此,利用信息摘要算法能大大降低去重容器的存储空间使用率,并提高判断速度,且由于其强唯一性的特征,几乎不存在误判。

init基类

# 基于信息摘要算法进行数据的去重判断和存储

# 1. 基于内存的存储
# 2. 基于redis的存储
# 3. 基于mysql的存储
import six

import hashlib


class BaseFilter(object):
    """基于信息摘要算法进行数据的去重判断和存储"""

    def __init__(self,
                 hash_func_name="md5",
                 redis_host="localhost",
                 redis_port=6379,
                 redis_db=0,
                 redis_key="filter",
                 mysql_url=None,
                 mysql_table_name="filter"):
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_db = redis_db
        self.redis_key = redis_key
        self.mysql_url = mysql_url
        self.mysql_table_name = mysql_table_name

        self.hash_func = getattr(hashlib, hash_func_name)
        self.storage = self._get_storage()

    @staticmethod
    def _safe_data(data):
        """
        python2   str  === python3   bytes
        python2   uniocde === python3  str
        :param data: 给定的原始数据
        :return: 二进制类型的字符串数据
        """
        if six.PY3:
            if isinstance(data, bytes):
                return data
            elif isinstance(data, str):
                return data.encode()
            else:
                raise Exception("请提供一个字符串")  # 建议使用英文来描述
        else:
            if isinstance(data, str):
                return data
            elif isinstance(data, unicode):
                return data.encode()
            else:
                raise Exception("请提供一个字符串")  # 建议使用英文来描述

    def _get_hash_value(self, data):
        """
        根据给定的数据,返回的对应信息摘要hash值
        :param data: 给定的原始数据(二进制类型的字符串数据)
        :return: hash值
        """
        hash_obj = self.hash_func()
        hash_obj.update(self._safe_data(data))  # python3   bytes    python2 str
        hash_value = hash_obj.hexdigest()
        return hash_value

    def save(self, data):
        """
        根据data计算出对应的指纹进行存储
        :param data: 给定的原始数据
        :return: 存储的结果
        """
        hash_value = self._get_hash_value(data)
        return self._save(hash_value)

    def _save(self, hash_value):
        """
        存储对应的hash值(交给对应的子类去继承)
        :param hash_value: 通过信息摘要算法求出的hash值
        :return: 存储的结果
        """
        pass

    def is_exists(self, data):
        """
        判断给定的数据对应的指纹是否存在
        :param data: 给定的原始数据
        :return: True or False
        """
        hash_value = self._get_hash_value(data)
        return self._is_exists(hash_value)

    def _is_exists(self, hash_value):
        """
        判断对应的hash值是否已经存在(交给对应的子类去继承)
        :param hash_value: 通过信息摘要算法求出的hash值
        :return: 判断的结果(True or False)
        """
        pass

    def _get_storage(self):
        """
        返回对应的一个存储对象(交给对应的子类去继承)
        :return:
        """
        pass


from .mysql_filter import MySQLFilter
from .memory_filter import MemoryFilter
from .redis_filter import RedisFilter

2.1、基于python中的set进行去重判断依据的存储

# 基于python中的 集合数据结构进行去重判断依据的存储
from . import BaseFilter


class MemoryFilter(BaseFilter):
    """基于python中的 集合数据结构进行去重判断依据的存储"""

    def _get_storage(self):
        return set()

    def _save(self, hash_value):
        """
        利用set进行存储
        :param hash_value:
        :return:
        """
        return self.storage.add(hash_value)

    def _is_exists(self, hash_value):
        if hash_value in self.storage:
            return True
        return False

2.2、基于mysql的去重判断依据的存储

# 基于mysql的去重判断依据的存储

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

from . import BaseFilter

Base = declarative_base()


# class Filter(Base):
#
#     __tablename__ = "filter"
#
#     id = Column(Integer, primary_key=True)
#     hash_value = Column(String(40), index=True, unique=True)


class MySQLFilter(BaseFilter):
    """基于mysql的去重判断依据的存储"""

    def __init__(self, *args, **kwargs):
        self.table = type(
            kwargs["mysql_table_name"],
            (Base,),
            dict(
                __tablename__=kwargs["mysql_table_name"],
                id=Column(Integer, primary_key=True),
                hash_value=Column(String(40), index=True, unique=True)
            )
        )

        BaseFilter.__init__(self, *args, **kwargs)

    def _get_storage(self):
        """返回一个mysql连接对象(sqlalchemy的数据库连接对象)"""
        engine = create_engine(self.mysql_url)
        Base.metadata.create_all(engine)  # 创建表、如果有就忽略
        session = sessionmaker(engine)
        return session

    def _save(self, hash_value):
        """
        利用mysql进行存储
        :param hash_value:
        :return:
        """
        session = self.storage()
        filter = self.table(hash_value=hash_value)
        session.add(filter)
        session.commit()
        session.close()

    def _is_exists(self, hash_value):
        """查询mysql表中是否存在对应的判断依据"""
        session = self.storage()
        ret = session.query(self.table).filter_by(hash_value=hash_value).first()
        session.close()
        if ret is None:
            return False
        return True

2.3、基于redis的持久化存储的去重判断依据的实现

# 基于redis的持久化存储的去重判断依据的实现
import redis

from . import BaseFilter


class RedisFilter(BaseFilter):
    """基于redis的持久化存储的去重判断依据的实现"""

    def _get_storage(self):
        """返回一个redis连接对象"""
        pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, db=self.redis_db)
        client = redis.StrictRedis(connection_pool=pool)
        return client

    def _save(self, hash_value):
        """
        利用redis的无序集合进行存储
        :param hash_value:
        :return:
        """
        return self.storage.sadd(self.redis_key, hash_value)

    def _is_exists(self, hash_value):
        """判断redis对应的无序集合中是否有对应的判断依据"""
        return self.storage.sismember(self.redis_key, hash_value)

2.4、测试

# -*- coding: utf-8 -*-
from information_summary_filter import MemoryFilter
from information_summary_filter import RedisFilter
from information_summary_filter import MySQLFilter

set_filter = MemoryFilter()

redis_filter = RedisFilter(redis_host="172.17.0.3")

mysql_url = "mysql+pymysql://root:password@172.17.0.4:3306/data?charset=utf8"
mysql_filter = MySQLFilter(mysql_url=mysql_url, mysql_table_name='filter')


def filter_test(filters):
    for d in data:
        if filters.is_exists(d):
            print("发现重复的数据: ", d)
        else:
            filters.save(d)
            print("保存去重的数据:", d)


if __name__ == '__main__':
    # 原始数据
    data = ["111", "qwe", '222', "333", "111", "qwe", "中文", "qwer"]

    # set去重测试
    filter_test(set_filter)
    print("set_filter", '*'*20)

    # redis去重测试
    filter_test(redis_filter)
    print("redis_filter", '*' * 20)

    # mysql去重测试
    filter_test(mysql_filter)
    print("mysql_filter", '*' * 20)

3、布隆过滤器

redis版布隆过滤器
在这里插入图片描述

# 布隆过滤器 redis版本实现
import hashlib

import redis
import six


# 1. 多个hash函数的实现和求值
# 2. hash表实现和实现对应的映射和判断


class MultipleHash(object):
    """根据提供的原始数据,和预定义的多个salt,生成多个hash函数值"""

    def __init__(self, salts, hash_func_name="md5"):
        self.hash_func = getattr(hashlib, hash_func_name)
        if len(salts) < 3:
            raise Exception("请至少提供3个salt")
        self.salts = salts

    def get_hash_values(self, info):
        """根据提供的原始数据, 返回多个hash函数值"""
        hash_values = []
        for i in self.salts:
            hash_obj = self.hash_func()
            hash_obj.update(self._safe_data(info))
            hash_obj.update(self._safe_data(i))
            ret = hash_obj.hexdigest()
            hash_values.append(int(ret, 16))
        return hash_values

    @staticmethod
    def _safe_data(item):
        """
        python2   str  === python3   bytes
        python2   uniocde === python3  str
        :param item: 给定的原始数据
        :return: 二进制类型的字符串数据
        """
        if six.PY3:
            if isinstance(item, bytes):
                return item
            elif isinstance(item, str):
                return item.encode()
            else:
                raise Exception("请提供一个字符串")  # 建议使用英文来描述
        else:
            if isinstance(item, str):
                return item
            elif isinstance(item, unicode):
                return item.encode()
            else:
                raise Exception("请提供一个字符串")  # 建议使用英文来描述


class BloomFilter(object):
    """基于布隆过滤器的去重判断依据的存储"""

    def __init__(self, salts, redis_host="localhost", redis_port=6379, redis_db=0, redis_key="bloom_filter"):
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_db = redis_db
        self.redis_key = redis_key
        self.client = self._get_redis_client()
        self.multiple_hash = MultipleHash(salts)

    def _get_redis_client(self):
        """返回一个redis连接对象"""
        pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, db=self.redis_db)
        client = redis.StrictRedis(connection_pool=pool)
        return client

    def save(self, value):
        """利用多个加盐hash值,存储到hash表里"""
        hash_values = self.multiple_hash.get_hash_values(value)
        for hash_value in hash_values:
            offset = self._get_offset(hash_value)
            self.client.setbit(self.redis_key, offset, 1)
        return True

    def is_exists(self, value):
        """查询redis的hash表中是否存在对应的判断依据"""
        hash_values = self.multiple_hash.get_hash_values(value)
        for hash_value in hash_values:
            offset = self._get_offset(hash_value)
            v = self.client.getbit(self.redis_key, offset)
            if v == 0:
                return False
        return True

    @staticmethod
    def _get_offset(hash_value):
        """hash表实现。对hash之后的值求余,默认256M"""
        # 2**8 = 256
        # 2**20 = 1024 * 1024
        # (2**8 * 2**20 * 2*3) 代表hash表的长度  如果同一项目中不能更改
        return hash_value % (2 ** 8 * 2 ** 20 * 2 * 3)


if __name__ == '__main__':

    data = ["asdfasdf", "123", "123", "456", "asf", "asf"]

    bm = BloomFilter(salts=["1", "2", "3", "4"], redis_host="172.17.0.3")
    for d in data:
        if not bm.is_exists(d):
            bm.save(d)
            print("映射数据成功: ", d)
        else:
            print("发现重复数据:", d)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值