为Django模型生成唯一哈希值

17

我希望为每个模型使用唯一的哈希值而不是id。

我实现了以下函数以便在整个应用程序中轻松使用。

import random,hashlib
from base64 import urlsafe_b64encode

def set_unique_random_value(model_object,field_name='hash_uuid',length=5,use_sha=True,urlencode=False):
    while 1:
        uuid_number = str(random.random())[2:]
        uuid = hashlib.sha256(uuid_number).hexdigest() if use_sha else uuid_number
        uuid = uuid[:length]
        if urlencode:
            uuid = urlsafe_b64encode(uuid)[:-1]
        hash_id_dict = {field_name:uuid}
        try:
            model_object.__class__.objects.get(**hash_id_dict)
        except model_object.__class__.DoesNotExist:
            setattr(model_object,field_name,uuid)
            return
我在寻求反馈,还有其他什么方法可以做到吗?如何改进它?关于它,有什么好的、坏的和丑陋的方面?

请问您需要随机哈希还是跨时间和空间唯一的哈希?我之所以问这个问题,是因为很多用户只需要前者,但却将“唯一”和“随机”这两个术语混用。 - nikola
你可以发布一下你是如何解决的吗?我正在寻找解决方案。 - Thomas Schwärzl
生成唯一ID太疯狂了。如果使用uuid,只需生成一个uuid,不要更改长度或任何内容。将模型设置为使用该字段作为主键。保存时仅生成一个uuid,甚至不必担心冲突。 - dalore
4个回答

34

我不喜欢这一部分:

uuid = uuid[:5]

在最理想的情况下(uuid均匀分布),在1k个元素后,您将以大于0.5的概率遇到碰撞!
这是因为生日问题。简而言之,当元素数量大于可能标签数量的平方根时,已经证明碰撞的概率超过了0.5。
您有0xFFFFF = 10^6个标签(不同的数字),因此在生成1000个值后,您将开始发生冲突。
即使您将长度扩大到-1,仍然存在问题:
str(random.random())[2:]

你将在3 * 10^6后开始发生碰撞(相同的计算结果)。
我认为你最好使用uuid来确保唯一性,这里有一个例子。
>>> import uuid
>>> uuid.uuid1().hex
'7e0e52d0386411df81ce001b631bdd31'

更新 如果您不信任数学,只需运行以下示例即可查看碰撞:

 >>> len(set(hashlib.sha256(str(i)).hexdigest()[:5] for i in range(0,2000)))
 1999 # it should obviously print 2000 if there wasn't any collision

4
生日悖论实际上适用于随机数生成。但是,Python的uuid包并没有特别涉及随机数生成。实际上,在您的示例中,uuid1()远非像加密安全那样随意。只是提出这一点,以防有人可能会将Python的uuid包与随机数生成等同起来。 - nikola

15

不好的写法:

import random

文档中写到:

该模块实现了各种分布的 伪随机 数字生成器。

如果可以,请使用os.urandom

返回一个包含 n 个随机字节的字符串,适用于密码学。

这是我在我的模型中使用它的方式:

import os
from binascii import hexlify

def _createId():
    return hexlify(os.urandom(16))

class Book(models.Model):
    id_book = models.CharField(max_length=32, primary_key=True, default=_createId)

需要注意的一点是,urandom 比伪随机数慢得多,因此如果您不需要它用于加密目的,可能不值得使用。在我的 Mac OSX 上,它慢了 21 倍。考虑以下内容: >>> timeit.Timer('import random; random.random()').timeit(100000) 0.1538231372833252 >>> timeit.Timer('import os; os.urandom(2)').timeit(100000) 3.1858959197998047 - Piotr Czapla
我刚刚检查了一下,uuid甚至更慢 :) - Piotr Czapla
这很危险!如果你有一个键冲突,那么你的新记录将会悄悄地覆盖你现有的记录。 - sherbang
3
不,主键必须是唯一的,所以如果发生冲突,你会得到一个“完整性错误(IntegrityError)” ,这件事不会默默无言。 - nikola
你应该使用uuid包。至少uuid1、uuid2、...、uuid5的实现遵循标准化规范。 - Flavian Hautbois

14

Django 1.8+内置了UUIDField。以下是建议的实现方式,使用标准库的uuid模块,取自文档

import uuid
from django.db import models

class MyUUIDModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # other fields

对于旧版本的Django,您可以使用django-uuidfield软件包。


8

使用数据库引擎的UUID支持,而不是自己编写哈希算法。几乎所有除SQLite之外的数据库都支持它们,因此没有理由不使用它们。


9
希望看到一些关于如何在Django的ORM中实现此操作的示例。 - Noe Nieto
好的,这不是“Django”的问题。在PostgreSQL中你有这个 http://www.postgresql.org/docs/8.3/static/datatype-uuid.html。我认为在其他数据库中,它们有类似的东西。 - Tan Nguyen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接