JSON字符串中的二进制数据。比Base64更好的解决方案

770

JSON格式本身不支持二进制数据。必须对二进制数据进行转义,以便将其放入JSON中的字符串元素(即使用反斜杠转义的双引号中的零个或多个Unicode字符)。

显而易见的一种转义二进制数据的方法是使用Base64。但是,Base64具有高处理开销。此外,它将3个字节扩展为4个字符,导致数据大小增加约33%。

这种情况的一个用例是CDMI云存储API规范的v0.8草案。您可以使用JSON通过REST-Webservice创建数据对象,例如:

PUT /MyContainer/BinaryObject HTTP/1.1
Host: cloud.example.com
Accept: application/vnd.org.snia.cdmi.dataobject+json
Content-Type: application/vnd.org.snia.cdmi.dataobject+json
X-CDMI-Specification-Version: 1.0
{
    "mimetype" : "application/octet-stream",
    "metadata" : [ ],
    "value" :   "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
    IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg
    dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu
    dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
    ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=",
}

是否有更好的方法和标准方法将二进制数据编码为JSON字符串?


50
上传文件只需操作一次,所以这不是什么大问题。而下载文件时,你可能会惊讶地发现,在使用gzip压缩的情况下,base64编码的效果非常好。因此,如果你的服务器启用了gzip功能,那么也可能没有太大问题。 - cloudfeet
6
另外一个值得推荐的解决方案是 http://msgpack.org/,专为技术极客设计。详情请访问:https://github.com/msgpack/msgpack/blob/master/spec.md - nicolallias
5
@cloudfeet,每个用户每次操作只能使用一次。非常重要的一件事情。 - Pacerier
7
请注意,通常每个字符占用 2 个字节的内存。因此,Base64 在传输过程中可能会产生 +33%(4/3)的开销,但将该数据放入传输中、检索它并利用它需要增加 +166%(8/3)的开销。举例来说:如果一个JavaScript字符串的最大长度为100k个字符,则只能使用Base64表示37.5k字节的数据,而不能表示75k字节的数据。这些数字可能会成为应用程序的许多部分的瓶颈,例如 JSON.parse 等。 - Pacerier
10
“通常每个字符需要2个字节的内存”这个说法并不准确。例如v8引擎中有OneByte和TwoByte字符串类型。只有在必要的情况下才会使用TwoByte字符串类型,以避免过度的内存消耗。Base64可以使用OneByte字符串进行编码。 - ZachB
显示剩余4条评论
19个回答

560
根据JSON规范,如果您的JSON以UTF-8传输,则可以使用94个Unicode字符表示为一个字节。基于此,我认为在空间方面,最好的选择是base85,它将四个字节表示为五个字符。但是,这仅比base64提高了7%的空间效率,计算成本更高,并且实现不如base64常见,因此可能不是一个良好的选择。
您还可以将每个输入字节简单地映射到U+0000-U+00FF中对应的字符,然后进行JSON标准所需的最小编码来传递这些字符。这里的优点是,在内置函数之外,所需的解码为零,但空间效率很差——如果所有输入字节同等可能,则扩展105%(相对于25%的base85或33%的base64)。
最终结论:在我看来,base64胜出,因为它普遍易用,性能也足够好,没有必要被替换。
另请参阅:Base91Base122

7
仅使用实际字节编码引号字符怎么会导致扩展105%,而Base64只有33%?Base64不是133%吗? - jjxtra
19
Base91不太适用于JSON,因为其字母表中包含引号。在最糟糕的情况下(所有引号都被输出),经过JSON编码后,它的负载大小将增加245%。 - jarnoh
46
Python 3.4现在包含base64.b85encode()和b85decode()。简单的编码和解码时间测量显示,b85比b64慢13倍以上。因此,我们虽然可以减小7%的大小,但性能损失高达1300%。 - Pieter Ennes
3
@hobbs,JSON规定控制字符必须被转义。RFC20第5.2节定义DEL为一个控制字符。 - Tino
2
@hobbs 这是一个有趣的观点。现在的问题是,哪一个是正确的?json.org还是RFC7159?由于两个定义不同,有人应该研究一下并进行修复。目前我仍然坚持使用json.org、RFC20、维基百科和Python实现。 - Tino
显示剩余10条评论

329

我遇到了同样的问题,并想分享一个解决方案:multipart/form-data。

通过发送multipart表单,您首先以字符串方式发送JSON元数据,然后分别使用Content-Disposition名称对二进制原始数据(图像,wav等)进行索引。

这里有一个关于如何在obj-c中执行此操作的教程,这是一个博客文章,解释了如何使用表单边界划分字符串数据并将其与二进制数据分离。

唯一真正需要更改的是服务器端;您将不得不捕获应适当引用POST的二进制数据的元数据(通过使用Content-Disposition边界线)。

尽管服务器端需要额外的工作,但如果您需要发送许多图像或大图像,则值得这样做。如果需要,可以与gzip压缩结合使用。

在我看来,发送base64编码数据是一个hack; RFC multipart/form-data是为解决此类问题而创建的:将二进制数据与文本或元数据一起发送。


5
顺便提一下,Google Drive API 是这样做的:https://developers.google.com/drive/v2/reference/files/update#examples - Mathias Conradt
10
为什么这个答案使用本地特性而不是试图把一个圆形(二进制)钉子塞进一个方形(ASCII)洞里,但评分却如此之低? - Mark K Cowan
9
将数据以Base64编码发送是一种hack,multipart/form-data也是如此。即使你链接的博客文章也写道“使用Content-Type multipart/form-data表示您发送的实际上是一个表单。但它不是。”因此,我认为Base64 hack 不仅更容易实现,而且更可靠。我见过一些库(例如Python)硬编码了multipart/form-data内容类型。 - t3chb0t
8
@t3chb0t,“multipart/form-data”媒体类型最初是为传输表单数据而设计的,但如今它已经被广泛应用于HTTP/HTML之外的领域,尤其是编码电子邮件内容。现在RFC7578建议将其作为通用编码语法使用。 - lorenzo
5
可能是因为虽然这个回答对于问题的解决有帮助,但它并没有按照实际问题回答,实际问题是“低开销的二进制到文本编码在JSON中的使用”,而这个回答完全不涉及JSON。 - Chinoto Vokro
显示剩余2条评论

46
UTF-8的问题在于它不是最节省空间的编码方式。而且,一些随机二进制字节序列是无效的UTF-8编码,因此不能将随机二进制字节序列解释为某些UTF-8数据,因为它将是无效的UTF-8编码。这个约束对UTF-8编码的好处在于使其具有鲁棒性,并且可以定位多字节字符的起始和结束位置。
因此,如果编码范围在[0..127]之间的字节值只需要在UTF-8编码中使用一个字节,则编码范围在[128..255]之间的字节值将需要2个字节!比这更糟糕的是,在JSON中,控制字符,“和\都不允许出现在字符串中。因此,二进制数据需要进行一些转换才能正确编码。
假设我们的二进制数据中均匀分布的随机字节值,那么平均而言,一半的字节将以一个字节编码,另一半将以两个字节编码。UTF-8编码的二进制数据将具有150%的初始大小。
Base64编码仅增长到初始大小的133%。因此,Base64编码更有效率。
使用另一种基础编码呢?在UTF-8中,编码128个ASCII值是最节省空间的。在8位中,您可以存储7位。因此,如果我们将二进制数据切成7位块以将它们存储在UTF-8编码字符串的每个字节中,则编码数据仅会增长到初始大小的114%。比Base64更好。不幸的是,我们无法使用这个简单的技巧,因为JSON不允许某些ASCII字符。 ASCII的33个控制字符([0..31]和127)以及“和\必须被排除。这只留下了128-35 = 93个字符。在理论上,我们可以定义一个Base93编码,将编码后的大小增加到8 / log2(93)= 8 * log10(2)/ log10(93)= 122%。但是Base93编码不像Base64编码那样方便。Base64需要将输入字节序列分成6位块,对于这些块,简单的按位操作效果很好。此外,133%不比122%多得多。
这就是为什么我独立得出了通用结论,即Base64确实是在JSON中编码二进制数据的最佳选择。我的答案提供了正当理由。我同意从性能角度来看它并不理想,但请考虑使用JSON的好处,它具有易于在所有编程语言中操纵的人类可读字符串表示形式。
如果性能至关重要,则应考虑使用纯二进制编码替换JSON。但对于JSON,我的结论是Base64是最好的。

Base128怎么样,然后让JSON序列化程序转义“和\?我认为可以合理地期望用户使用JSON解析器实现。 - jcalfee314
1
@jcalfee314 很遗憾,这是不可能的,因为ASCII码低于32的字符在JSON字符串中是不允许的。已经定义了基数介于64和128之间的编码,但所需的计算量比base64更高。编码后文本大小的增益不值得。 - chmike
1
@Pacerier,当使用UTF8编码时,我的陈述是正确的。因此它并不是“完全错误”的。当用2个字节来存储每个字符时,存储大小就会变成二进制大小的260%。正如您所知,JSON用于数据存储或传输,在这种情况下使用UTF8编码。在这种情况下,也就是问题所关心的,我的评论是正确和相关的。 - chmike
将二进制编码为UTF-8,只需要修复无效的字节序列,不是吗?您不需要将> 127个字符代码映射到其精确表示中,只需将二进制数据视为utf-8字符串并转义FE和FF,并以某种方式填充截断的序列即可。 - w00t
1
然而,我刚才只是运行了 base64 imgfile | gzip | wc -l 命令,这只增加了原始图像文件的少数百分比。因此,由于 base64 很容易使用且 gzip 转换几乎是必需的,在传输压缩数据的压缩 JSON 时使用 base64 确实是一个好主意。然而,将未压缩的数据转换为 base64 再经过 gzip 处理所产生的字节数要比 gzip+base64+gzip 更高。 - w00t
显示剩余8条评论

45

1
在某些情况下,由于长度前缀和显式数组索引,BSON将使用比JSON更多的空间。 - Pawel Cioch
1
好消息:BSON原生支持二进制、日期时间等多种类型(如果您正在使用MongoDB,则特别有用)。坏消息是:它的编码是二进制字节...因此它对于OP来说不是一个答案。但是,如果您使用原生支持二进制的通道(如RabbitMQ消息、ZeroMQ消息或自定义TCP或UDP套接字),它将非常有用。 - Dan H

23

2
这是一个声称拥有更好性能的 JavaScript zip 实现:zip.js - Janus Troelsen
请注意,您仍然可以(并且应该)在之后进行压缩(通常通过 Content-Encoding),因为 base64 可以很好地进行压缩。 - Mahmoud Al-Qudsi
@MahmoudAl-Qudsi 你的意思是你要 base64(zip(base64(zip(data)))) 吗?我不确定再添加另一个 zip 然后再将其 base64(以便能够将其作为数据发送)是否是个好主意。 - andrej
@andrej https://dev59.com/TVcP5IYBdhLWcg3wf6Gb - android.weasel
@android.weasel,链接的解决方案指出您需要将gzip数据发送回服务器,而mod_deflate会在服务器端解压缩数据。到目前为止还不错,但这不能被称为透明地通过任何方式将gzipped数据发送回服务器。您必须自己在客户端(使用浏览器的JavaScript)压缩数据并特别制作标头。这是一个脆弱且不实用的解决方案。 - andrej
显示剩余4条评论

14

yEnc可能适合您:

http://en.wikipedia.org/wiki/Yenc

"yEnc是一种用于在[文本]中传输二进制文件的二进制到文本编码方案。它使用8位扩展ASCII编码方法,降低了先前基于US-ASCII的编码方法的开销。如果每个字节值平均出现的频率相同,则yEnc的开销通常(与uuencode和Base64等6位编码方法相比)只有1-2%,而其他方法可能达到33%-40%的开销。...到了2003年,yEnc成为Usenet上二进制文件的事实标准编码系统。"

然而,yEnc是一种8位编码方式,因此将其存储在JSON字符串中会遇到与存储原始二进制数据相同的问题——简单地存储会导致大约100%的扩展,这比base64更糟糕。


52
由于许多人似乎仍在查看此问题,我想提醒一下,我认为yEnc在这里并没有真正起到帮助作用。yEnc是一种8位编码,因此将其存储在JSON字符串中与存储原始二进制数据具有相同的问题-采用天真的方式意味着大约会扩展100%,这比base64更糟糕。 - hobbs
在使用像yEnc这样的编码与JSON数据中包含大字母时,可以考虑使用escapeless作为一个很好的替代方案,它提供了固定的、预先已知的开销。 - Ivan Kosarev
@hobbs,将8位字节存储到8位编码中,为什么会导致100%的开销? - user988346
@user988346,因为你不能直接将“8位编码”嵌入到JSON中,你必须先以某种方式将其编码为UTF-8字符。 - hobbs
@hobbs,yenc的重点明确是不这样做。这就是它的名字。另外,JSON并不强制要求UTF8。虽然有一个RFC这样要求,但json.org并没有,而它链接的ECMA404也没有。它只是说必须是某种Unicode。我认为你可以有有效的JSON,其中包含“utf-21”直接二进制数据块,除了需要转义结束引号。你只需要找到或创建解析器来处理它,但这就是使用yenc时会遇到的情况。这将与具有相同名称的多个键一样。在技术上是正确的,但大多数情况下不受支持。 - undefined

12

虽然base64的扩展率约为33%,但并不一定意味着处理开销显著高于此:这实际上取决于您正在使用的JSON库/工具包。编码和解码是简单直接的操作,甚至可以针对字符编码进行优化(因为JSON仅支持UTF-8/16/32)--对于JSON字符串条目,base64字符始终只占用一个字节。

例如,在Java平台上有一些可以有效地完成这项工作的库,因此开销大多是由于扩展大小而引起的。

我同意之前两个答案:

  • base64是简单常用的标准,因此很难专门找到更好的与JSON一起使用的方法(base-85被PostScript等使用;但在考虑时,好处仅在于边际效益)
  • 在编码之前(解码之后)进行压缩可能有很多意义,具体取决于您使用的数据

9

Smile格式

它的编码、解码和压缩非常快速。

速度比较(基于Java,但仍然有意义):https://github.com/eishay/jvm-serializers/wiki/

此外,它是JSON的扩展,允许您跳过字节数组的base64编码。

当空间关键时,Smile编码的字符串可以进行gzip压缩。


5
链接已失效。这个链接似乎是最新的:https://github.com/FasterXML/smile-format-specification - Zero3
这就是为什么在答案中添加链接是一个不好的做法。至少要添加一个有用的片段到答案中 :-) - Robert Perry

8

再补充一个低级恐龙程序员使用的选项...

一种自古以来就存在的老式方法是Intel HEX格式。它于1973年确立,UNIX纪元始于1970年1月1日。

  • 是否更高效?不是。
  • 是否是一个已经成为标准的格式?是。
  • 与JSON类似易读吗?有点像,并且比大多数二进制解决方案更容易阅读。

JSON格式如下:

{
    "data": [
    ":10010000214601360121470136007EFE09D2190140",
    ":100110002146017E17C20001FF5F16002148011928",
    ":10012000194E79234623965778239EDA3F01B2CAA7",
    ":100130003F0156702B5E712B722B732146013421C7",
    ":00000001FF"
    ]
}

13
它的效率更低吗?是的。 - spinkus
3
我们知道它的空间效率较低,但它的时间效率是否也较低呢?它明显更易于人类阅读理解。 - Alwyn Schoeman
在Intel HEX中,每一行都被放置在目标文件中的一个明确的16位地址上。如果我们将这些解释为128字节块的地址,该格式可以表示高达8 MB的文件。如果这些文件包含大量空洞(连续的零字节),则可以将其从编码中省略掉,实际上编码可以非常高效。这是一个相当特殊的情况,不太可能在实践中有用。 - Lutz Prechelt

6

由于您希望将二进制数据嵌入到严格基于文本且非常有限的格式中,因此我认为与您期望维护JSON的便利性相比,Base64的开销是最小的。如果处理能力和吞吐量是一个问题,那么您可能需要重新考虑文件格式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接