Base64 与 Hex、Base32、ASCII85 对比
2026-09-30 · 1312 词
Base64 是把字节变成文本的默认选择,但它不是唯一选择,也不总是正确选择。十六进制更好读、其他方面更差;Base32 更大,但能在摧毁大小写的环境里存活;ASCII85 更小,而且会出现在你意想不到的地方。每一种都源于某个具体约束,选错往往不是谁有意的决定。
四种放在一起
- Hex —— 字母表
0-9a-f,每 3 字节 6 个字符,开销 +100%,不区分大小写 - Base32 —— 字母表
A-Z2-7,每 5 字节 8 个字符,开销 +60%,不区分大小写(惯例用大写) - Base64 —— 字母表
A-Za-z0-9+/,每 3 字节 4 个字符,开销 +33%,区分大小写 - ASCII85 —— 字母表
!到u,每 4 字节 5 个字符,开销 +25%,区分大小写
开销这一列是算术,不是偏好。Hex 每个字节花掉两个字符,所以数据翻倍。Base32 每字符装 5 位,装下 5 字节的 40 位需要 8 个字符 —— 60% 开销。Base64 把 6 位装进 4 个字符,覆盖 3 字节。ASCII85 把 4 字节装进 5 个字符,是四者中效率最高的;PostScript 和 PDF 用它来嵌入流数据。
Hex:给人看,也给哈希用
Hex 是这里效率最低的编码,但当数据要给人看时它最有用。每个字节对应固定的一对字符,映射可以记住,偏移也一眼可读:文件的第 4 个字节就是 hex dump 里的第 8、9 个字符,不需要算。
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
正因为这个性质,哈希、校验和、MAC 地址、UUID 和文件签名都写成 hex。当你在文件开头读到 89 50 4E 47,你读到的就是 PNG 签名,且与格式规范里写的形式一致 —— 文档和你的 hex dump 之间不需要任何翻译步骤。
需要知道的失败形态:hex 不是 Base64,而解码器在你搞混时很少抱怨。0-9a-f 是两套字母表的子集,所以 Buffer.from(hexString, 'base64') 会无报错地返回字节 —— 只是不是你想要的那些字节。这是少数"解码成功但答案就是错的"情形之一,所以当一段载荷"能解但说不通"时,值得先检查这一点。
Base32:当大小写不可信时
Base32 用 32 个大写字母和数字,它为那些大小写会被破坏或容易混淆的环境而生:DNS 名、某些条码与二维码流程、需要念出来的序列号,以及文档里声明不区分大小写的系统。60% 的开销就是消除歧义的价钱。
import base64
print(base64.b32encode(b'hello').decode()) # NBSWY3DP
它的字母表还排除了 0、1、8、9,扩展变体里还排除 I、L、O 和 U,从而去掉了人们会看错的字符对。如果必须有人手工转录一个 token,这比多出的那 27 个百分点重要得多。如果是机器在传输,base32 是严格劣于 Base64 的选择。
Base64:默认选择,两套字母表
Base64 之所以占主导,是因为它命中了当时可得的最佳折中:保持每个字符可打印的前提下,每字符 6 位已经是上限;而 64 是能塞进可打印 ASCII 范围的最大 2 的幂。它的两个问题 —— / 和 + 与 URL 语法冲突、以及区分大小写 —— 分别由 URL 安全变体和"了解你的传输通道"来解决。
实践中 Base64 的大部分痛苦来自字母表本身:把 I 抄成 l、把 0 抄成 O,得到的字符串会毫无怨言地解码成错误的字节。这就是为什么实现应当在拒收输入时报出字符位置与码点,也是为什么在两个符号长得像时,"检查码点"比"检查字符"是更好的建议。
ASCII85:更小,而且大多隐形
ASCII85 用 85 个符号(! 到 u)把 4 字节编成 5 个字符,开销 25%,而不是 Base64 的 33%。省下来的是真的,但幅度不大,代价是格式宽容度差得多:没有通常意义上的填充机制、对长串零字节有特殊规则、字母表里还包含在 PostScript 和 XML 中需要转义的字符。
你更可能在 PDF 或 PostScript 文件里遇到它,而不是主动选它。如果确实需要解码,Python 内置了 base64.a85decode;另外值得知道的是 PDF 还定义了 ASCIIHexDecode —— 一张 PDF 的流过滤器列表,就是这四种编码里有三种同时出现的地方。
怎么选,一段话说完
Hex 用在人或者规范要读这些字节的场合,以及定宽可读性比体积更重要的时候:哈希、签名、二进制协议 dump、颜色值。Base64 用在其他一切穿越文本的场景 —— JSON、XML、邮件、HTTP 体、data URI、内联资源 —— 并在字符串会出现在 URL、文件名或 Cookie 里时切到它的 URL 安全字母表。Base32 只在大小写或人工转录成为硬约束时才用。ASCII85 在你实现一个已经在用它的格式时用,此外不要。
它们之间的转换
四种都是无损往返,所以转换是安全的 —— 但每一次转换都是一个藏错的地方,因为这些编码产出的字符串在另一种字母表下往往也"看起来合理"。
const toHex = (bytes) => Buffer.from(bytes).toString('hex');
const toB64 = (bytes) => Buffer.from(bytes).toString('base64');
const toB64Url = (bytes) => Buffer.from(bytes).toString('base64url');
const fromHex = (s) => Buffer.from(s, 'hex');
const fromB64 = (s) => Buffer.from(s, 'base64');
// 把 hex 字符串当 base64 解码:不报错,字节是错的
fromB64(toHex(Buffer.from('hello'))).toString('utf8') // 不是 'hello'
最安全的习惯是转换字节,而不是转换字符串:先解成字节,再从那些字节重新编码。直接把 hex 字符串用字符串操作转成 Base64 是可行的,但它默默假定了两种编码都在规范形态 —— 没有空白、没有换行、填充正确 —— 而这个假定恰好在"上游做了点不寻常的事"时失效。
如果你想在选解码器之前先看清一段载荷到底是什么,答案通常来自最开始的几个字节而不是字母表:一个看起来像 hex 的字符串按 hex 解码后以 89 50 开头,那就是 PNG,其余格式见 图片文件签名。而如果目标只是看到图片,Base64 转图片 会按顺序尝试各种合理解读,并告诉你哪种生效了。
还有一个对比值得记在心里,因为它决定了体积有多重要:Base64 和 ASCII85 用于把任意字节搬过文本通道,hex 用于阅读字节,Base32 用于转录字节。只有前两者与效率有关,而它们之间那 8 个百分点的差距(33% 对 25%)很少是决定因素 —— 周围那个格式通常才是。如果你在与体积较劲而不是与编码较劲,更有用的问题是"这些数据该不该是文本";为什么 Base64 会让文件大 33% 讲的就是内联从哪一点开始不再划算。