你所不知道的ndJSON:序列化與管道流

一直以爲對JSON所有的語法都瞭如指掌,畢竟json的標準用一隻手都數的過來,直到我發現了一個叫ndJSON的標準,簡單說,以下2種語法都是合法的:

640?wx_fmt=png

圖一:json格式

640?wx_fmt=png

圖二:ndjson格式

其中圖一是常見的json格式,而且整個json對象是一個列表:元素由逗號分隔,再由方括號閉合。圖二則是一種稱爲ndJSON的格式,由換行符(0x0A)分隔每個json對象,最外面也沒有閉合字符對。ndjson的mime類型是application/x-ndjson。

需要注意的是,圖一和圖二並不等同,就是說,圖一和圖二不僅使用了不同的序列化格式,數據所表達的含義也是不同的。圖一隻表達了一個對象:一個列表,圖二則表達了3個對象:3個“data”字典。這個區別是json和ndjson的本質區別。

NDJSON(ndjson.org)

640?wx_fmt=png

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個端的數據傳輸而沒有對數據做整體處理的需求,就得使用管道流了。

640?wx_fmt=png

其實流的概念非常簡單,所有的數據傳輸都是流,都需要把大的數據分割成若干小份然後依次傳輸,只不過大多情況下傳輸都是通過底下的api自動完成的,我們感受不到“分割”的過程,也很難感受到“管道傳輸”的過程。正是這種底層的屏蔽造成了我們的無知,當要我們親自設計管道的時候就嗝屁了。

在上面這個跨3端管道傳輸數據流的任務中,需要一邊序列化一邊走管道,最合適的做法就是將整張表格分割成一個個json對象(無論是sql還是mongo,表中的每一行都可以看成一個json對象),然後通過主機管道流向文件系統。這裏出現了一個問題,數據流的最終存在形式是什麼?是一個json文件嗎?不可能,因爲json文件只能表示一個json對象,而數據庫表中有若干個對象。那給mysql表中的每一行保存一份json文件?好像也不合適。

HACK JSON

勉強的方法是使用一個json文件存放一份超長的json列表來收納每一行數據。之所以勉強是因爲構造一個json列表需要一些hack技巧:一開始需要寫一個‘[’,中間每個json對象之間需要寫‘,’,傳輸完成後又需要一個‘]’,所以我的代碼是這樣的:

fsWriter.write('[')	
dbReader.on('data', rowObj => {	
    fsWriter.write(JSON.stringify(rowObj));	
    fsWriter.write(',');	
});	
dbReader.on('close', () => {	
    // 由於json不允許在最後一個列表元素後面加逗號,hack一個空字典	
    fsWriter.write('{}]');	
    fsWriter.end();	
});

只能說,hack一時爽,一直hack一直爽,天天hack火葬場。通過hack來達到目的是有後遺症的,容易給你帶來一堆麻煩事。假如我想在json文件最後插入一條記錄或者讀取一條記錄怎麼辦?json是作爲一個整體來編譯處理的,想要讀取其中的某一部分也得先編譯整個json對象。這是json設計上的一個缺陷,即整體無法直接分割,當然如果你想hack json的話我也不攔你,只是如果想要實現一個通用的方法就得重新設計json流的格式了。帶着這個疑問,我想起了程序員3大錯覺之一的“我超越了標準庫”,於是在維基百科上查了一下原來真的有json流格式。

640?wx_fmt=png

如圖,維基百科介紹了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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章