我知道如何创建随机字符串,比如:

''.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

推荐答案

基本改进、集合和本地名称

使用set,而不是列表,测试唯一性要快得多;集合成员测试需要与集合大小无关的恒定时间,而列表需要O(N)线性时间.使用集合理解一次生成一系列键,以避免在循环中查找和调用set.add()方法;如果是随机的,较大的键无论如何产生副本的可能性很小.

因为这是在一个紧密的循环中完成的,所以在尽可能优化所有名称查找的同时,这是值得的:

import secrets
import numpy as np
from functools import partial

def produce_amount_keys(amount_of_keys, _randint=np.random.randint):
    keys = set()
    pickchar = partial(secrets.choice, string.ascii_uppercase + string.digits)
    while len(keys) < amount_of_keys:
        keys |= {''.join([pickchar() for _ in range(_randint(12, 20))]) for _ in range(amount_of_keys - len(keys))}
    return keys

_randint关键字参数将np.random.randint名称绑定到函数中的一个局部名称,这比全局名称引用更快,尤其是在涉及属性查找时.

pickchar()部分避免查找模块或更多局部变量的属性;它是一个具有所有引用的可调用函数,因此执行速度更快,尤其是在循环中执行时.

只有在产生了重复项时,while循环才会继续迭代.如果没有重复的话,我们在一个集合中产生足够的键来填充剩余的键.

第一次改进的时间安排

对于100个项目,差异并没有那么大:

>>> timeit('p(100)', 'from __main__ import produce_amount_keys_list as p', number=1000)
8.720592894009314
>>> timeit('p(100)', 'from __main__ import produce_amount_keys_set as p', number=1000)
7.680242831003852

但是,当你开始扩大这个范围时,你会注意到O(N)成员资格测试成本与列表的对比确实会拖累你的版本:

>>> timeit('p(10000)', 'from __main__ import produce_amount_keys_list as p', number=10)
15.46253142200294
>>> timeit('p(10000)', 'from __main__ import produce_amount_keys_set as p', number=10)
8.047800761007238

我的版本速度几乎是10k物品的两倍;40k个项目可以在大约32秒内运行10次:

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_list as p', number=10)
138.84072386901244
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_set as p', number=10)
32.40720253501786

列表版本耗时超过2分钟,是原来的10倍多.

Numpy是随机的. Select 功能,加密功能不强

你可以通过放弃secrets模块而改用np.random.choice()来加快速度;不过,这不会产生加密级别的随机性,但拾取随机字符的速度是随机字符的两倍:

def produce_amount_keys(amount_of_keys, _randint=np.random.randint):
    keys = set()
    pickchar = partial(
        np.random.choice,
        np.array(list(string.ascii_uppercase + string.digits)))
    while len(keys) < amount_of_keys:
        keys |= {''.join([pickchar() for _ in range(_randint(12, 20))]) for _ in range(amount_of_keys - len(keys))}
    return keys

这带来了巨大的不同,现在仅需16秒就可以生成10乘以40k的密钥:

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_npchoice as p', number=10)
15.632006907981122

进一步调整了itertools模块和发电机

我们还可以从itertools模块Recipes部分中选取unique_everseen() function,让其考虑唯一性,然后使用无限生成器和itertools.islice() function将结果限制为我们想要的数字:

# additional imports
from itertools import islice, repeat

# assumption: unique_everseen defined or imported

def produce_amount_keys(amount_of_keys):
    pickchar = partial(
        np.random.choice,
        np.array(list(string.ascii_uppercase + string.digits)))
    def gen_keys(_range=range, _randint=np.random.randint):
        while True:
            yield ''.join([pickchar() for _ in _range(_randint(12, 20))])
    return list(islice(unique_everseen(gen_keys()), amount_of_keys))

这一速度稍快,但仅略快:

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_itertools as p', number=10)
14.698191125993617

操作系统.Uradom()字节和另一种生成字符串的方法

接下来,我们可以继续使用UUID4(基本上只是os.urandom()的包装器)和Base64.但是,通过大小写折叠Base64并用随机选取的字符替换2个字符,他的方法严重限制了这些字符串中的熵(你不会产生可能的全部唯一值,一个20个字符的字符串每99437位熵中只使用(256 ** 15) / (36 ** 20)==1!).

Base64编码既使用大小写字符和数字,也使用-/字符(或+_表示URL安全变体).对于仅大写字母和数字,您必须将输出大写,并将额外的两个字符映射到其他随机字符,这一过程会从os.urandom()提供的随机数据中丢弃大量熵.除了使用Base64,您还可以使用Base32编码,它使用大写字母和数字2到8,因此生成的字符串可能为32**n,而不是36**n.然而,这可以进一步加快上述try 的速度:

import os
import base64
import math

def produce_amount_keys(amount_of_keys):
    def gen_keys(_urandom=os.urandom, _encode=base64.b32encode, _randint=np.random.randint):
        # (count / math.log(256, 32)), rounded up, gives us the number of bytes
        # needed to produce *at least* count encoded characters
        factor = math.log(256, 32)
        input_length = [None] * 12 + [math.ceil(l / factor) for l in range(12, 20)]
        while True:
            count = _randint(12, 20)
            yield _encode(_urandom(input_length[count]))[:count].decode('ascii')
    return list(islice(unique_everseen(gen_keys()), amount_of_keys))

这是really快:

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b32 as p', number=10)
4.572628145979252

40k键,10次,仅用4秒多.所以速度大约是75倍;使用os.urandom()作为源的速度是不可否认的.

这是,cryptographically strong againos.urandom()生成用于加密的字节.另一方面,我们将可能产生的字符串数量减少了90%以上(((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100是90.5),我们不再使用输出中的0189位数.

所以也许我们应该使用urandom()技巧来产生一个正确的Base36编码;我们必须生产自己的b36encode()功能:

import string
import math

def b36encode(b, 
        _range=range, _ceil=math.ceil, _log=math.log, _fb=int.from_bytes, _len=len, _b=bytes,
        _c=(string.ascii_uppercase + string.digits).encode()):
    """Encode a bytes value to Base36 (uppercase ASCII and digits)

    This isn't too friendly on memory because we convert the whole bytes
    object to an int, but for smaller inputs this should be fine.
    """
    b_int = _fb(b, 'big')
    length = _len(b) and _ceil(_log((256 ** _len(b)) - 1, 36))
    return _b(_c[(b_int // 36 ** i) % 36] for i in _range(length - 1, -1, -1))

用这个:

def produce_amount_keys(amount_of_keys):
    def gen_keys(_urandom=os.urandom, _encode=b36encode, _randint=np.random.randint):
        # (count / math.log(256, 36)), rounded up, gives us the number of bytes
        # needed to produce *at least* count encoded characters
        factor = math.log(256, 36)
        input_length = [None] * 12 + [math.ceil(l / factor) for l in range(12, 20)]
        while True:
            count = _randint(12, 20)
            yield _encode(_urandom(input_length[count]))[-count:].decode('ascii')
    return list(islice(unique_everseen(gen_keys()), amount_of_keys))

这相当快,而且最重要的是产生了36个大写字母和数字的完整范围:

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b36 as p', number=10)
8.099918447987875

诚然,base32版本的速度几乎是这个版本的两倍(这要归功于使用表的高效Python实现),但使用自定义Base36编码器的速度仍然是非加密安全的numpy.random.choice()版本的两倍.

然而,再次使用os.urandom() produces bias;我们必须产生比12到19个36位"数字"所需的更多的熵位.例如,对于17位数字,我们无法使用字节生成36**17个不同的值,只能生成最接近的256**11字节的等效值,这大约是高出的1.08倍,因此我们最终会偏向AB,并在较小程度上偏向C(感谢Stefan Pochmann指出这一点).

Picking an integer below (36 ** length) and mapping integers to base36

因此,我们需要找到一种安全的随机方法,它可以为我们提供均匀分布在0(含)和36 ** (desired length)(不含)之间的值.然后我们可以将数字直接映射到所需的字符串.

首先,将整数映射为字符串;为了以最快的速度生成输出字符串,对以下内容进行了调整:

def b36number(n, length, _range=range, _c=string.ascii_uppercase + string.digits):
    """Convert an integer to Base36 (uppercase ASCII and digits)"""
    chars = [_c[0]] * length
    while n:
        length -= 1
        chars[length] = _c[n % 36]
        n //= 36
    return ''.join(chars)

接下来,我们需要一种快速、准确的方法来 Select 一个范围内的数字.你仍然可以使用os.urandom(),但是你必须将字节屏蔽到最大位数,然后循环直到你的实际值低于限制.这实际上已经由secrets.randbelow() function人实施.在Python版本中<;3.6您可以使用random.SystemRandom().randrange(),它使用完全相同的方法和一些额外的包装来支持大于0的下限和步长.

使用secrets.randbelow(),功能变为:

import secrets

def produce_amount_keys(amount_of_keys):
    def gen_keys(_below=secrets.randbelow, _encode=b36number, _randint=np.random.randint):
        limit = [None] * 12 + [36 ** l for l in range(12, 20)]
        while True:
            count = _randint(12, 20)
            yield _encode(_below(limit[count]), count)
    return list(islice(unique_everseen(gen_keys()), amount_of_keys))

这与base64解决方案非常接近(可能有偏差):

>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_below as p', number=10)
5.135716405988205

这几乎与Base32方法一样快,但会产生全范围的关键点!

Python-3.x相关问答推荐

在numpy. linalg的qr之后使用scipy. integrate中的solve_ivp时出现了一个奇怪的错误

如何验证具有内部json字符串的json字符串?

无法使用诗词安装PyYaml

Numba编译时间呈指数级增长--可以像C编译器一样配置优化级别吗?

如何将日期时间索引写入日期类型的表?

Pandas 窗口聚合两个排序表

删除括号和大括号中不必要的空格

Pandas数据单调行为

基本 Flask 应用程序未运行(TypeError:模块中缺少必填字段type_ignores)

在带有 M1 芯片(基于 ARM 的 Apple Silicon)的 Mac 上安装较早版本的 Python(3.8 之前)失败

如何在元素列表中找到最大的数字,可能是非唯一的?

Python中的多行日志(log)记录

Python:在 map 对象上调用列表两次

如何遍历某些扩展名的文件?

如何在继承的数据类中创建可选字段?

对字节进行按位运算

具有不均匀间隙的 Python 范围

Python pathlib 获取父级相对路径

尾部斜杠的 FastAPI 重定向返回非 ssl 链接

在 Meta 中创建具有动态模型的通用序列化程序