高效生成一个包含16个字符、字母和数字的字符串。

114

我正在寻找一种非常快速的方式来生成表中主键的字母数字唯一标识符。

像这样的东西是否可行?

def genKey():
    hash = hashlib.md5(RANDOM_NUMBER).digest().encode("base64")
    alnum_hash = re.sub(r'[^a-zA-Z0-9]', "", hash)
    return alnum_hash[:16]

如何生成随机数?如果我基于 microtime,那么我必须考虑到来自不同实例的 genKey() 函数可能同时调用的情况。

还有其他更好的方法吗?


13个回答

192

由于没有任何答案能够为您提供一个由0-9,a-z,A-Z字符组成的随机字符串:这里是一个有效的解决方案,它将为您提供约62^16 = 4.76724 e+28个密钥中的一个:

import random, string
x = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(16))
print(x)

即使不熟记ASCII代码,也可以轻松阅读此内容。

python 3.6.2以来,甚至还有一种更短的版本:

import random, string
x = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
print(x)

1
这个数字实际上是错误的。我已经更新了它。它是通过将可能字符的数量乘以字符串长度来计算的。 - David Schumann
1
感谢提到 random.choices。我之前没有听说过它,使用它比循环选择要快得多。 - Andrew
1
谢谢您。 - R. Karlus
ascii_uppercase or ascii_lowercase - DanielBell99

64
你可以使用这个:

>>> import random
>>> ''.join(random.choice('0123456789ABCDEF') for i in range(16))
'E2C6B2E19E4A7777'

不能保证生成的键是唯一的,因此在原始插入失败的情况下应准备好使用新键重试。另外,您可能希望考虑使用确定性算法从自动增量ID生成字符串,而不是使用随机值,因为这将保证唯一性(但也会给出可预测的键)。


1
随机数并不是真正的随机,而是根据文档所述的伪随机。请改用os.urandom。 - nikola
8
@prometheus. 是否 os.urandom 不是伪随机的? - aaronasterling
1
我在回应Mark Byers对术语“随机值”使用不严谨。os.urandom仍然是伪随机的,但它是具有密码学安全性的伪随机,这使得它比random更适用于广泛的用例。 - nikola
1
@nikola,如果密钥只是伪随机的话并不重要,因为它们用于索引。 - yamm
3
也许很显然,但“确定性”并不意味着唯一,你必须实际检查算法是否具有非常长的重复周期。“get_key = lambda n:n%10”是确定性的,但长时间内不是唯一的。 - Mark
1
这个答案只使用了A-F字符,而问题强烈暗示需要a-z和A-Z。 - Hugh W

44

在2016年12月发布的Python 3.6中,引入了secrets模块。

现在可以通过以下方式生成随机令牌:

import secrets

secrets.token_hex(16)

来自Python文档:

secrets模块用于生成加密强度的随机数,适用于管理诸如密码、账户认证、安全令牌和相关秘密等数据。

特别地,应优先使用secrets而不是random模块中的默认伪随机数生成器,后者设计用于建模和模拟,而非安全或加密。

https://docs.python.org/3/library/secrets.html


3
文档字符串:“返回一个随机的十六进制文本字符串。该字符串有 nbytes 个随机字节,每个字节转换为两个十六进制数字。如果 nbytesNone 或未提供,则使用合理的默认值。”意思是 32 == len(secrets.token_hex(16)) - Edward Corrigall

42

可以查看uuid模块(适用于Python 2.5+)。

一个快速的示例:

import uuid
uid = uuid.uuid4()
print(uid.hex)
df008b2e24f947b1b873c94d8a3f2201

请注意,原帖要求一个16位字母数字字符串,但UUID4字符串长度为32个字符。您不应该截断这个字符串,而是应该使用完整的32个字符。


7
这段话的意思是:这个32个字符长度的Guid截取会不安全。为了让翻译更加通俗易懂,可以这样翻译:Guid(全球唯一标识符)有32个字符长度,如果将其截取可能不太安全。 - Brian
关于截断的问题,是真的。另一方面,我只会存储32个字符(除非你有非常特殊的原因只存储16个字符)。 - ChristopheD
1
@Brian 你好,我想知道为什么 GUID 不安全?你有相关的参考资料吗? - Adiyat Mubarak
1
@AdiyatMubarak:从根本上讲,您不需要引用。Guid被记录为唯一的。Guid的一半没有被记录为唯一的。话虽如此,https://blogs.msdn.microsoft.com/oldnewthing/20080627-00/?p=21823解释了当您截断一个特定的GUID算法时会发生什么。 - Brian
这将返回只包含字母 a - f 的十六进制数。 - kta

11

有一个官方的配方:

import string
import secrets
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for i in range(16))
print(password)

这将创建类似于'STCT3jdDUkppph03'的输出。

我喜欢它,简洁明了。谢谢分享! - Carlo
3
我认为这是目前最好的答案。比token_hex更“正常”;比token_hex更密集。 - dstandish

5

对于随机数,一个好的来源是os.urandom

import os
import hashlib
random_data = os.urandom(128)
hashlib.md5(random_data).hexdigest()[:16]

我忘记了那个非常棒的urandom函数:V,这很好,比将字符集添加到字符串中然后循环要好。内置;) - m3nda
1
这在其他答案中也提到过,您不应该截断MD5哈希。 - bman
@bman:我知道截断非常长的UUID存在严重问题,因为随机性不是线性分布的。对于MD5来说,这应该不是问题。 - max
@rlotun 你不能仅仅切掉哈希并说它是随机的。如果这样做,哈希函数就不能保证是随机的。 - Jurakin

3
import random
''.join(random.sample(map(chr, range(48, 57) + range(65, 90) + range(97, 122)), 16))

输出结果如下:
'CDh0geq3NpKtcXfP'

4
你提供的解决方案会省略数字9以及大小写字母Z和z。此外,sample()函数每个字符只会选取一次,因此它所产生的排列数量较少。以下代码将生成一个由16位随机数字、大写字母和小写字母组成的字符串: ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(6666)) - David Schumann

2
我更倾向于使用os.urandom而非secrets.token_hex,因为它从更丰富的字符集中采样,因此需要更小的长度来实现相同的熵。 os.urandom从随机源中读取数据,被认为是安全的(请参见有关urandom是否安全的相关答案)。您可以从urandom中读取任意数量的数据,并按以下方式生成随机的字母数字组合:
import math
import os
def random_alphanumeric(str_len: int) -> str:
  rand_len = 3 * (math.ceil(str_len / 3) + 1)
  return base64.b64encode(os.urandom(rand_len), altchars=b'aA').decode('ascii')[:str_len]

注意:上述函数不安全。由于您需要“非常快速地生成字母数字”,因此该函数在性能上牺牲了安全性,因为与urandom给出的结果相比,aA(或您选择用其他字符替换+/)的频率将增加。

如果您把随机性放在性能之上,可以尝试以下操作:

def secure_random_alphanumeric(str_len: int) -> str:
  ret = ''
  while len(ret) < str_len:
    rand_len = 3 * (math.ceil((str_len - len(ret)) / 3) + 2)
    ret += base64.b64encode(os.urandom(rand_len)).decode('ascii').replace('+', '').replace('/', '').replace('=', '')
  return ret[:str_len]

注意,串联调用replace比逐个调用更快,如此答案所述。
另外,在上面的代码中,当确定rand_len以减少实现所需长度的迭代次数时,+1被替换为+2。 甚至可以替换为+3或更多以进一步减少迭代的可能性,但这样做会损失串联replace调用的性能。

2
这个值在每次调用时增加1(会循环)。决定存储该值的最佳位置将取决于您如何使用它。您可能会发现这篇文章有趣,因为它不仅讨论了Guid的工作原理,还讨论了如何制作一个更小的Guid。
简短的答案是: 使用其中一些字符作为时间戳,另一些字符作为“唯一标识符”,一个值在每次调用uid生成器时递增1。

0
import math
import secrets


def random_alphanum(length: int) -> str:
    if length == 0:
        return ''
    elif length < 0:
        raise ValueError('negative argument not allowed')
    else:
        text = secrets.token_hex(nbytes=math.ceil(length / 2))
        is_length_even = length % 2 == 0
        return text if is_length_even else text[1:]
  • uuid方法效率低且有限,因为uuid仅返回36个字符,然后被截断。
  • 默认的伪随机数生成器不适用于安全或加密应用,可使用标准模块secrets,该模块专为这些应用而设计。

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