使用此 Python 脚本在 Base94 中编码二进制

二进制数据到文本的可逆编码,字母表介于 2 到 94 个符号之间。
98 位读者喜欢这个。
Binary code on a computer screen

公共领域,通过 LibreShot

人类以多种不同的方式传递信息。在互联网上,主要格式是文本,也就是您正在阅读这篇文章的方式。但是,互联网上还有其他数据,例如图像和声音文件等等。在线发布图像或将文档附加到电子邮件似乎很容易,直到您意识到 HTTP/1.1 和 SMTP 是基于文本的协议。通过此类协议传输的数据必须表示为 ASCII 文本的子集(具体而言,字符 33 到 126)。

数字图像已经由显示它的计算机编码为二进制数据。换句话说,数字图像不像印在纸上的物理照片:它是由计算机速记的集合,由您用于查看它的任何图像查看器解码,无论该图像查看器是 Web 浏览器、照片编辑应用程序还是任何可以显示图像的软件。

为了将图像重新编码为 ASCII,通常使用 Base64,这是一种二进制到文本编码规则系统,可以将二进制数据表示为 ASCII 字符串。这是一个以 webp 格式保存的单个黑色像素,采用 Base64 编码

UklGRiYAAABXRUJQVlA4IBoAAAAwAQCdASoBAAEAAIAOJaQAA3AA/v9gGAAAAA==

本文是关于二进制/文本转换器、它们最流行的实现以及使用可变字母表的非标准方法。这是一个理论原型,旨在作为纯粹的学术练习,因为时间和空间复杂度使其仅适用于小型文件(最多几十千字节)。但是,此实现允许您选择任何基数,而不依赖于二的幂(例如七或 77)。

转换二进制文件

这种转换器的主要目的是将二进制文件放入适用于通过具有有限支持符号范围的通道发送的形式。一个很好的例子是任何基于文本的网络协议,其中所有传输的二进制数据都必须可逆地转换为纯文本形式,并且数据中不包含控制符号。ASCII 代码 0 到 31 被认为是控制字符,当通过任何不允许端点传输具有代码 0 到 255 的完整八位字节(二进制)的逻辑通道传输时,它们会丢失。

现在用于此目的的标准解决方案是 Base64 算法,如上所示并在 IETF 的 RFC 4648 中定义。此 RFC 还描述了 Base32 和 Base16 作为可能的变体。这里的关键点是它们都具有相同的特征:它们都是二的幂。支持的符号(代码)范围越广,转换结果的空间效率就越高。它会更大,但问题是会大多少。例如,Base64 编码使输出大约大 33%,因为三个输入(八位值位)字节被转换为四个输出(六位值位,26=64)字节。因此,比率始终为 4/3;也就是说,输出大 1/3 或 33.(3)%。实际上,Base32 非常低效,因为它意味着将五个输入(八位值位)字节转换为八个输出(五位值位,25=32)字节,比率为 8/5;也就是说,输出大 3/5 或 60%。在这种情况下,很难考虑 Base16 的任何效率,因为它的输出大小大 100%(每个具有八位值位的字节由两个四位值位字节表示,也称为半字节,24=16)。这甚至不是翻译,而只是八位字节的十六进制视图表示。

这些输入和输出字节比率是使用最小公倍数 (LCM) 为 Base64/32/16 编码计算的。您可以计算它,为此,您还需要一个函数:最大公约数 (GCD)。

  1. Base64(输入:八位,输出:六位)
    • LCM(8, 6) = 8*6/GCD(8,6) = 24 位
    • 输入:24/8 = 3 字节
    • 输出:24/6 = 4 字节
    • 比率(输出/输入):4/3
  2. Base32(输入:八位,输出:五位)
    • LCM(8, 5) = 8*5/GCD(8,5) = 40 位
    • 输入:40/8 = 5 字节
    • 输出:40/5 = 8 字节
    • 比率(输出/输入):8/5
  3. Base16(输入:八位,输出:四位)
    • LCM(8, 4) = 8*4/GCD(8,4) = 8 位
    • 输入:8/8 = 1 字节
    • 输出:8/4 = 2 字节
    • 比率(输出/输入):2/1

有限字符集

如果通道只能传输少量(如九个或 17 个)不同的符号怎么办?也就是说,如果您的文件由 256 符号字母表(正常的八位字节)表示,那么您不受编码器或解码器的计算能力或内存限制。但是,如果您只能发送七个不同的符号而不是 256 个怎么办?Base64、32 和 16 在这种情况下不适用。如果您只有七个符号可用于编码,那么 Base7 是唯一可能的输出格式。

或者,如果通道关注传输的数据量怎么办?无论传输什么,Base64 始终将数据增加 33%。例如,Base94 仅将输出增加 22%。

似乎 Base94 不是极限。如果前 32 个 ASCII 代码是控制字符,并且总共有 256 个代码,那么是什么阻止您使用 256-32=224 个符号的字母表?原因:并非所有这 224 个 ASCII 代码都具有可打印字符。一般来说,只有七位(0 到 127)是标准化的,其余的(128 到 255)用于各种语言环境,例如 Koi8-R、Windows-1251 等等。这意味着在标准化范围内只有 128-32=96 可用。此外,ASCII 代码 32 是空格字符,127 也没有可见字符。因此,96-2 得到 94 个可打印字符,每个字符与其代码在所有可能的机器上都具有相同的关联。

使用这种编码方法,您可以使用最基本的符号实际传输任何数据。您甚至可以完全字面上地仅使用烟雾信号发送照片!

使用此 Python 脚本

这个解决方案非常简单,但这种简单性包含了一个重要的计算约束。整个输入文件可以被视为一个以 256 为基数的大数字。它可能是一个非常大的数字,需要数千位。然后您只需要将这个大数字转换为不同的基数即可。就这么简单。

Python 3 使其更加简单!通常,不同基数之间的转换是通过中间 Base10 完成的。好消息是 Python 3 内置了对大数字计算的支持(它与 Int 集成),并且 Int 类有一个方法可以读取任意数量的字节,并使用所需的字节序自动将它们表示为大的 Base10 数字。因此,所有这些复杂性都可以在短短两行代码中实现,这真是太棒了!

with open('input_file', 'rb') as f:
    in_data = int.from_bytes(f.read(), 'big')

在这里,变量 in_data 是以 Base10 为基数的大数字。大部分计算发生并且大部分时间都消耗在这两行代码中。从这个初始编码开始,您可以将数据转换为任何其他基数,就像通常使用正常的小十进制数一样。

这是一个示例脚本,我会在 我的 GitHub 存储库 中保持更新。

#!/usr/bin/env python3

from sys import argv
from math import ceil

base = 42 
abc = '''!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'''

def to_base(fn_src, fn_dst, base=base):
    out_data = []

    # represent a file as a big decimal number
    with open(fn_src, 'rb') as f:
        in_data = int.from_bytes(f.read(), 'big')
    
    # convert a big decimal number to a baseN
    d, r = in_data % base, in_data // base
    out_data.append(abc[d])
    while r: 
        d, r = r % base, r // base
        out_data.append(abc[d])

    # write a result as a string to a file
    with open(fn_dst, 'wb') as f:
        f.write(''.join(out_data).encode())

def from_base(fn_src, fn_dst, base=base):
    out_data = 0

    # read one long string at once to memory
    with open(fn_src, 'rb') as f:
        in_data = f.read().decode()

    # convert a big baseN number to decimal
    for i, ch in enumerate(in_data):
        out_data = abc.index(ch)*(base**i) + out_data

    # write a big decimal number to a file as a sequence of bytes
    with open(fn_dst, 'wb') as f:
        f.write(out_data.to_bytes(ceil(out_data.bit_length()/8), 'big'))

def usage():
    print(f'usage: {argv[0]} <-e|-d> src dst [base={base}]')
    raise SystemExit(1)

def main():
    if len(argv) == 5:
        base = int(argv[4])
    elif len(argv) < 4:
        usage()

    if argv[1] not in ('-e', '-d'):
        usage()
    elif argv[1] == '-e':
        to_base(argv[2], argv[3], base)
    elif argv[1] == '-d':
        from_base(argv[2], argv[3], base)
    else:
        usage()

if __name__ == '__main__':
    main()

这是 Oleksii Tsvietnov 博客上 Base94 的更长版本,并经作者许可在此处发布。

接下来阅读什么

1 条评论

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.