閱讀963 返回首頁    go 微軟 go Office


JSON數據亂碼問題

背景
程序員一提到編碼應該都不陌生,像gbk、utf-8、ascii等這些編碼更是經常在用,但時不時也會出個亂碼問題,解決這個問題的方法大部分都是先google和baidu一下,最後可能在某個犄角旮旯裏找到一點信息,然後就機械的按部就班的模仿下來,結果問題可能真就迎刃而解了,然後就草草了事,下回遇到相似的問題,可能又是重複上麵的過程。很少有人有耐心去花精力弄明白這寫問題的根本原因,以及解決這些問題的原理是什麼。這篇文章就是通過一個實際案例,試著去講清楚什麼是編碼,亂碼又是怎麼產生的,以及如何解決。該案例是從lua_cjson.c這個庫開始的,對這個庫不熟悉也沒關係,也不需要熟悉它,我們隻是借用它來說明亂碼問題,隻需要跟著文章的思路走就可以。

前段時間同事在作一個新項目的時候用到了lua_cjson.c這個庫(以下簡稱cjson),將json串轉換成LUA本地的數據結構,但是在使用的過程中出現了中文亂碼問題,奇怪的是隻有那麼幾個字是亂碼,這其中就包括”朶”字,其他字一切正常。經了解json串用的是GBK編碼,那問題就來了,為什麼用gbk編碼會出現這個問題,原因是什麼?又應該怎麼解決這個問題?

要解釋清楚這個問題,首先我們來看看json串都有哪些要求。

JSON規範
json全稱JavaScript Object Notion是結構化數據序列化的一個文本,可以描述四種基本類型(strings,numbers,booleans and null)和兩種結構類型(objects and arrays)。

RFC4627中有這樣一段話

A string is a sequence of zero or more Unicode characters.
字符串有零個或多個unicode字符序列組成.

在這裏稍微解釋下什麼是unicode字符。我們都知道ascii字符有字母、數字等,但是他收錄的字隻有一百多個。比如漢字就不是ascii字符,但是unicode收錄了漢字,所以漢字可以是unicode字符。這裏要說明的是unicode字符其實就是一些符號。

現在另一個問題出來了,在json文本中應該怎麼表示這些字符。
在規範的Encoding片段是這樣說的

JSON text SHALL be encoded in Unicode. The default encoding is UTF-8。
JSON文本SHALL把unicode字符編碼。默認使用utf-8編碼。

我們看到在這裏用到了SHALL[RFC2119]這個關鍵字,也就是說字符必須被編碼後才能作為JSON串使用。而且默認使用utf-8編碼。
如何判斷使用的是那種unicode編碼呢?

Since the first two characters of a JSON text will always be ASCII characters[RFC0020],

it is possible to determine whether an octet stream is UTF-8、UTF-16(BE or LE), or
UTF-32(BE or LE)by looking at the pattern of nulls in the first four octets.

由於json文本的前兩個字符(注意這裏說的是字符,不是字節)一定是ASCII字符,因此可以從一個字節
流的前四個字節(注意是字節)中判斷出該字節流是UTF-8、UTF-16(BE or LE)、or UTF-32(BE or LE)編碼。

00 00 00 xx UTF-32BE  (u32編碼大端)
xx 00 00 00 UTF-32LE  (u32編碼小端)
00 xx 00 xx UTF-16BE  (u16編碼大端)
xx 00 xx 00 UTF-16LE  (u16編碼小端)
xx xx xx xx UTF-8   (utf-8編碼)
ps:
u32用32位的4字節整數表示一個字符;

u16用16位的2字節整數表示一個字符,如果2字節表示不了,就用連續兩個16位的2字節整
數表示,所以就會出現u16編碼中有4個字節表示一個字符的情況,和u32的四字節不一
樣的是,該字符在u16中的前兩個字節和後兩個字節之間不會有字序的問題。

utf-8用多個8位的1字節序列來表示一個字符,所以沒有字序的問題.

截止到現在我們沒有看到任何關於可以使用GBK編碼的信息,難道json文本就不能用gbk編碼嗎,如果真的不能用的話,那為什麼cjson不是把所有的gbk編碼解釋稱亂碼,而是隻有某幾個字是亂碼.
在規範中對json解析器有這樣一段描述:

A JSON parser transforms a JSON text into another representation.
A JSON parser MUST accept all texts that conform to the JSON grammar.
A JSON parser MAY accept non-JSON forms or extensions.

json解析器可以將一個json文本轉換成其他表示方式。
json解析器MUST接受所有符合json語法的文本.
json解析器MAY接受非json形式或擴展的文本.

亂碼的原因
從規範對對解析器的描述可以看到,規範並沒有要求解析器必須對文本的編碼方式做校驗,而且解析器也可以有選擇的去接受非json形式的文本。

現在我們再來看看cjson解析器是如何做的,在cjson開頭的注釋中說了這麼一句話:

Invalid UTF-8 characters are not detected and will be passed untouched。
If required, UTF-8 error checking should be done outside this library。
發現無效的UTF-8編碼會直接放過,如果有必要對UTF-8編碼的檢查應該在該庫的之外。

說的很清楚,對非utf8編碼直接放過,不做任何檢查,所以用gbk編碼不符合規範,但又可以被解析的答案就出來了。那”朶”等這些字的亂碼問題又是怎麼回事? 我們現在看看cjson對規範中的另外兩個編碼utf16、utf32是如何做的,然後再說亂碼問題.

在cjson解析方法的開始處是這麼做的:

/* Detect Unicode other than UTF-8(see RFC 4627, Sec 3)
*
* CJSON can support any simple data type, hence only the first
* character is guaranteed to be ASCII (at worst:'"'). This is
* still enough to detect whether the wrong encoding is in use.
*/
if (json_len >=2 && (!json.data[0] || !json.data[1]))
luaL_error(1,"JSON parser does not support UTF-16 or UTF-32");

前麵我們說過一個json串的前兩個字符一定是ascii字符,也就是說一個json串至少也的有兩個字節.所以這段代碼首先判斷json串的長度是不是大於等2,然後根據串的前兩個字節的值,是否有零來判斷該文本是否是非utf-8編碼。結果已經看到了,人家不支持規範上說的u16和u32編碼.

現在我們就來看看”朶”這個子是如何變成亂碼的,經過對cjson源碼的分析得知,cjson在處理字節流的時候當遇見’\’反斜杠時會猜測後一個字節應該是要被轉義的字符,比如\b、\r之類的字符,如果是就放行,如果不是,cjson就認為這不是一個正確的json格式,就會把這個字節給幹掉,所以本來用兩個字節表示的漢子就硬生生的給掰彎了。
那”朶”字跟’\’反斜杠又有什麼關係? 查詢這兩字符在編碼中的表示得出:
“朶” 0x965C
“\” 0x5C

這樣我們就看到”朶”字的低位字節和”\”字符相同,都是0x5C,如果這時候”朶”字後邊不是b、r之類的可以被轉移ascii字符,cjson就會把這個字節和緊跟其後的一個字節抹掉,所以亂碼就產生了。

那我們應該怎麼解決這個問題,讓cjson可以順利的支持gbk編碼呢,首先我們看看gbk編碼是怎麼回事,為什麼會出現低位字節和ascii衝突的問題.

GB_編碼係列 
先來了解一下GB係列的編碼範圍問題:
GB2312(1980)共收錄7445個字符,6763個漢字和682個其他字符。
每個漢字及符號用兩個字節表示,為了跟ascii兼容,處理程序使用EUC存儲方法。
漢字的編碼範圍
高字節: 0xB0 – 0xF7,
低字節: 0xA1 – 0xFE,

占用72*94=6768,0xD7FA – 0xD7FE未使用。

GBK共收錄21886個字符,采用一字節和雙字節編碼。
單字節表示範圍
8位: 0x0 – 0x7F
雙字節表示範圍
高字節: 0x81 – 0xFE
低字節: 0x40 – 0x7E、0x80 – 0xFE

GB18030收錄70244個漢字,采用1、2、4字節編碼。
單字節範圍
8位: 0x0 – 0x7F 
雙字節範圍
高字節: 0x81 – 0xFE
低字節: 0x40 – 0xFE

四字節範圍
第一字節:0x81 – 0xFE
第二字節:0x30 – 0x39
第三字節:0x81 – 0xFE
第四字節:0x30 – 0x39

由於GB類的編碼都是向下兼容的,這裏就有一個問題,因為GB2312的兩個字節的高位都是1,符合這個條件的碼位隻有128*128=16384個。GBK和GB18030都大於這個數,所以為了兼容,我們從上麵的編碼範圍看到,這兩個編碼都用到了低位字節的最高位可以為0的情況。

最終得出的結論就是,在GBK編碼中隻要該字符是兩個字節表示,並且低位字節是0x5C的字符都會被cjson弄成亂碼.

解決方案:
1) 不要使用gbk編碼,將你的字符串轉換成utf-8編碼.
2) 對cjson源碼稍微做個改動,就是在每個字節到來之前先判斷該字節是否大於127,如果大於則將該字節個隨後的一個字節放過,否則交給cjson去處理。

最後更新:2017-05-22 12:01:22

  上一篇:go  《Apache Zookeeper 官方文檔》-1簡介
  下一篇:go  《Groovy官方文檔》Groovy開發套件-使用集合