科普

Base85 编码详解:原理、常见变体与实际应用

全面解析 Base85 编码的工作原理、Ascii85 与 Z85 等主流变体的异同,以及在 PDF、PostScript、Git 等场景中的实际应用。包含在线 Base85 编解码工具推荐。

在众多数据编码方案中,Base64 无疑是最广为人知的。但如果你追求更高的编码效率,Base85 或许是更好的选择。它能将 4 字节的二进制数据编码为 5 个 ASCII 字符,相比 Base64 的 4:3 比例(3 字节变 4 字符),空间利用率提升了约 7%。本文将带你全面了解 Base85 的原理、变体,以及它在现实世界中的应用。

如果你需要快速进行 Base85 编码或解码,可以使用我们的 Base85 在线编解码工具

1. 什么是 Base85?

Base85 是一种使用 85 个可打印 ASCII 字符来表示二进制数据的编码方法。它的核心思想非常直观:

4 个字节的二进制数据 → 5 个可打印字符

为什么是 85?因为 85⁵ = 4,437,053,125,略大于 2³² = 4,294,967,296(即 4 字节所能表示的最大值)。这意味着 5 个 Base85 字符恰好可以覆盖 4 字节数据的所有可能取值,实现无损编码。

相比之下,Base64 需要 4 个字符才能编码 3 个字节数据,编码膨胀率为 33%,而 Base85 的膨胀率仅为 25%。

编码效率对比

编码方式原始数据编码后膨胀率
十六进制1 字节2 字符100%
Base325 字节8 字符60%
Base643 字节4 字符33%
Base854 字节5 字符25%

2. Base85 的工作原理

2.1 编码过程

  1. 分组:将输入数据按每 4 个字节为一组进行分割。如果最后一组不足 4 字节,则用零字节填充。
  2. 转为整数:将每组 4 字节视为一个 32 位的大端序无符号整数。
  3. 进制转换:将该整数反复除以 85,得到 5 个余数(从低位到高位)。
  4. 字符映射:将每个余数加上偏移量(取决于具体变体),映射为一个可打印的 ASCII 字符。
  5. 尾部处理:如果最后一组进行了填充,则丢弃编码结果中多余的字符。

2.2 编码示例

假设我们要编码字符串 "Man "(4 个字节):

步骤 1:获取字节值

'M' = 77,  'a' = 97,  'n' = 110,  ' ' = 32

步骤 2:转为 32 位整数

77 × 256³ + 97 × 256² + 110 × 256 + 32 = 1,298,230,816

步骤 3:反复除以 85

1298230816 ÷ 85 = 15273303 余 61
  15273303 ÷ 85 =   179686 余 3
    179686 ÷ 85 =     2114 余 21
      2114 ÷ 85 =       24 余 64
        24 ÷ 85 =        0 余 24

步骤 4:映射为字符(Ascii85 变体,偏移量 33)

余数序列(高位到低位):24, 64, 21, 3, 61
加上偏移量 33:         57, 97, 54, 36, 94
对应 ASCII 字符:       9,  a,  6,  $,  ^

最终编码结果:9a6$^

2.3 解码过程

解码是编码的逆过程:

  1. 将编码字符串每 5 个字符为一组。
  2. 将每个字符减去偏移量,得到对应的数值。
  3. 将 5 个数值视为 85 进制数,转换为 32 位整数。
  4. 将该整数拆分为 4 个字节。
  5. 如果是最后一组且有填充,则丢弃对应的多余字节。

3. Base85 的主要变体

Base85 并非一个单一的标准,而是一个编码方案的家族。不同的变体使用不同的字符集、填充规则和分隔符。

3.1 Ascii85(btoa 格式)

Ascii85 是最经典的 Base85 变体,由 Paul E. Rutter 在 1984 年为 btoa 工具设计,后被 Adobe 采纳用于 PostScript 和 PDF。

字符集:使用 ASCII 码 33(!)到 117(u)的 85 个连续字符:

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstu

特殊规则

  • 全零压缩:如果一组 4 字节全为零(0x00000000),则用单个字符 z 表示,而非 !!!!!
  • 定界符:Adobe 版本使用 <~~> 作为编码数据的起始和结束标记。
  • 空白字符:编码数据中可以插入空格、制表符和换行符,解码时将被忽略。

示例

原始文本:Man is distinguished
Ascii85:  <~9jqo^BlbD-BleB1DJ+*+F(f,q~>

3.2 Z85(ZeroMQ Base85)

Z85 是由 ZeroMQ 社区设计的 Base85 变体,专门优化为适合在编程语言的字符串字面量和 XML/JSON 等格式中使用。

字符集(精心挑选的 85 个字符):

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#

设计特点

  • 避免使用单引号 '、双引号 " 和反斜杠 \,因此可以直接嵌入大多数编程语言的字符串中
  • 避免使用反引号 ` 和逗号 ,,在更多上下文中安全使用
  • 不使用定界符
  • 不支持全零压缩
  • 要求输入长度必须是 4 的倍数

3.3 RFC 1924 Base85(IPv6 编码)

RFC 1924 提出了一种使用 Base85 编码 IPv6 地址的方案。虽然该 RFC 最初是一个愚人节提案,但其定义的字符集在一些严肃项目中得到了实际应用。

字符集

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~

特点

  • 将 128 位的 IPv6 地址编码为 20 个字符(而标准表示法最长需要 39 个字符)
  • 字符集以数字和字母开头,排列更直观

3.4 变体对比

特性Ascii85Z85RFC 1924
设计时间198420101996
起始字符码33 (!)混合混合
定界符<~~>
全零压缩是(z
字符串安全否(含 " \
尾部填充灵活严格(4 的倍数)N/A
主要用途PDF / PostScriptZeroMQ / 网络IPv6 地址

4. 实际应用场景

4.1 PDF 和 PostScript

这是 Base85(Ascii85)最重要的应用领域。在 PDF 文件中,二进制数据流(如嵌入的图片、字体等)可以使用 Ascii85 编码:

stream
<~9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKF<GL>Cj@.4Gp$d7F!,L7@<6@)/0JDEF<G%<+EV:2F!,
O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF"AGXBPCsi+DGm>@3BB/F*&OCAfu2/AKY
i(DIb:@FD,*)+C]U=@3BN#EcYf8ATD3s@q?d$AftVqCh[NqF<G:8+EV:.+Cf>-FD5W8ARlolDIa
l(DId<j@<?3r@:F%a+D58'ATD4$Bl@l3De:,-DJs`8ARoFb/0JMK@qB4^F!,R<AKZ&-DfTqBG%G
>uD.RTpAKYo'+CT/5+Cei#DII?(E,9)oF*2M7/c~>
endstream

相比十六进制编码,Ascii85 可以将文件大小减少约 25%。

4.2 Git 二进制补丁

Git 在生成二进制文件的补丁时,使用 Base85 编码来表示二进制差异数据:

diff --git a/image.png b/image.png
GIT binary patch
literal 1234
zcmV;@1TFkJiwFP!0000...(Base85 编码的数据)

Git 使用的是一种自定义的 Base85 变体,字符集为:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~

4.3 ZeroMQ 消息传递

ZeroMQ 使用 Z85 编码来传递二进制数据(如 CURVE 加密密钥),使其可以安全地嵌入到配置文件和日志中:

# CURVE 公钥(32 字节 → 40 个 Z85 字符)
Yne@$w-vo<fVvi]a<NY6T1ed:M$fCG*[IaLV{hID

4.4 数据序列化

一些数据序列化库使用 Base85 作为编码二进制数据的选择,因为它比 Base64 更紧凑:

Python 标准库中就原生支持 Base85:

import base64

data = b"Hello, World!"
encoded = base64.b85encode(data)
print(encoded)  # b'NM&qnZ*Oe1c^gn'

decoded = base64.b85decode(encoded)
print(decoded)  # b'Hello, World!'

5. 编程实现示例

5.1 JavaScript(Ascii85)

// Ascii85 编码
function ascii85Encode(data) {
  const bytes = typeof data === 'string'
    ? new TextEncoder().encode(data)
    : new Uint8Array(data);

  let result = '<~';
  const padding = (4 - (bytes.length % 4)) % 4;
  const padded = new Uint8Array(bytes.length + padding);
  padded.set(bytes);

  for (let i = 0; i < padded.length; i += 4) {
    // 4 字节 → 32 位整数
    const value = (padded[i] << 24) | (padded[i+1] << 16)
                | (padded[i+2] << 8) | padded[i+3];

    if (value === 0 && i + 4 <= bytes.length) {
      result += 'z';  // 全零压缩
      continue;
    }

    // 整数 → 5 个 Base85 字符
    const chars = [];
    let v = value >>> 0;
    for (let j = 4; j >= 0; j--) {
      chars[j] = String.fromCharCode((v % 85) + 33);
      v = Math.floor(v / 85);
    }

    // 最后一组可能只需部分字符
    const charsNeeded = (i + 4 > bytes.length) ? (bytes.length - i + 1) : 5;
    result += chars.slice(0, charsNeeded).join('');
  }

  result += '~>';
  return result;
}

// Ascii85 解码
function ascii85Decode(str) {
  // 去除定界符
  str = str.replace(/^<~/, '').replace(/~>$/, '');
  str = str.replace(/\s/g, ''); // 忽略空白字符

  const bytes = [];

  for (let i = 0; i < str.length;) {
    if (str[i] === 'z') {
      bytes.push(0, 0, 0, 0);  // 全零展开
      i++;
      continue;
    }

    const group = str.slice(i, i + 5);
    const padLen = 5 - group.length;
    const padded = group + 'u'.repeat(padLen); // 用 'u'(84) 填充

    // 5 个字符 → 32 位整数
    let value = 0;
    for (const ch of padded) {
      value = value * 85 + (ch.charCodeAt(0) - 33);
    }

    // 32 位整数 → 4 字节
    const groupBytes = [
      (value >>> 24) & 0xFF,
      (value >>> 16) & 0xFF,
      (value >>> 8)  & 0xFF,
      value & 0xFF
    ];

    // 根据实际字符数保留对应字节
    for (let j = 0; j < 4 - padLen; j++) {
      bytes.push(groupBytes[j]);
    }

    i += group.length;
  }

  return new Uint8Array(bytes);
}

// 使用示例
const encoded = ascii85Encode('Hello, World!');
console.log(encoded);
// <~87cURD]j7BEbo80~>

const decoded = new TextDecoder().decode(ascii85Decode(encoded));
console.log(decoded);
// Hello, World!

5.2 Python

Python 标准库 base64 模块内置了对 Base85 和 Ascii85 的支持:

import base64

# ===== Ascii85 =====
data = b"Hello, World!"

# 编码
a85_encoded = base64.a85encode(data, adobe=True)
print(a85_encoded)   # b'<~87cURD]j7BEbo80~>'

# 解码
a85_decoded = base64.a85decode(a85_encoded, adobe=True)
print(a85_decoded)   # b'Hello, World!'

# ===== RFC 1924 / Base85 =====
b85_encoded = base64.b85encode(data)
print(b85_encoded)   # b'NM&qnZ*Oe1c^gn'

b85_decoded = base64.b85decode(b85_encoded)
print(b85_decoded)   # b'Hello, World!'


# ===== 自定义 Ascii85 编码实现 =====
def ascii85_encode(data: bytes) -> str:
    """将字节数据编码为 Ascii85 字符串"""
    result = []
    padding = (4 - len(data) % 4) % 4
    data_padded = data + b'\x00' * padding

    for i in range(0, len(data_padded), 4):
        # 4 字节 → 32 位整数
        value = int.from_bytes(data_padded[i:i+4], 'big')

        if value == 0 and i + 4 <= len(data):
            result.append('z')
            continue

        # 整数 → 5 个 Base85 字符
        chars = []
        for _ in range(5):
            chars.append(chr(value % 85 + 33))
            value //= 85
        chars.reverse()

        # 最后一组只保留需要的字符
        if i + 4 > len(data):
            chars = chars[:len(data) - i + 1]

        result.extend(chars)

    return '<~' + ''.join(result) + '~>'


# 使用自定义实现
print(ascii85_encode(b"Hello, World!"))
# <~87cURD]j7BEbo80~>

6. Base85 与 Base64 的选择

6.1 何时选择 Base85?

场景推荐原因
PDF / PostScript 流Base85行业标准,节省文件大小
Git 二进制补丁Base85Git 原生使用
ZeroMQ 密钥Z85字符串安全,无需转义
带宽敏感场景Base85编码膨胀率更低(25% vs 33%)

6.2 何时选择 Base64?

场景推荐原因
Web API / JSONBase64广泛支持,兼容性最好
电子邮件 / MIMEBase64MIME 标准
数据 URIBase64浏览器原生支持
通用场景Base64工具和库支持最广泛

6.3 效率实测

对于不同大小的数据,编码后的大小对比:

原始数据大小十六进制Base64Base85节省(vs Base64)
100 字节200 字符136 字符125 字符约 8%
1 KB2,048 字符1,368 字符1,280 字符约 6%
10 KB20,480 字符13,684 字符12,800 字符约 6%
1 MB~2 MB~1.33 MB~1.25 MB约 6%

7. Base85 的局限性

尽管 Base85 有更高的编码效率,但它也存在一些局限性:

7.1 字符集问题

Ascii85 使用的字符集包含大量特殊字符(如 ", \, <, > 等),这在某些上下文中会造成问题:

  • JSON:需要对 "\ 进行转义
  • XML:需要对 <, >, & 进行实体编码
  • URL:大部分字符需要百分号编码
  • Shell:许多字符需要转义

Z85 通过精心挑选字符集部分解决了这个问题,但仍不如 Base64url 那样通用。

7.2 库支持不够广泛

与 Base64 相比,Base85 的原生库支持较少:

  • 浏览器:没有原生的 btoa/atob 等效函数
  • 多数语言:需要第三方库或手动实现
  • Python:标准库支持(base64.a85encode/base64.b85encode
  • Go:标准库支持(encoding/ascii85

7.3 不同变体之间不兼容

Ascii85、Z85、RFC 1924 等变体使用不同的字符集和规则,它们之间不能互相转换。在使用时必须明确指定所使用的变体。

8. 常见问题

Base85 和 Ascii85 是同一个东西吗?

严格来说,Ascii85 是 Base85 的一个特定变体。“Base85”是一个更广泛的概念,涵盖所有使用 85 个字符进行编码的方案。但在日常使用中,两者经常被混用。

为什么不使用 Base128 来获得更高的效率?

虽然理论上可以使用更大的字符集,但 ASCII 中只有 95 个可打印字符(码位 32-126)。Base85 已经使用了其中的绝大部分。Base128 需要使用不可打印的控制字符,这会引发严重的兼容性问题。

Base85 编码是否安全?

Base85 是一种编码方式,不是加密方式。它不提供任何安全保护。编码后的数据可以被任何人轻松解码。如果需要保护数据安全,应该先加密,再编码。

如何判断一段数据使用的是哪种 Base85 变体?

  • 如果数据以 <~ 开头并以 ~> 结尾,则是 Ascii85(Adobe 格式)
  • 如果数据只包含 Z85 字符集中的字符(无特殊标点),可能是 Z85
  • 其他情况需要根据上下文判断

9. 总结

Base85 是一个在特定领域表现出色的编码方案家族。它以 25% 的编码膨胀率在所有常见 Base 编码中实现了最优的空间效率。

编码方式膨胀率适用场景
十六进制100%调试、底层数据查看
Base3260%大小写不敏感场景、密钥交换
Base6433%Web API、电子邮件、通用场景
Base8525%PDF、Git、ZeroMQ、带宽敏感场景

如果你正在处理 PDF 文件、Git 补丁或 ZeroMQ 消息,Base85 是一个值得了解和掌握的工具。

想要亲自体验 Base85 编码和解码?试试我们的 Base85 在线编解码工具,支持 Ascii85 和 Z85 等多种变体的快速转换。