在Python 3中生成随机长度的类似随机字符串的最快方法

时间:2018-01-24 11:14:38

标签: python string python-3.x random

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

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

5 个答案:

答案 0 :(得分:46)

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

使用而非列表,并且测试唯一性要快得多;集合成员资格测试需要与设置大小无关的恒定时间,而列表需要O(N)线性时间。使用set comprehension一次生成一系列键,以避免在循环中查找并调用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模块食谱部分获取unique_everseen() function,让它处理唯一性,然后使用无限生成器和{{3}将结果限制为我们想要的数字:

# 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()字节和生成字符串的不同方法

接下来,我们可以继续使用itertools.islice() function来使用UUID4(基本上只是Adam Barnes's ideas的包装)和Base64。但是通过大小写折叠Base64并用随机选择的字符替换2个字符,他的方法严重限制了这些字符串中的熵(你不会产生可能的全部唯一值,只使用{{1的20个字符的字符串每个99437位的熵都有=} 1。)。

Base64编码同时使用大写和小写字符和数字,但添加 (256 ** 15) / (36 ** 20)-字符(或/和{{1}对于URL安全变体)。对于只有大写字母和数字,您必须将输出大写并将这些额外的两个字符映射到其他随机字符,这个过程会从+提供的随机数据中丢弃大量熵。您也可以使用Base32编码,而不是使用Base64,它使用大写字母和数字2到8,因此产生的字符串有32 ** n种可能性,而不是36 ** n。但是,这可以加快上述尝试的速度:

_

真的快:

os.urandom()

40k键,10次,仅需4秒钟。所以大约快75倍;使用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 生成用于加密使用的字节。另一方面,我们将可能产生的字符串数量减少了90%以上(os.urandom()为90.5),我们不再使用os.urandom()((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100,{{1}输出中有}和0个数字。

所以也许我们应该使用1技巧来产生适当的Base36编码;我们必须制作自己的8函数:

9

并使用:

urandom()

这相当快,最重要的是产生36个大写字母和数字的全部范围:

b36encode()

当然,base32版本几乎是这个版本的两倍(由于使用表格的高效Python实现),但使用自定义Base36编码器的速度仍然是非加密安全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)) 会再次产生偏见;我们必须产生比12到19个base36&#39;之间所需的更多比特的熵。例如,对于17位数字,我们不能使用字节产生36 ** 17个不同的值,只有最近的256 ** 11字节,大约是1.08倍,因此我们将结束偏向于>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b36 as p', number=10) 8.099918447987875 numpy.random.choice(),并且在较小程度上os.urandom()(感谢os.urandom()指出这一点)。

选取A以下的整数并将整数映射到base36

因此,我们需要找到一种安全的随机方法,该方法可以在B(包含)和C(不包括)之间平均分配值。然后我们可以将数字直接映射到所需的字符串。

首先,将整数映射到字符串;以下内容已经过调整,以最快的速度生成输出字符串:

(36 ** length)

接下来,我们需要一种快速且加密安全的方法来选择范围内的数字。您仍然可以使用0,但是您必须将字节掩码到最大位数,然后循环直到您的实际值低于限制。这实际上已由Stefan Pochmann实施。在Python版本中&lt; 3.6你可以使用secrets.randbelow() function,它使用完全相同的方法和一些额外的包装来支持大于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)

然后这非常接近(可能有偏见的)base64解决方案:

os.urandom()

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

答案 1 :(得分:8)

这是速度赛吗?

在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()代替我们想要的字符,减去018 ,和9

新解决方案如下:

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对熵的评论让我思考。我与base64.upper()一起使用的方法使得字母比数字更常见。我用一个更加二元化的思维重新审视了这个问题。

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

这有一点点熵,因为第一个字符包含2..9的可能性稍小,第二个字符不太可能包含U..Z0,依此类推,但是&#39;比以前好多了。

它比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大写字母是连续的。

答案 2 :(得分: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 {label}s 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)

答案 3 :(得分:1)

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

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

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

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

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

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

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

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

    starts = string_lengths.cumsum()

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

  4. 按索引填写随机字符列表。

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

  5. 全部放在一起:

    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)
    

答案 4 :(得分:0)

替代方法:创建中的唯一性而不是通过测试

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

  1. 生成尽可能随机的输出
  2. 生成保证唯一的输出,看起来有点随机
  3. 将它们合并
  4. 现在您的输出保证是唯一的,并且看起来是随机的。

    实施例

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

    1. 生成长度为6到14
    2. 的随机输出
    3. 随机排列数字000000到999999(是的,6个数字相当于“牺牲”的明显随机性,但是使用更大的字符,你将不需要这么多字符)
    4. 现在以必须保留唯一性的方式组合它们。最简单的方法是实体的简单连接,但你当然可以想到不太明显的解决方案。
    5. 小规模示例

      1. 生成随机性:

        sdfdsf xxer ver

      2. 生成唯一性

        XD AE BD

      3. 联合

        xdsdfdsf aexxer bdver

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

相关问题