從ASCII、GBK與Unicode等字符編碼角度搞清楚為什么開發中會出現亂碼問題
在我們日常的開發過程中經常會出現亂碼問題,這種問題往往發生在數據的輸入和輸出過程中。 下面我們從字符編碼的角度來了解為什么不同的字符編碼之間會出現亂碼的問題。
我們都知道在計算機只存儲和處理的是二進制數據,所有字符(如文字、符號、數字)都需要被編碼為二進制形式,亂碼其實是字符的編碼和解碼二進制數據不匹配導致的問題,也就是當一個字符的編碼方式與解碼方式不一致時,解碼結果會與預期不符,呈現出錯誤的字符。
1、ASCII碼
眾所周知,計算機是美國人發明的,美國人將他們國家常用的字符如英文字母、數字和標點符號等可見字符,還有一些日常使用(如打印)中涉及到的不可見字符(如回車鍵這樣的控制字符)都存儲到計算機里面。接下來他們按照順序將不可見字符(33個)、可見字符(95個)列出來,于是就形成了如下圖所示的ASCII碼表:
圖片
給0、1、2、a、b.....這樣的字符稱之為ASCII碼字符集;給ASCII碼字符集對應的二進制碼稱之為ASCII碼。
ASCII碼如果在美國一直使用是沒有問題,但是其他國家學習計算機就會存在問題,因為沒有對應的字符編碼,于是對ACSII碼進行了擴展。
擴展的方法是將原ASCII碼的第一位0修改成第一位為1,然后從128個字符擴展到255個字符,這樣新增加了128個,給新增加128個字符取名叫做擴展字符集,如下圖所示的擴展圖:
圖片
通過擴展ASCII碼暫時解決了西方國家的字符編碼問題。隨著計算機不斷的發展和普及,計算機走向了世界各國,就比如我們中國來說,中國的漢字至少有上千個,這個時候256個字符已經不夠用了。
2、GB2312碼
由于8位表示一個字符已經無法滿足中文字符的實際需求了,所以就設計使用16位表示一個字符,那么中文怎么編碼呢?編碼的步驟如下所示:
(1)設定字符集
中文字符比較多,采用分區管理的方式管理中文字符集,共分成94個區,每個區含94個位,共8836個碼位。如下所示的第1區的字符集:
圖片
每個分區的存儲的內容如下表整理:
分區范圍 | 存儲的內容 |
01-09 | 收錄除漢字外的682個字符 |
10-15 | 用戶自定義符號區(未編碼) |
16-55 | 收錄3755個一級漢字,按拼音排序 |
56-87 | 收錄3008個二級漢字,按部首/筆畫排序 |
88-94 | 用戶自定義漢字區(未編碼) |
(2)定義漢字的位置
每個分區的字符如何確定其字符的位置呢?在GB212中使用分區號+字符的行列號來確定字符的位置,如下圖所示的第16分區中的”白“字:
圖片
”白“字使用其分區號(16)+行號(3)+ 列號(7)組成1637
(3)計算實際的存儲位置
將”白“字的碼位1637分別拆分成16、37后轉成十六進制,然后對這兩個十六進制數再分別與A0相加(加A0的好處是讓計算機區分ASCII碼和GB2312碼),將得到的結果合并就計算出了”白“字的存儲位置,如下圖所示的計算過程:
圖片
通過這種規則的計算,得到”白“字的實際在計算機中存儲位置為0xB0C5。
3、GBK
由于我們中國的漢字太多了,導致了很多漢字都不在GB2312碼中,為了滿足實際的需要,于是對GB2312碼進行擴充。
擴充的方法是將之前沒有用上的碼位都使用上,并且不在規定它的低位一定要大于127,可以小于127,但是必須要保證高位大于127,然后規定計算機只要遇到大于127的字節就表示一個漢字的開始。
圖片
通過這種方式新增了近2萬個字符和漢字,將這些字符集稱為GBK字符集,對應的編碼稱為GBK碼。
3、Unicode
中國可以實現自己的一套編碼規則,同樣的道理,其他的國家也可以實現自己的一套編碼,這樣就出現了新的問題,世界上這么多的編碼,那么不同國家在進行通信的時候就會出現亂碼的現象。于是ISO的組織提出了一套規范,這樣就出現了Unicode的標準。Unicode是一個標準,它包含了字符集以及對應的編碼規則,目的就是將世界上所有的字符整理到一起并且進行編碼。
3.1、UCS-2字符集
初期Unicode使用16位的UCS-2字符集,UCS-2字符集將世界上所有用到的字符羅列到一起,按照順序標上對應的位置編碼然后轉成二進制存儲,這樣UCS-2字符集可以表示65536個字符。如下圖所示的UCS-是字符集:
圖片
3.2、UCS-4字符集
UCS-2字符集還是無法表示世界上所有的語言字符,于是Unicode推出了UCS-4字符集,UCS-4字符集用32位表示一個字符,如下圖所示:
圖片
UCS-4字符集可以表示近42億個字符,基本可以容納世界上所有的字符了,由于UCS-4字符集占用的空間大,所以它沒有被各國很好的接受。
3.3、UTF-8編碼
到了互聯網飛速發展的階段,世界各國的交流日益頻繁,不同的編碼之間無法通信,大家便重新思考unicode編碼,于是便推出如下的編碼規則:
圖片
UTF-32屬于定長編碼,它的每個字符編碼固定占4字節,比如對于英文字母a,UTF-32表示這個字符需要32位,在ASCII的編碼中字符a只需8位就可以表示,那么那如果存儲的內容主要是英文,使用UTF-32占用的存儲空間就是使用ASCII編碼占用的存儲空間的四倍。
UTF-32編碼會造成嚴重的內存消耗,而UTF-8編碼則不存在這個問題,因為它是一種變長的編碼,對于英文字符UTF8和ASCII編碼是一樣的,只占一個字節,對于非英文的字符,UTF-8會使用2~4個字節來表示(如對于中文一般會使用三個字節來表示)。
UTF-8的優勢是可以有效的利用存儲空間避免浪費,并且UTF-8向后兼容了ASCII編碼,UTF-8的編碼規則有如下的幾種:
圖片
1個字節的UTF-8編碼,它的最高位固定為0,剩余的七位用來編碼,這和ASCII編碼是完全一樣的,所以為什么UTF-8可以兼容了ASCII就是這個原因。
2個字節的UTF-8編碼的首字節的前三位為110,其余字節的開頭兩位為10。
3個字節的UTF-8編碼的首字節的前四位為1110,其余字節的開頭兩位是10
4個字節的UTF-8編碼首字節的前五位為11110,其余四節的開頭是10。
從UTF-8的編碼規則我們可以看到對于2字節到4字節的編碼,它的首字節開頭有幾個連續的1,那就代表著它這個編碼占了幾個字節,那這樣解碼的時候就知道如何對這個二進制數據進行解碼。
那么一個漢字是如何轉成UTF-8編碼呢?以中文的這個”王“字為例,它在UCS-4中的編碼是0x0000 738B,其UTF-8的編碼過程如下所示:
圖片
注意的是,如果漢字的二進制無法填滿模板的所有的x空位,則剩余的空位默認都用0來填充,通過這種方式填充完以后就得到了漢字中所對應的UTF-8的編碼。同樣要解碼的話,也只需要逆序執行一次就可以得到對應的漢字。
至此我們了解了編碼的發展過程,由于每種編碼都有自己的規則,如果不按照它的規則進行解碼就得到一串亂碼。
在SpringBoot中默認的字符編碼通常是UTF-8,這是Java和SpringBoot推薦的標準字符編碼(特別是在處理Web請求和響應)。因為UTF-8編碼支持廣泛的字符集,包括大多數自然語言,并且可以有效地減少數據傳輸時的空間。