我知道如何创建随机字符串,例如:
''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(N))
但是,不应该有重复项,所以我目前只是检查列表中是否已经存在密钥,如以下代码所示:
import secrets
import string
import numpy as np
amount_of_keys = 40000
keys = []
for i in range(0,amount_of_keys):
N = np.random.randint(12,20)
n_key = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(N))
if not n_key in keys:
keys.append(n_key)
这对于像 40000
之类的少量键是可以的,但是随着键的增加,问题并不能很好地扩展。所以我想知道是否有更快的方法来获得更多键的结果,比如 999999
原文由 Kev1n91 发布,翻译遵循 CC BY-SA 4.0 许可协议
基本改进、集合和本地名称
使用 集合 而不是列表,唯一性测试要快得多;集合成员资格测试需要独立于集合大小的常数时间,而列表需要 O(N) 线性时间。使用集合理解一次生成一系列键,以避免在循环中查找和调用
set.add()
方法;适当地随机,较大的密钥无论如何产生重复的机会很小。因为这是在一个紧密的循环中完成的,所以值得您尽可能地优化所有名称查找:
_randint
关键字参数将np.random.randint
名称绑定到函数中的局部变量,引用速度比全局变量更快,尤其是在涉及属性查找时。pickchar()
部分避免查找模块或更多本地的属性;它是一个具有所有引用的单个可调用对象,因此执行速度更快,尤其是在循环中完成时。while
循环仅在产生重复项时才继续迭代。如果没有重复项,我们会在单个集合理解中生成足够的键来填充其余部分。第一次改进的时间
对于 100 个项目,差异并不大:
但是当您开始扩大规模时,您会注意到针对列表的 O(N) 成员资格测试成本确实拖累了您的版本:
我的版本速度几乎是 10k 项目的两倍; 40k 项目可以在大约 32 秒内运行 10 次:
列表版本用了 2 多分钟,是原来的十倍多。
Numpy 的 random.choice 函数,密码学强度不高
您可以通过放弃
secrets
模块并使用np.random.choice()
来加快速度;然而,这不会产生加密级别的随机性,但选择随机字符的速度是原来的两倍:这产生了巨大的变化,现在可以在 16 秒内生成 10 倍的 40k 密钥:
进一步调整 itertools 模块和生成器
我们还可以从
itertools
模块 Recipes 部分获取unique_everseen()
函数 让它处理唯一性,然后使用无限生成器和itertools.islice()
函数 将结果限制为我们想要的数字:这仍然稍微快一些,但只是稍微快一点:
os.urandom() 字节和生成字符串的不同方法
接下来,我们可以继续 Adam Barnes 关于使用 UUID4(基本上只是
os.urandom()
的包装器)和 Base64 的想法。但是通过大小写折叠 Base64 并用随机选择的字符替换 2 个字符,他的方法严重限制了这些字符串中的熵(你不会产生可能的唯一值的全部范围,一个 20 个字符的字符串只使用(256 ** 15) / (36 ** 20)
== 每 99437 位熵中有 1 个!)。The Base64 encoding uses both upper and lower case characters and digits but also adds the
-
and/
characters (or+
and_
for URL 安全变体)。对于仅大写字母和数字,您必须大写输出并将这两个额外的字符映射到其他随机字符,这个过程会从os.urandom()
提供的随机数据中丢弃大量熵。除了使用 Base64,您还可以使用 Base32 编码,它使用大写字母和数字 2 到 8,因此生成的字符串有 32 ** n 种可能性,而不是 36 ** n 种可能性。但是,这可以进一步加快上述尝试的速度:这 真的 很快:
40k 键,10 次,仅需 4 秒多一点。所以速度大约是原来的 75 倍;使用
os.urandom()
作为来源的速度是不可否认的。这又是 _密码学上强大的_;
os.urandom()
产生用于加密用途的字节。另一方面,我们将生成的可能字符串的数量减少了 90% 以上(((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100
是 90.5),我们不再使用0
,1
,8
和9
输出中的数字。所以也许我们应该使用
urandom()
技巧来生成正确的 Base36 编码;我们必须生成我们自己的b36encode()
函数:并使用它:
这是相当快的,最重要的是产生了 36 个大写字母和数字的全部范围:
诚然,base32 版本的速度几乎是这个版本的两倍(多亏了使用表的高效 Python 实现),但使用自定义 Base36 编码器的速度仍然是非加密安全版本
numpy.random.choice()
版本的两倍。但是,使用
os.urandom()
再次 _产生偏差_;我们必须产生比 12 到 19 个 base36“数字”所需的更多的熵。例如,对于 17 位数字,我们不能使用字节生成 36 ** 17 个不同的值,只能生成最接近的 256 ** 11 个字节,这大约高了 1.08 倍,因此我们最终会产生偏差朝向A
,B
,并且在较小程度上,C
(感谢 Stefan Pochmann 指出这个 outchmann)。在下面选择一个整数
(36 ** length)
并将整数映射到base36因此,我们需要寻求一种安全的随机方法,该方法可以为我们提供在
0
(含)和36 ** (desired length)
(不含)之间均匀分布的值。然后我们可以将数字直接映射到所需的字符串。首先,将整数映射为字符串;对以下内容进行了调整以最快地生成输出字符串:
接下来,我们需要一种快速且 加密安全 的方法来选择一个范围内的号码。您仍然可以为此使用
os.urandom()
,但是您必须将字节屏蔽到最大位数,然后循环直到您的实际值低于限制。这实际上已经由secrets.randbelow()
函数 实现。在 Python 版本 < 3.6 中,您可以使用random.SystemRandom().randrange()
,它使用完全相同的方法和一些额外的包装来支持大于 0 的下限和步长。使用
secrets.randbelow()
函数变为:然后这非常接近(可能有偏见的)base64 解决方案:
这几乎与 Base32 方法一样快,但会生成全范围的密钥!