随机数抽签器(等概率抽取 · 名单摇号 · 可复跑的 seed)— 整数、小数与分组

可以是区间里的整数、区间里的小数,也可以是你粘贴进来的一份名单——随机数取自浏览器加密级生成器,用“掩码加丢弃重抽”而不是取模来映射区间,所以每个结果的概率真的相等。支持不重复抽取、把名单平均分组,还有一个 seed 模式能让整次抽签复跑:你可以向别人证明结果只抽过一次,不是重抽到满意为止。页面下方有一段实测的取模偏差对比。输入的内容不会离开这台设备。

至
次
小数点后的位数,0 到 9
组数,留空=只抽签
#结果来自第几行用掉的字数

本次抽签记录

为什么取模不等于公平抽取

取一个随机字节再算 byte % 6 是最常用的捷径,可 256 并不是 6 的倍数:六个结果里有四个各自多拿到一个字节。下表让两种方法跑同一个随机源,所以这道偏差是你自己读出来的,不需要相信谁。

每种方法的次数
结果byte % 6期望掩码加重抽
生成器与 seed 模式

三件看着琐碎、做起来会错的事

三件看起来很琐碎、做起来会出错的事:在区间里取一个数,取几个不重复的数,从你粘贴进来的名单里抽人。区间抽取支持整数和小数,名单抽取不放回,两种模式都可以改成一律分组。每次抽签的字都来自浏览器的加密级生成器,结果旁边还列着这次花掉了多少个 32 位字——一次抽签究竟需要多少随机性,这是最诚实的量法。

表格下面那份"记录"有它具体的用处。一次抽签只有在事后拿得出来才值钱:随机源、样本池、每个结果连同它的下标、以及抽选的时刻。争论是在哪个群里开始的,就把这段贴到那里去,比说"我发誓真的是随机"有用得多。

为什么生成器比区间更重要

Math.random() 用来在游戏里洗个牌没问题,用在任何可能被人质疑的场合都不对:它是个伪随机生成器,状态就活在引擎里,不但可以被预测,而且取决于引擎,只带着 32 位种子。这里的字取自 crypto.getRandomValues,浏览器会从操作系统的熵池里给它补货。

第二处容易出错的地方是从一个字映射到区间的方式,那个取模偏差对比演示的正是它。一个字给你 4,294,967,296 个值;想要六个结果却去取一个字节的余数,就是把 256 个值折进六个桶。256 不是 6 的倍数,于是四个桶各装下 43 个字节,两个桶只有 42 个。被偏袒的那几个结果出现的频率是 16.796875%,而公平的抽取应当是 16.666667%——大约十三分之一个百分点,在一场抽奖里不算什么,在任何一个有阈值的场合就是决定性的。采样这步把字掩码到池子真正需要的那几位,掩出来的值越过末尾就丢掉重抽,所以被丢弃的那些抽取就是等概率的代价。两列用的是同一个源,所以那道差距是你自己看见的,不是听来的;字数则说明重抽到底花掉了什么。

不重复的抽取有两条路,按规模挑一条。池子小,就整个摊在内存里做一次部分洗牌(Fisher-Yates 提前收手),这样按构造就不可能有结果重复。池子大,就带着一组"已经被占"的值去采样,撞上重复就丢掉而不计数。两条路都让每一种组合同样可能;表格下面的说明会写清楚这次走的是哪一条。

可复跑的 seed 模式,以及它不适合做什么

勾上 seed 模式并输入一个种子,相同的种子配上相同的输入就会返回同一次抽签。它是用来核对结果、事后讲道理、或者复现一份 bug 报告的——一条从你输入的文本推导出来的确定性流,用之前先预热。

它明确不是用来在参赛者之间做公平选择的,因为种子是公开的,而一个公开的种子,是某个有心的参与者搜得动的东西:几千次尝试就能找到一个让他的名字排在最前的种子。只要有人能从"结果被怎么选"里获益,就用 crypto 源,并把记录留下来。

把限制说清楚

一次最多五百个抽取,名单最多两千个条目,小数位最多到九位,池子最大到 2^53 个结果——越过那条线整数就不再精确,采样这步会直接拒绝,而不是安静地采样一个比自己声称的更小的池。名单里的空行会被丢掉并且计数,重复的名字留作各自独立的条目,因为写法是一个名单唯一认得出的身份;分组没法平均时,表格下面的说明会写清有几组多出一人,以及是洗牌而不是名字决定了哪几组。

没有任何东西会被上传:名单、种子和结果都由你自己浏览器里的脚本处理,不发给任何人,关掉标签页就没了。输入的内容不会离开这台设备,离线、直接从磁盘打开也照样能抽。

微信
微信
支付宝
支付宝