天天看點

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解析方法的開始處是這麼做的:

<code>/* 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 &gt;=2 &amp;&amp; (!json.data[0] || !json.data[1])) lual_error(1,"json parser does not support utf-16 or utf-32");</code>

前面我們說過一個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位元組編碼。

單位元組範圍

雙位元組範圍

低位元組: 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去處理。