動態

詳情 返回 返回

JSON 序列化中的轉義和 Unicode 編碼 - 動態 詳情

其實這是我上一篇文章的姊妹篇。在研究 Unicode 顏文字的時候,由於我們的數據傳輸是通過 JSON 串來完成的,在對顏文字進行轉碼傳輸的過程中,也發現了一個問題。解決問題之後,便有了本總結文。

JSON 中的普通轉義字符
個人認為,JSON 是目前針對程序員而言可讀性(readability)最佳的數據傳輸格式之一,並且 JSON 完整地考慮到了數據傳輸中的轉義,避免出現各種注入風險。當對 JSON 進行序列化操作時(Go 中稱為 marshal),根據 JSON 標準的説明,需要對字符串中的以下字符進行轉義:

符號 名稱 轉義後的字符串
" 雙引號 \"
/ 斜槓 /
\ 反斜槓 \
\b 退格符 \b
\f 垂直製表符 \f
Tab 水平製表符 \t
\r 回車 \r
\n 換行符 \n
< 左尖括號 \u003C

右尖括號 \u003E
& And 符號 \u0026
另外針對 Go 語言,個人建議再轉義一個百分號 % 為 \u0025,原因是在 Go 的各種字符串格式化操作中,百分號是一個關鍵字符,這樣可以避免在打日誌或者其他設計格式化的操作時出現錯誤。

JSON 中針對 Unicode 字符的處理
這裏所説的 Unicode 字符,準確而言指的是在 ASCII 範圍之外的字符,也就是值大於 0x7F 的 Unicode 字符。

其實大部分情況下,UTF-8 已經成為現代編程語言約定俗成的標準了,因此在 JSON 序列化時,只要簡單地對 Unicode 字符的值轉為二進制然後按照網絡字節序打包就可以了。

但是在某些情況下,當對端採用的不是 UTF-8,或者是對端採用的不是網絡字節序時(比如對方是技術底下/落後、但卻話語權強大的客户/合作商/集成商),這個時候,大家統一採用 ASCII 編碼,就能夠避免這些問題了。

那麼 JSON 是怎麼使用 ASCII 編碼來傳輸 Unicode 的呢?從前文的轉義其實就可以一窺端倪了——JSON 採用的是 \uXXXX 的形式來表示一個 Unicode 字符的。每個 Unicode 字符表示法中,XXXX 必須是4個十六進制數,即便高位為0也需要補全。通過這種方式,編碼和傳輸 Unicode 字符。在 ASCII 為主的數據傳輸中,這種編碼方式比較穩妥,並且不會額外增加過多的數據量。當然對於 Unicode 字符比較多的情況下(比如大量的中文),這就需要程序員考慮一下額外帶來的網絡花銷了。

大於 0xFFFF 的 Unicode 字符的編碼
讀者可能會注意到了,\uXXXX 格式最大隻能支持到 0xFFFF,但 Unicode 早就已經超過了這個範圍。大於 65535 的字符要怎麼表示呢?首先,絕對不是簡單地採用 \uXXXXX,這會導致編碼錯誤。

針對大於 65535 的字符,JSON 採用的是 UTF-16 編碼。UTF-16 採用了 Unicode 的一個特性:不超過20位。
比如我們用 u 代表這樣的一個字符,UTF-16 的處理方法如下:

首先,u = u - 0x10000
取 hi 等於做了減法之後的 u 的高10位:hi = (u & 0xFFC00) >> 10
取 lo 等於做了減法之後的 u 的低10位:lo = u & 0x003FF
高位加上 0xD800 後進行 \u 編碼
低位加上 0xDC00 後進行 \u 編碼
舉例説明:代表地球的顏文字符號 “🌍”,其編碼值為 0x1F30D,按照 UTF-16 編碼過程為:

u = 0x1F30D - 0x10000 = 0xF30D,二進制為:1111 0011 0000 1101
高10位等於 0000111100,低10位等於 1100001101
高位值 0x03C 做加法之後等於 0xD83C
低位值 0x30D 做加法之後等於 0xDF0D
最終編碼為 \uD83C\uDF0D
比如以下的 JSON:

{

"string":"我是地球🌍"

}
按照 ASCII 序列化之後,結果為:

{"string":"\u6211\u662F\u5730\u7403\uD83C\uDF0D"}

參考資料
UTF-16
JavaScript has a Unicode problem
Meaning of escaped unicode characters in JSON

Add a new 評論

Some HTML is okay.