Skip to content

16 行,一个首字符是数字的 UUID ​

我有个小脚本,需要生成 ID。要求不高:唯一、够长、不要看起来像密码。

uuid.uuid4() 其实就够。但它生成的是版本 4 UUID,字符串里固定有一位是 4。我这边有些地方按版本号做判断,看到 4 就按「纯随机」处理,排序时完全随机。

我想要的是「看起来像按时间排的」——版本 6 的布局。于是自己拼了一个:

python
# /// 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 位:

python
value = (value & ~(0xF << 76)) | (6 << 76)

先把这四位清零,再写入 6。

变体位在第 62 到 63 位:

python
value = (value & ~(0x3 << 62)) | (2 << 62)

RFC 4122 要求的变体是二进制 10,也就是整数 2。

就这两行。secrets.randbits(128) 给出 128 位密码学随机数,剩下两位段改掉,一个合法的 UUIDv6 就出来了。

它不是真的 v6 ​

得说清楚:真正的 UUIDv6 前 48 位是时间戳,我这版是纯随机。

时间戳我没塞。我要的只是版本号是 6、布局合法;按生成时间排序用不上。有些库看到版本 6 会走时间排序分支,有些服务会按版本号选校验规则。我只需要通过这一层检查。

这也意味着它不能用来排序。生成两个,先后顺序是随机的。对我这个用途来说无所谓。

首字符为什么不能是字母 ​

最后那个循环:

python
    if value >> 124 >= 0xA:
        print(uuid.UUID(int=value))
        break

value >> 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 代替:

bash
python -c "import secrets,uuid; ..."

但一行命令塞进引号里,转义就开始恶心;再加上重试循环,很快就没法读了。

十六行的文件,命名清楚,注释标了每一位在干什么。下次我要改首字符规则、换版本号、加前缀,打开就能改。

每次要 ID,我就跑它。十六行,打开就知道在干什么,改起来不用先读一遍。

Nine and AI — Licensed under the MIT License