在 Python 3 中生成具有随机长度的类随机唯一字符串的最快方法

Posted

技术标签:

【中文标题】在 Python 3 中生成具有随机长度的类随机唯一字符串的最快方法【英文标题】:Fastest way to generate a random-like unique string with random length in Python 3 【发布时间】:2018-07-03 10:03:52 【问题描述】:

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

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

【问题讨论】:

您使用 numpy 处理随机数而不是 stdlib random 模块有什么特别的原因吗? 不,随机的更快吗? 请注意,您实际上并没有生成 40k 个密钥。您正在生成 更少 键,因为如果有重复,您不会生成更多。 @MartijnPieters 好吧...至少有 99.999999997829302352% 的概率,他们实际上确实产生了 40k 个密钥:eval.in/951632 好吧定义随机......你想要它有多随机? 【参考方案1】:

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

使用集合,而不是列表,并且测试唯一性要快得多;集合成员资格测试需要与集合大小无关的恒定时间,而列表需要 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 多分钟,超过十倍。

Numpy 的 random.choice 函数,密码强度不高

您可以通过放弃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

os.urandom() 字节和产生字符串的不同方法

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

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

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))

真的快:

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

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

这是,再次加密强大os.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() 会再次产生偏差;我们必须产生比 12 到 19 个 base36“数字”所需的更多位的熵。例如,对于 17 位数字,我们不能使用字节产生 36 ** 17 个不同的值,只能产生最接近的 256 ** 11 个字节,这大约是高了 1.08 倍,所以我们最终会产生偏差朝向AB,以及在较小程度上C(感谢Stefan Pochmann 指出这一点)。

选择(36 ** length)以下的整数并将整数映射到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)

接下来,我们需要一种快速且加密安全的方法来在一个范围内挑选我们的号码。您仍然可以为此使用os.urandom(),但是您必须将字节屏蔽到最大位数,然后循环直到您的实际值低于限制。这实际上已经由secrets.randbelow() function 实现。在 Python 版本 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 方法一样快,但可以生成全范围的键!

【讨论】:

您可以将生成器表达式而不是列表传递给join @Michael:这会使连接变慢,所以我明确不这样做。 @Michael:见List comprehension without [ ] in Python 仔细研究了 base64(或者更确切地说是 base32)解决方案以及 b32encode 的工作原理。我不认为这是有偏见的。您想要 32 的幂,并且您创建的幂至少为 256 的幂。所以你得到一个精确的倍数,你只是创建了一些额外的位。 b32encode 的工作方式以及从其结果中提取的方式,我认为您只提取随机部分,而不是从填充中提取。 @CesareIurlaro 是的,我的答案中的代码生成的字符串随机长度至少为 12 个字符,每个字符最多 20 个字符。【参考方案2】:

所以这是一场速度赛吗?

基于 Martijn Pieters 的工作,我有一个解决方案,它巧妙地利用另一个库来生成随机字符串:uuid

我的解决方案是生成一个uuid4,对其进行base64编码并将其大写,以仅获取我们想要的字符,然后将其切片为随机长度。

这适用于这种情况,因为我们所追求的输出长度 (12-20) 比 uuid4 的最短 base64 编码要短。它也非常快,因为uuid 非常快。

我还把它变成了一个生成器而不是一个常规函数,因为它们可以更高效。

有趣的是,使用标准库的 randint 函数比 numpy 的要快。

这是测试输出:

Timing 40k keys 10 times with produce_amount_keys
20.899942063027993
Timing 40k keys 10 times with produce_amount_keys, stdlib randint
20.85920040300698
Timing 40k keys 10 times with uuidgen
3.852462349983398
Timing 40k keys 10 times with uuidgen, stdlib randint
3.136272903997451

这是uuidgen()的代码:

def uuidgen(count, _randint=np.random.randint):
    generated = set()

    while True:
        if len(generated) == count:
            return

        candidate = b64encode(uuid4().hex.encode()).upper()[:_randint(12, 20)]
        if candidate not in generated:
            generated.add(candidate)
            yield candidate

而here 是整个项目。 (在撰写本文时提交d9925d)。


感谢 Martijn Pieters 的反馈,我对方法进行了一些改进,增加了熵,并将其加速了大约 1/6 倍。

在将所有小写字母转换为大写字母时仍然会丢失很多熵。如果这很重要,那么可能建议使用 b32encode() 代替,它包含我们想要的字符,减去 0189

新的解决方案如下:

def urandomgen(count):
    generated = set()

    while True:
        if len(generated) == count:
            return

        desired_length = randint(12, 20)

        # # Faster than math.ceil
        # urandom_bytes = urandom(((desired_length + 1) * 3) // 4)
        #
        # candidate = b64encode(urandom_bytes, b'//').upper()
        #
        # The above is rolled into one line to cut down on execution
        # time stemming from locals() dictionary access.

        candidate = b64encode(
            urandom(((desired_length + 1) * 3) // 4),
            b'//',
        ).upper()[:desired_length]

        while b'/' in candidate:
            candidate = candidate.replace(b'/', choice(ALLOWED_CHARS), 1)

        if candidate not in generated:
            generated.add(candidate)
            yield candidate.decode()

以及测试输出:

Timing 40k keys 10 times with produce_amount_keys, stdlib randint
19.64966493297834
Timing 40k keys 10 times with uuidgen, stdlib randint
4.063803717988776
Timing 40k keys 10 times with urandomgen, stdlib randint
2.4056471119984053

我的存储库中的新提交是5625fd。


Martijn 关于熵的 cmets 让我思考。我对base64.upper() 使用的方法使字母比数字更常见。我以更加二元的思维重新审视了这个问题。

我们的想法是从os.urandom() 获取输出,将其解释为一长串 6 位无符号数字,并将这些数字用作允许字符的滚动数组的索引。第一个 6 位数字将从A..Z0..9A..Z01 范围内选择一个字符,第二个 6 位数字将从2..9A..Z0..9A..T 范围内选择一个字符,依此类推。

这会稍微降低熵,因为第一个字符包含2..9 的可能性会稍微降低,第二个字符包含U..Z0 的可能性会降低,依此类推,但它比以前好多了。

uuidgen()稍快,比urandomgen()稍慢,如下图:

Timing 40k keys 10 times with produce_amount_keys, stdlib randint
20.440480664998177
Timing 40k keys 10 times with uuidgen, stdlib randint
3.430628580001212
Timing 40k keys 10 times with urandomgen, stdlib randint
2.0875444510020316
Timing 40k keys 10 times with bytegen, stdlib randint
2.8740892770001665

我不完全确定如何消除最后一点熵粉碎;偏移字符的起点只会稍微移动模式,随机化偏移会很慢,打乱地图仍然会有一段时间......我对想法持开放态度。

新代码如下:

from os import urandom
from random import randint
from string import ascii_uppercase, digits

# Masks for extracting the numbers we want from the maximum possible
# length of `urandom_bytes`.
bitmasks = [(0b111111 << (i * 6), i) for i in range(20)]
allowed_chars = (ascii_uppercase + digits) * 16  # 576 chars long


def bytegen(count):
    generated = set()

    while True:
        if len(generated) == count:
            return

        # Generate 9 characters from 9x6 bits
        desired_length = randint(12, 20)
        bytes_needed = (((desired_length * 6) - 1) // 8) + 1

        # Endianness doesn't matter.
        urandom_bytes = int.from_bytes(urandom(bytes_needed), 'big')

        chars = [
            allowed_chars[
                (((urandom_bytes & bitmask) >> (i * 6)) + (0b111111 * i)) % 576
            ]
            for bitmask, i in bitmasks
        ][:desired_length]

        candidate = ''.join(chars)

        if candidate not in generated:
            generated.add(candidate)
            yield candidate

完整的代码以及更深入的实现自述文件已在de0db8 结束。

我尝试了几件事来加快实施速度,如回购所示。肯定有帮助的是数字和 ASCII 大写字母是连续的字符编码。

【讨论】:

为什么 b64encode 十六进制字符串?您正在严重减少熵,您的输出基于每个只有 16 个不同值的字节。这严重减少了您在其中使用的键的范围。至少,将二进制数据编码为 base 64。接下来,base 64 使用一组不同的字符(不仅仅是字母和数字,还有 +/ 这个;这很重要,你可能想要将它们映射到有效的关键字符。 我其实不知道 b64 使用了 +/,我现在就去玩一下。 另外,b64encode() 会生成一个bytes 对象,而不是str 字符串,因此您必须再次解码。 如果我使用uuid4().bytes 而不是uuid4().hex.encode(),我可以轻松生成一百万个唯一的 12 字符值,而后者每百万生成大约 5 个重复值。而且它当然更快,因为您不必生成十六进制字节串。 Base32 也缺少 01,而不仅仅是 89【参考方案3】:

一个简单快速的:

def b36(n, N, chars=string.ascii_uppercase + string.digits):
    s = ''
    for _ in range(N):
        s += chars[n % 36]
        n //= 36
    return s

def produce_amount_keys(amount_of_keys):
    keys = set()
    while len(keys) < amount_of_keys:
        N = np.random.randint(12, 20)
        keys.add(b36(secrets.randbelow(36**N), N))
    return keys

-- 编辑: 以下是 Martijn 答案的先前版本。在我们讨论之后,他添加了另一个解决方案,这与我的基本相同,但有一些优化。但是,它们并没有太大帮助,在我的测试中,它只比我的快 3.4%,所以在我看来,它们大多只是使事情复杂化。 --

与 Martijn 在his accepted answer 中的最终解决方案相比,我的解决方案要简单得多,大约快 1.7 倍,而且没有偏差:

Stefan
8.246490597876106 seconds.
8 different lengths from 12 to 19
  Least common length 19 appeared 124357 times.
  Most common length 16 appeared 125424 times.
36 different characters from 0 to Z
  Least common character Q appeared 429324 times.
  Most common character Y appeared 431433 times.
36 different first characters from 0 to Z
  Least common first character C appeared 27381 times.
  Most common first character Q appeared 28139 times.
36 different last characters from 0 to Z
  Least common last character Q appeared 27301 times.
  Most common last character E appeared 28109 times.

Martijn
14.253227412021943 seconds.
8 different lengths from 12 to 19
  Least common length 13 appeared 124753 times.
  Most common length 15 appeared 125339 times.
36 different characters from 0 to Z
  Least common character 9 appeared 428176 times.
  Most common character C appeared 434029 times.
36 different first characters from 0 to Z
  Least common first character 8 appeared 25774 times.
  Most common first character A appeared 31620 times.
36 different last characters from 0 to Z
  Least common last character Y appeared 27440 times.
  Most common last character X appeared 28168 times.

Martijn 的第一个字符有偏差,A 出现的频率太高,8 的出现频率太低。我进行了十次测试,他最常见的第一个字符总是AB(每个五次),他最不常见的字符总是789(两个、三个和五次,分别)。我也分别检查了长度,长度17特别糟糕,他最常见的第一个字符总是出现大约51500次,而他最不常见的第一个字符出现大约25400次。

有趣的旁注:我正在使用 Martijn 驳回的 secrets 模块 :-)

我的整个剧本:

import string
import secrets
import numpy as np
import os
from itertools import islice, filterfalse
import math

#------------------------------------------------------------------------------------
#   Stefan
#------------------------------------------------------------------------------------

def b36(n, N, chars=string.ascii_uppercase + string.digits):
    s = ''
    for _ in range(N):
        s += chars[n % 36]
        n //= 36
    return s

def produce_amount_keys_stefan(amount_of_keys):
    keys = set()
    while len(keys) < amount_of_keys:
        N = np.random.randint(12, 20)
        keys.add(b36(secrets.randbelow(36**N), N))
    return keys

#------------------------------------------------------------------------------------
#   Martijn
#------------------------------------------------------------------------------------

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()):
    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_martijn(amount_of_keys):
    def gen_keys(_urandom=os.urandom, _encode=b36encode, _randint=np.random.randint, _factor=math.log(256, 36)):
        while True:
            count = _randint(12, 20)
            yield _encode(_urandom(math.ceil(count / _factor)))[-count:].decode('ascii')
    return list(islice(unique_everseen(gen_keys()), amount_of_keys))

#------------------------------------------------------------------------------------
#   Needed for Martijn
#------------------------------------------------------------------------------------

def unique_everseen(iterable, key=None):
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

#------------------------------------------------------------------------------------
#   Benchmark and quality check
#------------------------------------------------------------------------------------

from timeit import timeit
from collections import Counter

def check(name, func):
    print()
    print(name)

    # Get 999999 keys and report the time.
    keys = None
    def getkeys():
        nonlocal keys
        keys = func(999999)
    t = timeit(getkeys, number=1)
    print(t, 'seconds.')

    # Report statistics about lengths and characters
    def statistics(label, values):
        ctr = Counter(values)
        least = min(ctr, key=ctr.get)
        most = max(ctr, key=ctr.get)
        print(len(ctr), f'different labels from', min(ctr), 'to', max(ctr))
        print(f'  Least common label', least, 'appeared', ctr[least], 'times.')
        print(f'  Most common label', most, 'appeared', ctr[most], 'times.')
    statistics('length', map(len, keys))
    statistics('character', ''.join(keys))
    statistics('first character', (k[0] for k in keys))
    statistics('last character', (k[-1] for k in keys))

for _ in range(2):
    check('Stefan', produce_amount_keys_stefan)
    check('Martijn', produce_amount_keys_martijn)

【讨论】:

不错!你的又快了一点,因为它放弃了 int -> bytes 和 bytes -> int 转换。 请注意,您的b36 号码基本上是倒数的。在这里并不重要。然而,这似乎是我的情况出现偏差的原因,因为我生成的整数值大于36 ** (desired length),这增加了我再次丢弃的更多信息。这在长度 17 中尤其明显,因为17 / math.log(256, 36) 几乎但不完全是 11。一个字节的剩余 0.0139 小数会比其他值中的较大边距产生更大的偏差。这一切都是因为 base36 不能干净地划分为 2 的幂。看起来我只能用randbelow() 来避免偏差! @MartijnPieters 是的,“落后”就是我将其从 base36 重命名为 b36 的原因。使用“基础”这个词感觉不对,因为这听起来像是数学基础表示。但是只有“b”我觉得可以这样做,它没有加载。如果有的话,“b36”听起来像编程中的编码,它不需要一个任意大的数字,而是 bytes,然后我认为我正在做的类似于用小端。是的,randbelow 可能是避免偏见的最佳选择。或者在您的方法中,您可以丢弃太大的字节,但这似乎很复杂。【参考方案4】:

警告:这不是加密安全。我想给 Martijn 的最佳答案中的另一种 numpy 方法。

numpy 函数并未真正优化为在小任务的循环中重复调用;相反,最好批量执行每个操作。这种方法提供了比您需要的更多的键(在这种情况下是大量的,因为我夸大了高估的需要),因此内存效率较低,但仍然非常快。

    我们知道您所有的字符串长度都在 12 到 20 之间。只需一次生成所有字符串长度。我们知道最终的set 有可能缩减最终的字符串列表,因此我们应该预料到这一点并制作比我们需要的更多的“字符串长度”。 20,000 额外是多余的,但要说明一点:

    string_lengths = np.random.randint(12, 20, 60000)

    与其在for 循环中创建我们的所有序列,不如创建一个足够长的一维字符列表,以将其切割成 40,000 个列表。在绝对最坏的情况下,(1) 中的所有随机字符串长度都是最大长度 20。这意味着我们需要 800,000 个字符。

    pool = list(string.ascii_letters + string.digits)

    random_letters = np.random.choice(pool, size=800000)

    现在我们只需要将随机字符列表切碎。使用np.cumsum(),我们可以获得子列表的连续起始索引,np.roll() 将该索引数组偏移 1,以提供相应的结束索引数组。

    starts = string_lengths.cumsum()

    ends = np.roll(string_lengths.cumsum(), -1)

    按索引切碎随机字符列表。

    final = [''.join(random_letters[starts[x]:ends[x]]) for x, _ in enumerate(starts)]

把它们放在一起:

def numpy_approach():
    pool = list(string.ascii_letters + string.digits)
    string_lengths = np.random.randint(12, 20, 60000)   
    ends = np.roll(string_lengths.cumsum(), -1) 
    starts = string_lengths.cumsum()
    random_letters = np.random.choice(pool, size=800000)
    final = [''.join(random_letters[starts[x]:ends[x]]) for x, _ in enumerate(starts)]
    return final

timeit 结果:

322 ms ± 7.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

【讨论】:

嘿,你的电脑和我的和 Martijn 的不一样;你介意timeit'ing 我们在你的机器上和你的机器上的实现之一,所以我们有某种形式的上下文吗? @AdamBarnes 当然可以,但是我要等好几个小时才能到达那台 PC。完成后会更新您。 非常感谢。 @AdamBarnes 在尝试设置测试用例的过程中,我认为您的输出中可能存在问题。我测量了所有输出字符串的长度:len_dict = 12: 4451, 13: 4352, 14: 4492, 15: 26705。但是,创建和耗尽生成器的时间相当相似 (262 ms ± 2.41 ms) - 你的速度更快 - 对我来说。 这真的很简单;我只生成最多 15 个字符的位掩码。多么愚蠢。【参考方案5】:

替代方法:创造而不是测试的独特性

解决您的问题的明显方法是生成随机输出,然后检查它是否是唯一的。虽然我没有提供实现,但这里有另一种方法:

    生成看起来尽可能随机的输出 生成保证唯一且看起来有些随机的输出 将它们组合起来

现在您的输出保证是唯一的,并且看起来是随机的。

示例

假设您要生成 999999 个长度为 12 和 20 的字符串。该方法当然适用于所有字符集,但让我们保持简单并假设您只想使用 0-9。

    生成长度为 6 到 14 的随机输出 随机排列从 000000 到 999999 的数字(是的,6 位数字在明显的随机性方面“牺牲”了很多,但如果使用更大的字符集,则不需要这么多字符) 现在以必须保留唯一性的方式组合它们。最简单的方法是简单地连接实体,但您当然可以想出不太明显的解决方案。

小规模示例

    产生随机性:

    sdfdsf xxer 版本

    生成唯一性

    xd ae bd

    合并

    xdsdfdsf 艾克斯 bdver

请注意,此方法假定您每个条目的字符数最少,您的问题似乎就是这种情况。

【讨论】:

这个的主要问题(除了缺乏熵)是它仍然需要以某种方式从允许的字符快速构建一个字符串。 Martijn 和我自己使用 base64 库解决了这个问题,它接受字节并返回 ASCII,一旦你这样做了,你也可以把它交给 urandom() 字节。由于集合,唯一性不是缓慢因素。在不检查唯一性的情况下,我的解决方案之间的时间差异是如此之小,以至于由于系统负载,一个随机比另一个快。 @AdamBarnes 感谢您的评论。我担心检查唯一性太有效,这种方法不值得。我将它留在这里是为了娱乐价值。 -- 最后一点:你确定速度差异不是那么小,因为你已经接受了使用集合的潜在开销(它擅长检查唯一性吗?) 我“有信心”,我不确定。由于您没有提供代码示例,因此我无法测试您的实现,尽管我可以自己编写它,但我正忙着将自己的答案扩展到荒谬的长度……(对此感到抱歉)。如果您想参与其中,请发疯,一起执行一个实现,然后自己看看。如果您愿意,可以针对my repository 发起 PR。【参考方案6】:

简单

如果你使用 python > 3.6

d = 100(任意长度)

或者如果你想要一个随机范围

d = random.randrange(start=start_range,stop=stop_range)

import string
import random
random_str = ''.join(random.choices(string.ascii_uppercase +
                         string.digits, k = d))

如果您需要更安全的方式

random_str = ''.join(secrets.choice(string.ascii_uppercase + string.digits)
                                              for i in range(d))

【讨论】:

以上是关于在 Python 3 中生成具有随机长度的类随机唯一字符串的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

golang 在Golang中生成给定长度的随机字符串

在 R 中生成具有相同数量节点和关系的多个随机图?

具有给定长度的Java随机数[重复]

如何在 Postgres 9.6+ 中生成长度为 N 的随机、唯一的字母数字 ID?

在 Python 中生成一个随机字母

在 Python 中生成随机数的标准方法是啥?