Base91 编码/解码
工作原理:
输入先变成字节,再打包成 13 比特一组、每组变成两个字符;在短形式会有歧义之处则改用一个 14 比特组。
输入是:
工作原理:
空白字符会被忽略,每个字符都在本页下方印出的字母表中查找。未知字符会连同其位置一并报告。
输出为:
在输入框中按 Ctrl+Enter 即执行当前模式。
请选择编码或解码并点击其操作按钮。自测会运行 130 次编码/解码往返并在此报告数量。
字母表,全文印出: Base91 背后没有标准化组织,字母表在不同实现之间不一定相同。本页使用参考实现的字母表,下列所有值都是用表中列出的恰好 91 个字符产生的。
绿色格是可以安全出现在 HTML 文本或 URL 查询中的字符;橙色格必须转义或做百分号编码(见表下方的说明)。
绿色单元格是可以安全放在 HTML 文本或 URL 查询串中的字符;橙色单元格必须转义或百分号编码(见表下方的说明)。
什么是 Base91?
Base91 是一种只用可打印 ASCII 字符书写任意字节的方式,干的活和 Base64 一样,但每个字符塞进了更多比特。它不把输入切成 6 比特组、每组印一个字符,而是不断收集比特,直到凑够至少 13 个,然后印出两个字符,共同表示这 13(或 14)比特的值。
91 个字符的字母表中取两个字符能命名 91 × 91 = 8281 个不同的值,而 213 = 8192,所以两个字符总是装得下 13 比特 — 还剩 89 个值富余。这就是全部诀窍:每 2 字符 13 比特即每字符 6.5 比特,对比 Base64 的 6,换来约 23% 而非 33% 的开销。
30 秒版本:test 变成 fPNKd,Hello, World! 变成 >OwJh>}AQ;r@@Y?F。解码是精确的逆过程,且需要同一张 91 字符字母表 — 换一张字母表,同样的字节会得到不同的输出。
13 和 14 比特从哪里来
91 × 91 = 8281 个值可由一对字符命名,多于 213 = 8192 而少于 214 = 16384。所以一对字符总能承载 13 比特,有时能承载 14 比特。编码器先试 13 比特:如果 13 比特的值大于 88,就直接印出该值;否则再取第 14 个比特,改印得到的 14 比特值。
88 这个边界并非任意选取。两个字符能命名的最大值是 90 + 91 × 90 = 8280,而 8192 + 89 = 8281 已经超过它。所以任何 14 比特组的低 13 比特窗口都不可能达到 89 或更高,于是解码器仅凭值本身就能把两种情形区分开:
| 解码器看到的 | 消耗的比特 | 原因 |
| value & 8191 在 89 到 8191 | 13 比特 | 89 及以上的值只能来自 13 比特分支,因为低 13 比特窗口那么高的 14 比特组至少是 8281 — 没有一对字符能命名它。 |
| value & 8191 在 0 到 88 | 14 比特 | 编码器只会在 13 比特读数不超过 88 时才退回 14 比特,所以这个分支就是那条退路,别无其他。 |
结尾剩下的东西 — 1 到 13 比特不等 — 印成一个字符,剩余值确实超过 7 比特时再加印第二个字符。这就是编码长度按两个字符步进、有时停在奇数的原因。
字母表,以及它舍弃的三个字符
可打印 ASCII 共 94 个字符,从 ! (0x21) 到 ~ (0x7E)。Base91 用了其中 91 个,舍弃的是连字符 -、撇号 ' 和反斜杠 \。字母、数字和几乎所有标点都在字母表中恰好出现一次,顺序如上表所示;第 92 到 94 个可打印字符只是无法被寻址。
因为字母表是固定顺序的,值对应的字符靠查表而非公式。两个按不同顺序排列字母表的实现都自称“Base91”,却读不了彼此的输出 — 这是最常见的困惑来源。本页印出自己的字母表,任何输出永远都可以据此追溯。
Base91 与其他可打印编码的对比
| 编码 | 每字符比特数 | 相对输入的大小 | 备注 |
| Base16(hex) | 4 | 200% | 每字节两个字符,直读无碍,无需查表。 |
| Base32 | 5 | 160% | 大小写不敏感、适合人口述,但它是这组里最浪费的。 |
| Base58 | ~5.86 | ~137% | 为地址而生、不为密度:它避开了易混淆的字符。 |
| Base64 | 6 | 133% | 无处不在的默认选择:每 3 字节 4 个字符,最后一个字符浪费掉 6 个未用比特。 |
| Ascii85 / Base85 | 6.4 | 125% | 每 4 字节 5 个字符,无需查表,但它要除以 85 — 见 Ascii85 转换器。 |
| Base91 | ~6.5 | ~123% | 其中最密的:每 2 字符 13 比特,代价是时不时来一个 14 比特组。 |
百分比为各编码的渐近代价(四舍五入);一个真实 92 字节输入的实测字符数在本页上方的表格里。Base91 已接近可打印 ASCII 所能达到的极限:每字符 7 比特意味着要用 128 个不同的字符,而可打印区段提供不了这么多。
Base91 不是什么
- 不是标准。没有 RFC。参考实现是 Joachim Henke 以 BSD 3 条款许可证发布的 basE91 0.6.0;“Base91”一词也被用来指并非这张字母表的其他字母表。
- 不是加密。每个字符都是对比特的直接重排,任何拿到字母表的人都能一遍逆向。
- 不是 URL 安全。字母表里有
#、%、&、/、? 和 ",它们在 URL 中都会改变含义,所以 Base91 输出放进查询串之前必须做百分号编码。
- 不是 HTML 安全。它还包含
<、>、& 和 ",放进标记之前必须转义 — 可用 HTML 实体转换器。
- 不是定长。输出长度取决于比特模式,而不只是字节数,所以同样大小的两个输入可能相差一个字符。
真会咬人的陷阱
- 本页的字母表就是参考字母表。如果另一个工具对同样的输入给出不同输出,先比较字母表再断定哪边坏了。这 91 个字符必须以相同的顺序出现。
- 一个错字能改变整个尾部。组边界由之前的值决定,串前部的一处改动会使其后全部偏移,而尾部只用最后一组的剩余比特解码。
- 奇数输出长度是正常的。它不代表字符串被截断;最后一组的剩余值不超过 7 比特时就只印一个字符。
- 公开的示例可能带一个你没输入的换行。附带了行尾的 shell 命令,是著名 Base91 值比同样文本长一个字符的常见原因:
echo "Hello, World!" 交给编码器 14 字节、印出 >OwJh>}AQ;r@@Y?FF,而纯文本的 13 字节印出 >OwJh>}AQ;r@@Y?F。两者都对,只是字节数不同 — 输出框下方的计数会告诉你手里是哪一个。
- 把二进制数据当文本解码行不通。解码出的字节不是有效 UTF-8 时,就没有合理的文本可显示 — 这正是十六进制输出模式的用途。
快问快答
「为什么本页显示的值和另一个 Base91 工具相差一个字符?」
几乎总是字母表不同。第二常见的原因是某个工具只用 13 比特分支、从不用 14 比特退路;那个变体代码更短,但与这里使用的参考算法输出不同。
还有一个只靠数字节数就能了断的精确情形,它解释了流传最广的那个例子:末尾的换行符。大多数公开向量都是 shell 命令产出的,而 shell 会补上收尾的行结束符。Hello, World! 的 13 字节在这里印 16 个字符,而 Hello, World! 后跟换行符的 14 字节印出 17 字符的 >OwJh>}AQ;r@@Y?FF。把两个值分别粘进解码框:本页会报告字节数,两种读数可以分得清清楚楚,不用争论。示例按钮会根据你正在使用的读取方式载入两者中对应的一个,并跟随编码/解码开关和输入是/输出为选择器:文本读取方式得到 16 字符的值,十六进制读取方式得到带末尾 0A 字节一目了然的 17 字符值,编码器则得到要编码的文本而不是 Base91 字符串。
「Base91 比 Base64 小吗?」
是的,按编码后的大小约小 8%:每 2 字符 13 比特对比每字符 6 比特。在 92 字节文本上,Base64 印 124 个字符,Base91 印 114 个。
「我能用 Base64 解码器解这个吗,或者反过来?」
不能。两种字母表在字母和数字上重叠,所以 Base64 解码器会把 Base91 字符串当“有效”字符接受,然后默默产出乱码。两者除了方案的外形以外毫无一致之处。
「输出像 Base64 一样以填充结尾吗?」
不会。Base91 根本没有填充字符:结尾剩下的比特就作为最后一组印出,不需要添加或去除任何东西。
「我怎么知道本页的结果是对的?」
按自测 0 到 64 字节。它对 130 种字节模式做编码再解码,覆盖 13/14 比特分界发生变化的每个长度,并把回来的字节与送进去的字节逐一比对。示例按钮的向量还额外与本站测试工具中第二个独立实现交叉校验过。
标准与致谢
本页实现了 Joachim Henke 的 basE91 参考实现(0.6.0 版)的算法,该实现以 BSD 3 条款许可证发布,字母表印于上方。解码接受并忽略空白字符,与参考工具一致。
本页文字为本站撰写;算法并非从任何地方抄来。示例按钮的值是参考包自带的那一个示例(标注“参考示例”),加上本站测试工具中保存的第二个独立实现产出的值(标注“交叉校验”)。