【问题标题】:Short Unique Hexadecimal String in PythonPython中的短唯一十六进制字符串
【发布时间】:2017-01-01 10:01:02
【问题描述】:

我需要在 Python 3 中生成一个满足以下要求的唯一十六进制字符串:

  1. 应包含 6 个字符
  2. 它不应只包含数字。必须至少有一个字符。
  3. 这些生成的字符串应该是随机的。它们不应按任何顺序排列。
  4. 应该有最小的冲突概率

我考虑过 uuid4()。但问题是它生成的字符串包含太多字符,并且生成的字符串的任何子字符串在某些时候都可以包含所有数字(即没有字符)。

还有其他方法可以满足这个条件吗?提前致谢!

编辑

我们可以使用哈希(例如 SHA-1)来满足上述要求吗?

【问题讨论】:

  • uuid4() 仅生成数字 0-9 和字符 a-f(即十六进制数字)。您只想要十六进制数字,还是愿意使用任何字符?如果允许所有字符,它们可以是大写和小写吗?
  • 警告:6 个十六进制数字和至少需要一个字母 a-f 会给您 15,777,216 个可能的字符串。由于生日悖论,随机选择这些您应该期望在 4000 个字符串大小的集合中看到单个重复。根据您对这些代码的处理方式,您需要允许这样做。
  • 其实我计算16**5*6=6 291 456可能的字符串。
  • 纯字母字节可以放在 6 个不同的位置,但在某些情况下,不同的位置会产生相同的字符串。我认为有16**6(可能的十六进制字符串)-10**6(可能的十进制字符串)=15 777 216 排列。
  • @MoinuddinQuadri:抱歉耽搁了。但是我还不能测试任何答案,因为我现在被困在其他一些事情上。向所有回答的人道歉并感谢你们!

标签: python python-3.x uuid


【解决方案1】:

这是一个从所有允许的字符串中均匀采样的简单方法。统一采样使冲突尽可能少,而不是保留以前的键的日志或使用基于计数器的哈希(见下文)。

import random
digits = '0123456789'
letters = 'abcdef'
all_chars = digits + letters
length = 6

while True:

   val = ''.join(random.choice(all_chars) for i in range(length))

   # The following line might be faster if you only want hex digits.
   # It makes a long int with 24 random bits, converts it to hex,
   # drops '0x' from the start and 'L' from the end, then pads
   # with zeros up to six places if needed
   # val = hex(random.getrandbits(4*length))[2:-1].zfill(length)

   # test whether it contains at least one letter
   if not val.isdigit():
       break

# now val is a suitable string
print val
# 5d1d81

或者,这里有一种更复杂的方法,它也可以均匀采样,但不使用任何开放式循环:

import random, bisect
digits = '0123456789'
letters = 'abcdef'
all_chars = digits + letters
length = 6

# find how many valid strings there are with their first letter in position i
pos_weights = [10**i * 6 * 16**(length-1-i) for i in range(length)]
pos_c_weights = [sum(pos_weights[0:i+1]) for i in range(length)]

# choose a random slot among all the allowed strings
r = random.randint(0, pos_c_weights[-1])

# find the position for the first letter in the string
first_letter = bisect.bisect_left(pos_c_weights, r)

# generate a random string matching this pattern
val = ''.join(
    [random.choice(digits) for i in range(first_letter)]
    + [random.choice(letters)]
    + [random.choice(all_chars) for i in range(first_letter + 1, length)]
)

# now val is a suitable string
print val
# 4a99f0

最后,这是一个更复杂的方法,它使用随机数 r 直接索引到允许值的整个范围,即,这会将 0-15,777,216 范围内的任何数字转换为合适的十六进制字符串。这可以用来完全避免冲突(下面将详细讨论)。

import random, bisect
digits = '0123456789'
letters = 'abcdef'
all_chars = digits + letters
length = 6

# find how many valid strings there are with their first letter in position i
pos_weights = [10**i * 6 * 16**(length-1-i) for i in range(length)]
pos_c_weights = [sum(pos_weights[0:i+1]) for i in range(length + 1)]

# choose a random slot among all the allowed strings
r = random.randint(0, pos_c_weights[-1])

# find the position for the first letter in the string
first_letter = bisect.bisect_left(pos_c_weights, r) - 1

# choose the corresponding string from among all that fit this pattern
offset = r - pos_c_weights[first_letter]
val = ''
# convert the offset to a collection of indexes within the allowed strings 
# the space of allowed strings has dimensions
# 10 x 10 x ... (for digits) x 6 (for first letter) x 16 x 16 x ... (for later chars)
# so we can index across it by dividing into appropriate-sized slices
for i in range(length):
    if i < first_letter:
        offset, v = divmod(offset, 10)
        val += digits[v]
    elif i == first_letter:
        offset, v = divmod(offset, 6)
        val += letters[v]
    else:
        offset, v = divmod(offset, 16)
        val += all_chars[v]

# now val is a suitable string
print val
# eb3493

均匀抽样

我在上面提到过,这对所有允许的字符串统一进行采样。这里的其他一些答案完全随机选择 5 个字符,然后在随机位置强制将一个字母放入字符串中。这种方法产生的包含多个字母的字符串比随机得到的要多。例如,如果为前 5 个插槽选择了字母,则该方法总是产生一个 6 个字母的字符串;然而,在这种情况下,第六个选择实际上应该只有 6/16 的机会是一个字母。只有当前 5 个插槽是数字时,才能通过将字母强制放入第六个插槽来解决这些方法。在这种情况下,所有 5 位字符串将自动转换为 5 位加 1 个字母,从而提供过多的 5 位字符串。使用统一采样,如果前 5 个字符是数字,则应该有 10/16 的机会完全拒绝字符串。

以下是一些说明这些抽样问题的示例。假设您有一个更简单的问题:您想要一个由两个二进制数字组成的字符串,并规定其中至少一个必须是 1。如果您以相等的概率产生 01、10 或 11,那么冲突将是最罕见的。您可以通过为每个插槽选择随机位,然后丢弃 00 来做到这一点(类似于我上面的方法)。

但假设您改为遵循以下规则:做出两个随机二元选择。第一个选项将按原样在字符串中使用。第二个选项将确定插入额外 1 的位置。这类似于此处其他答案使用的方法。那么您将有以下可能的结果,其中前两列代表两个二元选择:

0 0 -> 10
0 1 -> 01
1 0 -> 11
1 1 -> 11

这种方法有 0.5 的机会产生 11,或 0.25 产生 01 或 10,因此会增加 11 个结果之间发生冲突的风险。

您可以尝试如下改进:做出三个随机二元选择。第一个选项将按原样在字符串中使用。如果第一个选项是 0,则第二个选项将转换为 1;否则它将按原样添加到字符串中。第三个选项将确定插入第二个选项的位置。那么你有以下可能的结果:

0 0 0 -> 10 (second choice converted to 1)
0 0 1 -> 01 (second choice converted to 1)
0 1 0 -> 10
0 1 1 -> 01
1 0 0 -> 10
1 0 1 -> 01
1 1 0 -> 11
1 1 1 -> 11

这为 01 或 10 提供了 0.375 的机会,为 11 提供了 0.25 的机会。因此这将略微增加重复 10 或 01 值之间发生冲突的风险。

减少冲突

如果您愿意使用所有字母而不是仅使用“a”到“f”(十六进制数字),您可以更改 letters 的定义,如 cmets 中所述。这将提供更多样化的字符串和更少的冲突机会。如果您生成了 1,000 个允许所有大写和小写字母的字符串,那么您只有大约 0.0009% 的机会生成任何重复项,而只有 3% 的机会生成十六进制字符串。 (这也将几乎消除循环中的双重通过。)

如果您真的想避免字符串之间的冲突,您可以将之前生成的所有值存储在 set 中,并在退出循环之前对其进行检查。如果您要生成少于约 500 万个密钥,这将是一件好事。除此之外,您还需要相当多的 RAM 来保存旧密钥,并且可能需要在循环中运行几次才能找到未使用的密钥。

如果您需要生成更多的密钥,您可以加密一个计数器,如Generating non-repeating random numbers in Python 所述。计数器及其加密版本都是 0 到 15,777,216 范围内的整数。计数器将从 0 开始计数,加密版本看起来像一个随机数。然后,您将使用上面的第三个代码示例将加密版本转换为十六进制。如果这样做,您应该在开始时生成一个随机加密密钥,并在每次计数器滚动超过您的最大值时更改加密密钥,以避免再次产生相同的序列。

【讨论】:

  • 很好的解释!点赞!但由于某种原因,我被限制不使用循环,我需要使用 uuid4() 或哈希来完成!
  • 嗯,这让它更有趣。但是什么的哈希值?计数器变量的哈希?随机数的哈希?
  • 随机数或字符串的哈希!
  • @JayPatel 我添加了一个哈希函数,可以将 0-15777216 范围内的任何数字转换为唯一的有效字符串(代码块 3)。我给它一个随机数以获得一个随机字符串。但是直接创建一个合适的随机字符串可能更简单(代码块2)。
  • 你是对的!这些方法太复杂了!哈哈..开玩笑!但是很好……通过哈希,我指的是sha1hashlib!但是非常感谢您提供详细解释的答案!
【解决方案2】:

以下方法的工作原理如下,首先选择一个随机字母以确保规则 2,然后从所有可用字符列表中选择 4 个随机条目。随机播放结果列表。最后,在除0 之外的所有条目列表中添加一个值,以确保字符串包含 6 个字符。

import random

all = "0123456789abcdef"
result = [random.choice('abcdef')] + [random.choice(all) for _ in range(4)]
random.shuffle(result)
result.insert(0, random.choice(all[1:]))
print(''.join(result))

给你类似的东西:

3b7a4e

这种方法避免了重复检查结果以确保它满足规则。

【讨论】:

  • 你为什么把第一个数字设为非零?
  • 这真的取决于它将如何使用。如果将字符串转换为数字,则前导零将使其少于 6 个字符。如果这不是问题,则可以删除该步骤。
  • 这种方法总是产生一个有效的字符串,但它不会对允许的区域进行均匀的采样。将第一选择限制为字母会使结果偏向多字母字符串,从而增加了冲突的机会。
  • 通过强加必须有一个字母的规则,结果已经倾斜。这个脚本没有强加第一个选择是字母的限制,这就是shuffle()的用途。
  • 我的意思是,您的方法将生成比通过在 allowed 字符串集中均匀采样所得到的字符串更多的字符串。无论他们如何洗牌,这都是正确的。例如,您的方法将生成概率为(6/16)**5 = 0.0074 的 6 个字母字符串。但是在允许的字符串中均匀采样会产生 6 个字母的字符串,概率为 (6**6)/(16**6 - 10**6) = 0.0030,其中 6**6 是可能的 6 个字母字符串的数量,16**6 - 10**6 是允许的字符串的数量。
【解决方案3】:

注意:更新了十六进制唯一字符串的答案。之前我假设是字母数字字符串。

您可以使用 uuidrandom 库创建自己的独特功能

>>> import uuid
>>> import random
# Step 1: Slice uuid with 5 i.e. new_id = str(uuid.uuid4())[:5] 
# Step 2: Convert string to list of char i.e. new_id = list(new_id)
>>> uniqueval = list(str(uuid.uuid4())[:5])
# uniqueval = ['f', '4', '4', '4', '5']

# Step 3: Generate random number between 0-4 to insert new char i.e.
#         random.randint(0, 4)
# Step 4: Get random char between a-f (for Hexadecimal char) i.e.
#         chr(random.randint(ord('a'), ord('f')))
# Step 5: Insert random char to random index
>>> uniqueval.insert(random.randint(0, 4), chr(random.randint(ord('a'), ord('f'))))
# uniqueval = ['f', '4', '4', '4', 'f', '5']

# Step 6: Join the list
>>> uniqueval = ''.join(uniqueval)
# uniqueval = 'f444f5'

【讨论】:

  • 我认为这总是包含至少一个数字,但问题要求至少一个字母。
  • 谢谢。是的,我想念理解那部分。更新了答案
  • @MoinuddinQuadri:只是问问。您对重复相同字符串的概率有任何想法吗?我的意思是唯一性的机会高多少?只是想得到一个想法!
  • @MoinuddinQuadri:你在 0 到 4 岁时是否特别有任何目的?如果我们在 0-5 之间插入呢?
【解决方案4】:

此函数返回符合您要求的第n个字符串,因此您可以简单地生成唯一整数并使用此函数进行转换。

def inttohex(number, digits):
    # there must be at least one character:
    fullhex = 16**(digits - 1)*6
    assert number < fullhex
    partialnumber, remainder = divmod(number, digits*6)
    charposition, charindex = divmod(remainder, digits)
    char = ['a', 'b', 'c', 'd', 'e', 'f'][charposition]
    hexconversion = list("{0:0{1}x}".format(partialnumber, digits-1))
    hexconversion.insert(charposition, char)

    return ''.join(hexconversion)

现在您可以使用例如获取特定的一个

import random

digits = 6
inttohex(random.randint(0, 6*16**(digits-1)), digits)

您不能同时拥有最大的随机性和最小的冲突概率。我建议使用随机排序的列表跟踪您分发了哪些数字,或者您是否以某种方式遍历所有数字。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-07-27
    • 2023-04-08
    • 2017-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-14
    • 2012-05-24
    相关资源
    最近更新 更多