私は次のようなランダムな文字列を作成する方法を知っています:
''.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()
partialは、モジュールまたはより多くのローカルの属性の検索を回避します。すべての参照が適切に配置されている単一の呼び出し可能オブジェクトであるため、特にループで実行される場合は実行が高速になります。
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
私のバージョンはすでに10,000個のアイテムのほぼ2倍の速さです。 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倍以上かかりました。
secrets
モジュールを放棄し、代わりにnp.random.choice()
を使用することで、これをさらに高速化できます。ただし、これにより暗号化レベルのランダム性は生成されませんが、ランダム文字の選択は2倍高速です。
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
モジュールから unique_everseen()
function を取得することもできますRecipesセクションで一意性を処理してから、無限ジェネレーターと 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
次に、UUID4(基本的には os.urandom()
の単なるラッパー)とBase64を使用するために Adam Barnesのアイデア を続けることができます。しかし、Base64を大文字に変換し、2つの文字をランダムに選択した文字に置き換えることにより、彼の方法はこれらの文字列のエントロピーを厳しく制限します(可能な範囲で一意の値の全範囲を生成することはありません。20文字の文字列は(256 ** 15) / (36 ** 20)
== 1エントロピーの99437ビットごと!).
Base64エンコードでは、大文字と小文字の両方と数字を使用しますが、adds-
および/
文字(またはURLセーフなバリアントの場合は+
および_
)も使用します。大文字と数字のみの場合、出力を大文字にして、これらの余分な2文字を他のランダム文字にマッピングする必要があります。このプロセスは、os.urandom()
によって提供されるランダムデータから大量のエントロピーを破棄します。 Base64を使用する代わりに、大文字と2から8の数字を使用するBase32エンコードを使用することもできます。したがって、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))
これはreally高速です:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b32 as p', number=10)
4.572628145979252
40秒のキー、10回、わずか4秒で。約75倍の速さです。 os.urandom()
をソースとして使用する速度は否定できません。
これは、再び暗号的に強力; os.urandom()
は、暗号化用のバイトを生成します。一方、生成される可能性のある文字列の数を90%以上削減しました(((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100
は90.5)。出力で0
、1
、8
および9
桁を使用しなくなりました。
したがって、おそらく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バージョンはこのバージョンのほぼ2倍の速さです(テーブルを使用した効率的なPython実装のおかげです)。 numpy.random.choice()
バージョン。
ただし、os.urandom()
バイアスを生成を再度使用します。 12〜19のbase36 'digits'に必要なエントロピー以上のビットを生成する必要があります。たとえば、17桁の場合、バイトを使用して36 ** 17の異なる値を生成することはできません。256** 11バイトに最も近いものだけで、これは約1.08倍高すぎるため、最終的にバイアスになりますA
、B
、およびそれよりも少ない範囲で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バージョン<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アプローチとほぼ同じ速度ですが、すべてのキーを生成します!
だから、それはスピードレースですか?
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
ここ はプロジェクト全体です。 (コミット時 d9925d 執筆時点)。
Martijn Pietersからのフィードバックのおかげで、私はこの方法をいくらか改善し、エントロピーを増やし、約1/6の速度で高速化しました。
すべての小文字を大文字にキャストすると、多くのエントロピーが失われます。それが重要な場合は、代わりにb32encode()
を使用することをお勧めします。これには、0
、1
、8
、および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()
で使用した方法は、文字SOを数字よりもはるかに一般的にします。
このアイデアは、os.urandom()
から出力を取得し、6ビットの符号なし数字の長い文字列として解釈し、許可された文字のローリング配列のインデックスとしてそれらの数字を使用することでした。最初の6ビット番号はA..Z0..9A..Z01
の範囲から文字を選択し、2番目の6ビット番号は2..9A..Z0..9A..T
の範囲から文字を選択します。
これにより、最初の文字に2..9
が含まれる可能性がわずかに低くなり、2番目の文字に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
そして、完全なコードは、実装の詳細なREADMEとともに) de0db8 で終わりました。
リポジトリに表示されているように、実装を高速化するためにいくつかのことを試しました。間違いなく役立つのは、数字とASCII大文字が連続している文字エンコードです。
シンプルで速いもの:
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
-Edit:以下は、Martijnの回答の以前のリビジョンを参照しています。私たちの議論の後、彼は別のソリューションを追加しました。これは基本的に私のものと同じですが、いくつかの最適化があります。しかし、彼らはあまり助けにはなりませんが、私のテストでは私のものよりも約3.4%速いだけなので、私の意見では、それらはほとんど物事を複雑にします。 -
彼の受け入れられた答え のMartijnの最終的な解決策と比較して、私の方がずっと単純で、約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'sは最初の文字に偏りがあり、A
は非常に頻繁に表示され、8
滅多にありません。私はテストを10回実行しました。彼の最も一般的な最初の文字は常にA
またはB
(各5回)で、最も一般的でない文字は常に7
、8
または9
(それぞれ2、3、5回)。長さも個別にチェックしました。長さ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)
注意:これは暗号的に安全ではありません。私は、Martijnの素晴らしい答えに代わるnumpy
アプローチを提供したいと思います。
numpy
関数は、小さなタスクのループで繰り返し呼び出されるように最適化されていません。むしろ、各操作を一括して実行することをお勧めします。このアプローチは、必要以上のキーを提供します(この場合、私は過大評価する必要性を過度に誇張したため、このように大規模に)、メモリ効率は低くなりますが、それでも超高速です。
すべての文字列の長さが12〜20であることを知っています。すべての文字列の長さを一度に生成します。最終的なset
は文字列の最終リストを切り捨てる可能性があることを知っているので、それを予測し、必要以上の「文字列長」を作成する必要があります。 20,000余分に過剰ですが、それはポイントを作ることです:
string_lengths = np.random.randint(12, 20, 60000)
すべてのシーケンスをfor
ループで作成するのではなく、40,000個のリストにカットするのに十分な長さの1D文字リストを作成します。 absolute最悪のシナリオでは、(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)
質問に対する明白なアプローチは、ランダムな出力を生成し、それが一意であるかどうかを確認することです。私は実装を提供していませんが、代替アプローチがあります:
これで、一意であることが保証され、ランダムに見える出力ができました。
12と20の長さの999999文字列を生成するとします。このアプローチはもちろんすべての文字セットで機能しますが、単純に保ち、0〜9のみを使用すると仮定します。
ランダム性を生成する:
sdfdsf xxer ver
一意性を生成する
xd ae bd
組み合わせる
xdsdfdsf aexxer bdver
この方法では、エントリごとに最小文字数があることを前提としていることに注意してください。これは質問の場合のようです。