【问题标题】:python data type to track duplicates用于跟踪重复项的 python 数据类型
【发布时间】:2011-05-24 09:20:33
【问题描述】:

我经常用这样的方式跟踪重复:

processed = set() 
for big_string in strings_generator:
    if big_string not in processed:
        processed.add(big_string)
        process(big_string)

我正在处理大量数据,因此不想将处理后的集合保存在内存中。我有一个使用 sqlite 将数据存储在磁盘上的版本,但是这个过程运行速度要慢得多。

要减少内存使用你认为使用这样的哈希是什么:

processed = set() 
for big_string in string_generator:
    key = hash(big_string)
    if key not in ignored:
        processed.add(key)
        process(big_string)    

缺点是我可能会因偶尔的哈希冲突而丢失数据。 10 亿次哈希中的 1 次碰撞对我来说不是问题。

我尝试了 md5 哈希,但发现生成哈希成为一个瓶颈。

你会建议什么?

【问题讨论】:

  • 嗯,你知道你的选择。要么让对象保持活动状态,这样它就可以告诉set,如果当前项目确实是它本身,或者只是产生相同的散列,或者你将这些对象截断为散列并获得不可恢复的散列冲突。如果不了解对象的内部结构,就无法避免这种情况。
  • 如果你使用 md5,碰撞可以忽略不计(比宇宙射线造成的内存故障小)。
  • +1 对 Paulo:使用任何现代哈希,在您可能遇到偶然的哈希冲突之前,您的集合的内存就会用完。
  • 我发现计算 md5 哈希会增加太多开销 - 您会推荐什么替代方案?

标签: python memory hash types


【解决方案1】:

我假设您正在对网页进行哈希处理。您最多必须散列55 billion web pages(并且该度量几乎肯定会忽略一些重叠)。

你愿意接受不到十亿分之一的碰撞机会,这意味着如果我们查看一个哈希函数,如果哈希是真正随机的[^1],则碰撞次数接近我们得到的碰撞次数,我们想要一个大小为(55*10ˆ9)*10ˆ9 的哈希范围。那是log2((55*10ˆ9)*10ˆ9) = 66 位。

[^1]: 因为哈希值可以被认为是随机选择的, p(collision) = (occupied range)/(total range)

由于存在速度问题,但没有真正的加密问题,我们可以使用 > 66 位非加密哈希 with the nice collision distribution property outlined above

看起来我们正在寻找 Murmur3 哈希的 128 位版本。人们一直在报告speed increases upwards of 12x 在 64 位机器上将 Murmur3_128 与 MD5 进行比较。您可以使用this library 进行速度测试。另请参阅此相关answer,其中:

  • 显示在 python 的 str_hash 范围内的速度测试结果,您已经认为该速度可以接受 elsewhere - 尽管 python 的 hash 是一个 32 位散列,只剩下 2ˆ32/(10ˆ9)(即只有 4 个)以小于十亿分之一的碰撞几率存储的值。
  • 生成了一个library of python bindings,您应该可以直接使用它。

最后,我希望已经概述了可以让您与其他大小不同的函数进行比较的推理,如果您觉得需要它(例如,如果您提高了碰撞容限,如果您的索引集的大小小于整个互联网等等……)。

【讨论】:

    【解决方案2】:

    你必须决定哪个更重要:空间或时间。

    如果是时间,那么您需要创建 large_item 的唯一表示,它占用尽可能少的空间(可能是一些 str 值),易于计算(即快速)并且不会发生冲突,并存储他们在set

    如果有空间,请找到最快的磁盘支持解决方案,并存储可识别large_item 的最小可能唯一值。

    因此,无论哪种方式,您都需要小的唯一标识符 - 取决于 large_item 的性质,这可能是一个巨大的胜利,也可能是不可能的。

    更新

    它们是 html 内容的字符串

    那么也许是一个混合解决方案:在内存中保留一个set 来存储普通的 Python 散列,同时将实际的 html 内容保存在磁盘上,由该散列键控;当您检查当前的large_item 是否在set 中并获得肯定时,请仔细检查磁盘支持的解决方案以查看它是否真正成功,然后酌情跳过或处理。像这样的:

    import dbf
    on_disk = dbf.Table('/tmp/processed_items', 'hash N(17,0); value M')
    index = on_disk.create_index(lambda rec: rec.hash)
    
    fast_check = set()
    def slow_check(hashed, item):
        matches = on_disk.search((hashed,))
        for record in matches:
            if item == record.value:
                return True
        return False
    
    for large_item in many_items:
        hashed = hash(large_item) # only calculate once
        if hashed not in fast_check or not slow_check(hashed, large_item):
            on_disk.append((hashed, large_item))
            fast_check.add(hashed)
            process(large_item)    
    

    仅供参考:dbf 是我编写的一个模块,您可以在 PyPI 上找到它

    【讨论】:

    • 它们是html内容的字符串
    【解决方案3】:

    如果 many_item 已驻留在内存中,则您不会创建 large_item 的另一个副本。您只是在忽略集中存储对它的引用。

    如果 many_items 是文件或其他生成器,您将不得不查看其他替代方案。

    例如,如果 many_items 是一个文件,也许您可​​以在文件中存储指向该项目的指针,而不是实际的项目

    【讨论】:

    • @user470379:除非many_items 是一个生成器,否则它也可以让项目保持活力......
    【解决方案4】:

    正如您已经看到的几个选项,但不幸的是,它们都不能完全解决这种情况,部分原因是

    1. 内存约束,将整个对象存储在内存中
    2. 没有完美的Hash函数,对于巨大的数据集变化的碰撞是有的。
    3. 更好的哈希函数 (md5) 更慢
    4. 使用像 sqlite 这样的数据库实际上会使事情变慢

    当我阅读以下摘录时

    我有一个使用 sqlite 将数据存储在磁盘上的版本,但是这个过程运行得慢得多。

    我觉得如果你在这方面工作,它可能会对你有所帮助。应该是这样的

    1. 使用tmpfs 创建一个虚拟磁盘。 tmpfs 与其他实现相比有几个优点,因为它支持将较少使用的空间交换到交换空间。
    2. 将 sqlite 数据库存储在 ramdisk 上。
    3. 更改 ramdisk 的大小并分析您的代码以检查您的性能。

    我想您已经有一个工作代码可以将您的数据保存在 sqllite 中。您只需要定义一个 tmpfs 并使用路径来存储您的数据库。

    警告:这是一个仅限 linux 的解决方案

    【讨论】:

    • 在 Linux、OSX 和 Windows 上运行
    【解决方案5】:

    【讨论】:

      【解决方案6】:

      你总是可以用processed 标志来装饰large_item。或类似的东西。

      【讨论】:

      • 如果 many_items 可以抛出对一个公共对象的多个引用,那么它就可以工作。但通常情况是多个对象具有相同的值。
      【解决方案7】:

      你可以试试str类型的__hash__函数。

      In [1]: hash('http://stackoverflow.com')
      Out[1]: -5768830964305142685 
      

      这绝对不是加密哈希函数,但有一点机会不会发生太多冲突。它的工作原理如下所述:http://effbot.org/zone/python-hash.htm

      【讨论】:

        【解决方案8】:

        我建议您分析标准 Python 哈希函数并选择最快的:对于您的应用程序来说,它们对于碰撞都是“安全的”。

        以下是hashmd5sha1 的一些基准:

        In [37]: very_long_string = 'x' * 1000000
        In [39]: %timeit hash(very_long_string)
        10000000 loops, best of 3: 86 ns per loop
        
        In [40]: from hashlib import md5, sha1
        
        In [42]: %timeit md5(very_long_string).hexdigest()
        100 loops, best of 3: 2.01 ms per loop
        
        In [43]: %timeit sha1(very_long_string).hexdigest()
        100 loops, best of 3: 2.54 ms per loop
        

        md5sha1 在速度上相当。 hash 比这个字符串快 20k 倍,而且它似乎不太依赖于字符串本身的大小。

        【讨论】:

        • hash() 会足够高效,但预计多久会发生一次冲突?
        • hash 是 32 位或 64 位整数,具体取决于您的架构。考虑到hash 是用来实现dictset 的,它对你来说也应该足够好了;-)
        【解决方案9】:

        您的 sql lite 版本如何工作?如果您将所有字符串插入到数据库表中,然后运行查询“select distinct big_string from table_name”,数据库应该会为您优化它。

        您的另一个选择是使用 hadoop。

        另一种选择是将字符串拆分为多个分区,以便每个分区足够小以适合内存。那么您只需要检查每个分区内的重复项。您用来决定分区的公式将为每个副本选择相同的分区。最简单的方法是只查看字符串的前几位数字,例如:

        d=defaultdict(int)
        for big_string in strings_generator:
            d[big_string[:4]]+=1
        print d
        

        现在您可以决定您的分区,再次检查生成器并将每个 big_string 写入文件名中以 big_string 开头的文件。现在你可以在每个文件上使用你原来的方法,然后遍历所有文件

        【讨论】:

        • 文件系统查找也会很慢
        • 您目前的发电机是什么,速度有多快?例如您现在正在从文件中读取字符串吗?
        【解决方案10】:

        这可以更轻松地通过首先执行更简单的检查,然后通过更精细的检查来调查这些案例来实现。下面的示例包含您的代码的摘录,它正在对更小的数据集执行检查。它通过首先匹配一个检查成本低的简单案例来做到这一点。如果您发现 (filesize, checksum) 对的区分度不够,您可以轻松更改它以获得更便宜但更有效的检查。

        # Need to define the following functions
        def GetFileSize(filename):
            pass
        def GenerateChecksum(filename):
            pass
        def LoadBigString(filename):
            pass
        
        # Returns a list of duplicates pairs.
        def GetDuplicates(filename_list):
            duplicates = list()
            # Stores arrays of filename, mapping a quick hash to a list of filenames.
            filename_lists_by_quick_checks = dict()
            for filename in filename_list:
                quickcheck = GetQuickCheck(filename)
                if not filename_lists_by_quick_checks.has_key(quickcheck):
                    filename_lists_by_quick_checks[quickcheck] = list()
                filename_lists_by_quick_checks[quickcheck].append(filename)
            for quickcheck, filename_list in filename_lists.iteritems():
                big_strings = GetBigStrings(filename_list)
                duplicates.extend(GetBigstringDuplicates(big_strings))
            return duplicates
        
        def GetBigstringDuplicates(strings_generator):
            processed = set()
            for big_string in strings_generator:
                if big_sring not in processed:
                    processed.add(big_string)
                    process(big_string)
        
        # Returns a tuple containing (filesize, checksum).
        def GetQuickCheck(filename):
            return (GetFileSize(filename), GenerateChecksum(filename))
        
        # Returns a list of big_strings from a list of filenames.
        def GetBigStrings(file_list):
            big_strings = list()
            for filename in file_list:
                big_strings.append(LoadBigString(filename))
            return big_strings
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-01-21
          • 1970-01-01
          • 2017-05-04
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多