Python爬虫中的去重处理

Python爬虫中的去重处理

  • 一:Python爬虫去重应用场景及基本原理
  • 二:基于信息摘要算法的去重
    • 2.1 信息摘要hash算法去重方案实现
    • 2.2 去重过滤器基类实现
    • 2.3 基于set的去重过滤器
    • 2.4 基于redis无序集合的去重过滤器
    • 2.5 基于mysql的去重过滤器
  • 三:基于simhash算法的去重
  • 四:布隆过滤器原理与实现

一:Python爬虫去重应用场景及基本原理

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

  1. 防止发出重复的请求
  2. 防止存储重复的数据

去重实现的基本原理
根据给定的判断依据和给定的去重容器,将原始数据逐一进行判断,判断去重容器中是否有该数据。如果没有那就把该数据对应的判断依据添加去重容器中,同时标记该数据是不重复数据;如果有就不添加,同时标记该数据是重复数据。
判断依据(原始数据,原始数据特征值)
去重容器(存储判断数据)

二:基于信息摘要算法的去重

信息摘要hash算法指可以将任意长度的文件,字节数据,通过一个算法得到一个固定长度的文件。如MD5(128位),SHA1(160位)等。hash算法得出的结果其实质上就是一串数值,如md5的128位指的是二进制长度,十六进制的长度是32位。一个十六进制等于四个二进制。
特征:只要源文本不同,计算得到的结果必然不同(摘要)
摘要:摘要算法主要用于比对信息源是否一致,因为只要源发生变化,得到的摘要必然不同;而且通常结果要比源短很多,所以称为“摘要”。
因此,利用信息摘要算法能大大降低去重容器的存储空间使用率,并提高判断速度,且由于其强唯一的特征,几乎不存在误判。

from hashlib import md5
m5 = md5()
s = 'life is short, you need python'
m5.update(s.encode())		# 必须传入二进制数据
print(m5.hexdigest())
# 0c653a0b179966f1f99b3a1f38a3f02e

2.1 信息摘要hash算法去重方案实现

实现以下三种方案:

  1. 普通内存版本
  2. Redis持久化版本
  3. MySQL持久化版本

python docker环境的部署与配置见:利用docker搭建Python开发环境
python docker环境的Dockerfile文件配置如下:

# 配置基本的python spider开发环境
FROM python:alpine3.7
# 修改alpine系统源及pip安装源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN mkdir -p /root/.pip
RUN echo -e "[global]\nindex-url = http://mirrors.aliyun.com/pypi/simple/\n[install]\ntrusted-host = mirrors.aliyun.com" > /root/.pip/pip.conf
# 安装开发环境所需要的包
RUN apk add --no-cache build-base &&\
    apk add --no-cache libffi-dev &&\
    pip install ipython &&\
    pip install redis &&\
    pip install pymysql &&\
    pip install sqlalchemy &&\
    pip install six &&\
    pip install w3lib &&\
    apk del build-base

2.2 去重过滤器基类实现

Python爬虫中的去重处理_第1张图片

# __init__.py
# 基于信息摘要算法进行数据的去重判断和存储
import hashlib
class BaseFilter(object):
    '''基于信息摘要算法进行数据的去重判断和存储'''
    def __init__(self, hash_func_nme = 'md5', redis_host = "localhost", redis_port = 6379, redis_password = '', 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_password = redis_password
        self.redis_key = redis_key
        self.mysql_url = mysql_url
        self.mysql_table_naem = mysql_table_name
        self.hash_func = getattr(hashlib, hash_func_nme)
        self.storage = self._get_storage()
    def _get_hash_value(self, data):
        '''
        根据给定的数据,返回对应的信息摘要hash值
        :param data: 给定的原始数据(二进制类型的字符串数据)
        :return: hash值
        '''
        hash_obj = self.hash_func()
        hash_obj.update(self._safe_data(data))
        hash_value = hash_obj.hexdigest()
        return hash_value
    def _safe_data(self, data):
        '''
        :param data: 给定的原始数据
        :return: 二进制类型的字符串数据
        '''
        if isinstance(data, bytes):
            return data
        elif isinstance(data, str):
            return data.encode()
        else:
            raise Exception('provide string')
    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 Flase
        '''
        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

2.3 基于set的去重过滤器

# memory_filter.py
from . import BaseFilter
class MemoryFilter(BaseFilter):
    '''基于python中的集合数据结构进行去重判断依据的存储'''
    def _save(self, hash_value):
        '''利用set进行存储'''
        return self.storage.add(hash_value)
    def _is_exists(self, hash_value):
        if hash_value in self.storage:
            return True
        return False
    def _get_storage(self):
        return set()

测试去重功能:

# demo.py
from info_summary_filter.memory_filter import MemoryFilter
filter = MemoryFilter()
data = ["111", "qwe", "222", "333", "111", "qwe", "中文"]
for d in data:
    if filter.is_exists(d):
        print('发现重复的数据:', d)
    else:
        filter.save(d)
        print('保存去重后的数据:', d)

输出结果为:

b4cb376a1337:python -u /opt/project/demo.py
保存去重后的数据: 111
保存去重后的数据: qwe
保存去重后的数据: 222
保存去重后的数据: 333
发现重复的数据: 111
发现重复的数据: qwe
保存去重后的数据: 中文

2.4 基于redis无序集合的去重过滤器

# redis_filter.py
# 基于redis的持久化存储的去重判断依据的实现
from . import BaseFilter
import redis
class RedisFilter(BaseFilter):
    def _save(self, hash_value):
        '''利用redis的无序集合进行存储'''
        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)  # return True or False
    def _get_storage(self):
        '''返回一个redis连接对象'''
        pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, password=self.redis_password, db=self.redis_db)  # 连接池
        client = redis.StrictRedis(connection_pool=pool)
        return client

测试去重功能:

filter = RedisFilter(redis_host = '172.18.0.3')		# IP为远程主机上的redis容器IP
data = ["111", "qwe", "222", "333", "111", "qwe", "中文"]
for d in data:
    if filter.is_exists(d):
        print('发现重复的数据:', d)
    else:
        filter.save(d)
        print('保存去重后的数据:', d)

2.5 基于mysql的去重过滤器

# mysql_filter.py
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):
    def __init__(self, *args, **kwargs):
    	# 将上面的Filter类放在这里动态创建
        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 _save(self, hash_value):
        session = self.storage()
        filter = self.table(hash_value=hash_value)
        session.add(filter)
        session.commit()
        session.close()
    def _is_exists(self, hash_value):
        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
    def _get_storage(self):
        '''返回一个mysql连接对象'''
        engine = create_engine(self.mysql_url)
        Base.metadata.create_all(engine)  # 创建数据表,如存在则忽略
        Session = sessionmaker(engine)
        return Session

测试去重功能:

from info_summary_filter.mysql_filter import MySQLFilter
mysql_url = 'mysql+pymysql://root:[email protected]:3306/spider?charset=utf8'
filter = MySQLFilter(mysql_url = mysql_url, mysql_table_name = 'ginvip')
data = ["111", "qwe", "222", "333", "111", "qwe", "中文"]
for d in data:
    if filter.is_exists(d):
        print('发现重复数据:', d)
    else:
        filter.save(d)
        print('保存去重的数据:', d)

三:基于simhash算法的去重

simhash算法是一种局部敏感哈希算法,能实现相似文本内容的去重。比如以下两篇新闻数据
Python爬虫中的去重处理_第2张图片
与信息摘要算法的区别:

  1. 信息摘要算法:如果原始内容只相关一个字节,所产生的签名也很可能差别很大。
  2. Simhash算法:如果原始内容只相关一个字节,所产生的签名差别非常小。
    Simhash值比对:通过两者的Simhash值的二进制位的差异来表示原始文本内容的差异。差异个数又被称为海明距离。
    Python爬虫中的去重处理_第3张图片

Simhash对长文本500字+比较适用,短文本可能偏差较大。在google的论文给出的数据中,64位simhash值,在海明距离为3情况下,可以认为两篇文档是相似的或者是重复的。当然这个值只是参考值,针对自己的应用可能有不同的测试取值。

Simhash算法的实现见:

https://leons.im/posts/a-python-implementation-of-simhash-algorithm/
https://github.com/leonsim/simhash

四:布隆过滤器原理与实现

Python爬虫中的去重处理_第4张图片

import hashlib
import redis
# 布隆过滤器redis版本实现
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, data):
        hash_values = []
        for i in self.salts:
            hash_obj = self.hash_func()
            hash_obj.update(self._safe_data(data))
            hash_obj.update(self._safe_data(i))
            ret = hash_obj.hexdigest()
            hash_values.append(int(ret, 16))
        return hash_values
    def _safe_data(self, data):
        '''
        :param data: 给定的原始数据
        :return: 二进制类型的字符串数据
        '''
        if isinstance(data, bytes):
            return data
        elif isinstance(data, str):
            return data.encode()
        else:
            raise Exception('provide string')
class BloomFilter(object):
    def __init__(self, salts, redis_host = 'localhost', redis_port = 6379, redis_password = '', redis_db = 0, redis_key = 'bloom_filter'):
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_password = redis_password
        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):
        pool = redis.ConnectionPool(host=self.redis_host, port=self.redis_port, password=self.redis_password, db=self.redis_db)  # 连接池
        client = redis.StrictRedis(connection_pool=pool)
        return client
    def save(self, data):
        hash_values = self.multiple_hash.get_hash_values(data)
        for hash_value in hash_values:
            offset = self._get_offset(hash_value)
            self.client.setbit(self.redis_key, offset, 1)
        return True
    def _get_offset(self, hash_value):
        # 2**8 = 256
        # 2**20 = 1024*1024
        return hash_value % (2**8 * 2**20 * 2*3)
    def is_exists(self, data):
        hash_values = self.multiple_hash.get_hash_values(data)
        bit_values = []
        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
if __name__ == '__main__':
    data = ['Bruce Lee', 'Jet Lee', 'Natasha', 'Jacky Chen', 'Bruce Lee']
    bm = BloomFilter(salts = ['1', '2', '3', '4'], redis_host = '172.18.0.3', redis_password='BruceLee')
    for d in data:
        if not bm.is_exists(d):
            bm.save(d)
            print('映射数据成功:', d)
        else:
            print('发现重复数据:', d)

你可能感兴趣的:(Python爬虫)