16 行,一个首字符是数字的 UUID
我有个小脚本,需要生成 ID。要求不高:唯一、够长、不要看起来像密码。
uuid.uuid4() 其实就够。但它生成的是版本 4 UUID,字符串里固定有一位是 4。我这边有些地方按版本号做判断,看到 4 就按「纯随机」处理,排序时完全随机。
我想要的是「看起来像按时间排的」——版本 6 的布局。于是自己拼了一个:
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import secrets
import uuid
while True:
value = secrets.randbits(128)
value = (value & ~(0xF << 76)) | (6 << 76) # UUIDv6
value = (value & ~(0x3 << 62)) | (2 << 62) # RFC 4122 variant
if value >> 124 >= 0xA:
print(uuid.UUID(int=value))
break十六行,没有依赖,uv run uuid49.py 直接跑。
两个位运算分别改什么
UUID 是 128 位整数。标准字符串形式 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 里,忽略连字符后的第 13 个十六进制位是版本号,第 17 个是变体位。
版本位在整数的第 76 到 79 位:
value = (value & ~(0xF << 76)) | (6 << 76)先把这四位清零,再写入 6。
变体位在第 62 到 63 位:
value = (value & ~(0x3 << 62)) | (2 << 62)RFC 4122 要求的变体是二进制 10,也就是整数 2。
就这两行。secrets.randbits(128) 给出 128 位密码学随机数,剩下两位段改掉,一个合法的 UUIDv6 就出来了。
它不是真的 v6
得说清楚:真正的 UUIDv6 前 48 位是时间戳,我这版是纯随机。
时间戳我没塞。我要的只是版本号是 6、布局合法;按生成时间排序用不上。有些库看到版本 6 会走时间排序分支,有些服务会按版本号选校验规则。我只需要通过这一层检查。
这也意味着它不能用来排序。生成两个,先后顺序是随机的。对我这个用途来说无所谓。
首字符为什么不能是字母
最后那个循环:
if value >> 124 >= 0xA:
print(uuid.UUID(int=value))
breakvalue >> 124 取最高的 4 位,也就是 UUID 字符串的第一个字符。它必须小于 0xA,也就是 0 到 9。
如果首字符变成 a 到 f,就重新生成。
理由不硬核:我希望这东西在列表里长得像编号,不像哈希。贴进表格、放进文件名、排在目录里,首字符是数字看起来整齐。有些地方按字符串排序,数字开头也更符合直觉。
生成失败就重来。最高位落在 a 到 f 的概率是 6/16,平均重试不到两次,几乎感觉不到。
为什么不用 uuid4 加改写
uuid.uuid4() 出来的是 UUID 对象,改版本位要把 .int 取出来,位运算,再 UUID(int=...) 塞回去。功能一样,但多一层转换。
而且 uuid4 已经设过版本 4 和变体位,我再覆盖一遍,读代码的人会困惑:到底以哪次为准?
直接从 secrets.randbits 开始,两个位段一次设好,每一步都清楚。
这脚本值不值得写
严格说,它可以用一行 python -c 代替:
python -c "import secrets,uuid; ..."但一行命令塞进引号里,转义就开始恶心;再加上重试循环,很快就没法读了。
十六行的文件,命名清楚,注释标了每一位在干什么。下次我要改首字符规则、换版本号、加前缀,打开就能改。
每次要 ID,我就跑它。十六行,打开就知道在干什么,改起来不用先读一遍。