一直以为对JSON所有的语法都了如指掌,毕竟json的标准用一只手都数的过来,直到我发现了一个叫ndJSON的标准,简单说,以下2种语法都是合法的:
图一:json格式
图二:ndjson格式
其中图一是常见的json格式,而且整个json对象是一个列表:元素由逗号分隔,再由方括号闭合。图二则是一种称为ndJSON的格式,由换行符(0x0A)分隔每个json对象,最外面也没有闭合字符对。ndjson的mime类型是application/x-ndjson。
需要注意的是,图一和图二并不等同,就是说,图一和图二不仅使用了不同的序列化格式,数据所表达的含义也是不同的。图一只表达了一个对象:一个列表,图二则表达了3个对象:3个“data”字典。这个区别是json和ndjson的本质区别。
NDJSON(ndjson.org)
ndjson(New-line Delimited JSON)是一个比较新的标准,本身超简单,就是一个.ndjson文件中,每行都是一个传统json对象,当然每个json对象中要去掉原本用于格式化的换行符,而json的string中本身就不允许出现换行符(取而代之的是\n),所以ndjson在语法上基本不会出现歧义。但现在问题来了,ndjson有什么用?
JSON流问题(https://en.wikipedia.org/wiki/JSON_streaming)
新的标准总是来自于新的需求。ndjson的出现起源于json流问题。当时,我在设计一个方法用于将mongodb数据库的一张表备份到一个文件中,由于涉及到3个端的数据传输而没有对数据做整体处理的需求,就得使用管道流了。
其实流的概念非常简单,所有的数据传输都是流,都需要把大的数据分割成若干小份然后依次传输,只不过大多情况下传输都是通过底下的api自动完成的,我们感受不到“分割”的过程,也很难感受到“管道传输”的过程。正是这种底层的屏蔽造成了我们的无知,当要我们亲自设计管道的时候就嗝屁了。
在上面这个跨3端管道传输数据流的任务中,需要一边序列化一边走管道,最合适的做法就是将整张表格分割成一个个json对象(无论是sql还是mongo,表中的每一行都可以看成一个json对象),然后通过主机管道流向文件系统。这里出现了一个问题,数据流的最终存在形式是什么?是一个json文件吗?不可能,因为json文件只能表示一个json对象,而数据库表中有若干个对象。那给mysql表中的每一行保存一份json文件?好像也不合适。
HACK JSON
勉强的方法是使用一个json文件存放一份超长的json列表来收纳每一行数据。之所以勉强是因为构造一个json列表需要一些hack技巧:一开始需要写一个‘[’,中间每个json对象之间需要写‘,’,传输完成后又需要一个‘]’,所以我的代码是这样的:
fsWriter.write('[')
mongoReader.on('data', rowObj => {
fsWriter.write(JSON.stringify(rowObj));
fsWriter.write(',');
});
mongoReader.on('close', () => {
// 由于json不允许在最后一个列表元素后面加逗号,hack一个空字典
fsWriter.write('{}]');
fsWriter.end();
});
只能说,hack一时爽,一直hack一直爽,天天hack火葬场。通过hack来达到目的是有后遗症的,容易给你带来一堆麻烦事。假如我想在json文件最后插入一条记录或者读取一条记录怎么办?json是作为一个整体来编译处理的,想要读取其中的某一部分也得先编译整个json对象。这是json设计上的一个缺陷,即整体无法直接分割,当然如果你想hack json的话我也不拦你,只是如果想要实现一个通用的方法就得重新设计json流的格式了。带着这个疑问,我想起了程序员3大错觉之一的“我超越了标准库”,于是在维基百科上查了一下原来真的有json流格式。
如图,维基百科介绍了4种不同的json流解决方案,其中第一种就是本文一开始讲到的ndjson,即使用换行符分割的json,由于换行符的特殊性,不会出现歧义:
{"some":"thing\n"}
{"may":{"include":"nested","objects":["and","arrays\n"]}}
ndjson和第二种解决方案比较相似,第二种是通过2个更特殊的控制字符来分割(确切的说是包裹)每一个独立对象,这两个字符是记录分隔符<RS>和行尾反馈符<LF>,这种解决方案利用这2个我前所未闻的控制字符来包裹每个json对象,颇有点超文本标记语言的感觉:
<RS>{"some":"thing"}<LF>
<RS>{
"may": {
"include": "nested",
"objects": [
"and",
"arrays"
]
}
}<LF>
图中第三种和第四种方案我就不推荐啦,第三种是不要分隔符,前后2个对象直接相连:
{"some":"thing\n"}{"may":{"include":"nested","objects":["and","arrays"]}}
第四种模仿二进制格式,将对象长度写在前缀里:
18{"some":"thing\n"}55{"may":{"include":"nested","objects":["and","arrays"]}}
这两种方案不仅长相丑陋,而且还容易引起歧义,强烈不推荐使用,而且4种方案中也只有第一种的ndjson实现了标准化,它也是最常用的。当然,这4种都是文本格式的流解决方案,在二进制流领域中问题就简单得多了,比如message pack对象的长度就写在前缀中,对象之后可以直接拼接下一个对象而不会出现任何歧义,就像刚刚的方案三一样。
最后总结一下ndjson对json的性能提升:ndjson使整个文件“流化”,或者说把整个文件分割成许多份,这样避免了整体的束缚,支持局部处理,变得更灵活更快,从而实现了序列化和流传输的同时进行。
参考链接
https://jimmy.blog.csdn.net/article/details/90678160
https://medium.com/@kandros/newline-delimited-json-is-awesome-8f6259ed4b4b
http://ndjson.org/
https://github.com/ndjson/ndjson-spec