前言
前人踩過的坑,後人不必再踩。
編碼格式,在前後端的對接中及其重要,由於一些編碼格式的侷限性,以及繁多的編碼格式,只要是雙方對接的編碼格式不對,通常都會發生中文亂碼問題。而作者也在實際專案中遇到了這種情況,並且進行了排查,對此學習過程進行記錄。
本文首先講下對應的基本知識點,從而講下一些基本操作,再通過實際專案中的排查過程進行概念的進一步理解。
什麼是編碼和解碼?
由來
計算機自己能理解的“語言”是二進位制數,最小的資訊標識是二進位制數,8個二進位制位表示一個位元組;而我們人類所能理解的語言文字則是一套由英文字母、漢語漢字、標點符號字元、阿拉伯數字等等很多的字元構成的字符集。如果要讓計算機來按照人類的意願進行工作,則必須把人類所使用的這些字符集轉換為計算機所能理解的二進位制碼,這個過程就是編碼,他的逆過程稱為解碼。
因為計算機只能處理數字,如果要處理文字,就必須先把文字轉換為數字才能處理。所以才要有編碼的存在。
Java中的相關用法
1.編碼(encode) : String ---> byte[]
String中有對應的方法:
①:byte[] getBytes() : 使用平臺的預設字符集將此 String 編碼為 byte 序列
②:byte[] getBytes(Charset charset) : 使用指定的字元編碼來編碼字串
③:byte[] getBytes(String charsetName) : 使用指定的字元編碼來編碼字串
2.解碼 (decode) : byte[] ---> String
String中有對應的構造方法:
①:String(byte[] bytes) : 通過使用平臺的預設字符集解碼指定的 byte 陣列
②:String(byte[] bytes, Charset charset) : 使用指定的字符集來解碼指定的byte陣列
③:String(byte[] bytes, String charsetName) : 使用指定的字符集來解碼指定的byte陣列
Byte
位元組(Byte)是儲存資料的基本單位,並且是硬體所能訪問的最小單位。
一個位元組儲存的數值範圍為0-255(無符號數)。
1B=8bit。
簡單理解,也就是能放8個0或者1,數值上等於一個一位的32進位制數,一個兩位的16進位制數。
編碼方式
大致講下常見的幾種編碼格式:ASCII,UTF-8,GBK
ASCII
我們知道,計算機內部,所有資訊最終都是一個二進位制值。每一個二進位制位(bit)有0
和1
兩種狀態,因此八個二進位制位就可以組合出256種狀態,這被稱為一個位元組(byte)。也就是說,一個位元組一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從00000000
到11111111
。
上個世紀60年代,美國製定了一套字元編碼,對英語字元與二進位制位之間的關係,做了統一規定。這被稱為 ASCII 碼,一直沿用至今。
ASCII 碼一共規定了128個字元的編碼,比如大寫的字母A
是65(二進位制01000001
)。這128個符號(包括32個不能列印出來的控制符號),只佔用了一個位元組的後面7位,最前面的一位統一規定為0
。
Unicode
Unicode讓全世界都說一種語言
為了實現跨語言、跨平臺的文字轉換和處理需求,ISO國際標準化組織提出了Unicode的新標準,這套標準中包含了Unicode字符集和一套編碼規範。Unicode字符集涵蓋了世界上所有的文字和符號字元,Unicode編碼方案為字符集中的每一個字元指定了統一且唯一的二進位制編碼,這就能徹底解決之前不同編碼系統的衝突和亂碼問題。
UTF-8
UTF-8 是 Unicode 的實現方式之一。UTF-8 挺巧妙的資料儲存格式
需要注意的是,Unicode 只是一個符號集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。
UTF-8 最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。
UTF-8 的編碼規則很簡單,只有二條:
1)對於單位元組的符號,位元組的第一位設為0
,後面7位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。
2)對於n
位元組的符號(n > 1
),第一個位元組的前n
位都設為1
,第n + 1
位設為0
,後面位元組的前兩位一律設為10
。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。
下表總結了編碼規則,字母x
表示可用編碼的位。
Unicode符號範圍 | UTF-8編碼方式 (十六進位制) | (二進位制) ----------------------+--------------------------------------------- 0000 0000-0000 007F | 0xxxxxxx 0000 0080-0000 07FF | 110xxxxx 10xxxxxx 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
跟據上表,解讀 UTF-8 編碼非常簡單。如果一個位元組的第一位是0
,則這個位元組單獨就是一個字元;如果第一位是1
,則連續有多少個1
,就表示當前字元佔用多少個位元組。
下面,還是以漢字嚴
為例,演示如何實現 UTF-8 編碼。
嚴
的 Unicode 是4E25
(100111000100101
),根據上表,可以發現4E25
處在第三行的範圍內(0000 0800 - 0000 FFFF
),因此嚴
的 UTF-8 編碼需要三個位元組,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然後,從嚴
的最後一個二進位制位開始,依次從後向前填入格式中的x
,多出的位補0
。這樣就得到了,嚴
的 UTF-8 編碼是11100100 10111000 10100101
,轉換成十六進位制就是E4B8A5
。
GBK
專門為解決漢字的編碼而生成的解決方案,一個漢字用兩個位元組表示
那麼,一個漢字究竟被儲存為什麼,就需要:先查unicode碼錶,然後根據在碼錶的位置進行計算。例如:“電”字,在碼錶中是3575,計算成utf8就是E794B5,而在GB2312的碼錶中為B5E7。
GBK的中文編碼是雙位元組來表示的,英文編碼是用ASCII碼錶示的,既用單位元組表示。但GBK編碼表中也有英文字元的雙位元組表示形式,所以英文字母可以有2種GBK表示方式。
為區分中文,將其最高位都定成1。英文單位元組最高位都為0。當用GBK解碼時,若高位元組最高位為0,則用ASCII碼錶解碼;若高位元組最高位為1,則用GBK編碼表解碼。
ISO-8859-1
tomcat預設編碼方式
ISO-8859-1編碼是單位元組編碼,向下相容ASCII,其編碼範圍是0x00-0xFF,0x00-0x7F之間完全和ASCII一致,0x80-0x9F之間是控制字元,0xA0-0xFF之間是文字元號。
BIG5
Big5,又稱為大五碼或五大碼,是使用繁體中文(正體中文)社群中最常用的電腦漢字字符集標準,共收錄13,060個漢字。大多用於我國臺灣,香港和澳門等。
位元組序
位元組序,簡單理解,就是位元組存放的順序的意思,這一小節是對於硬體底層是怎麼對位元組順序的儲存的闡述,不想看的可以略過。
上一節已經提到,UCS-2 格式可以儲存 Unicode 碼(碼點不超過0xFFFF
)。以漢字嚴
為例,Unicode 碼是4E25
,需要用兩個位元組儲存,一個位元組是4E
,另一個位元組是25
。儲存的時候,4E
在前,25
在後,這就是 Big endian 方式;25
在前,4E
在後,這是 Little endian 方式。
這兩個古怪的名稱來自英國作家斯威夫特的《格列佛遊記》。在該書中,小人國裡爆發了內戰,戰爭起因是人們爭論,吃雞蛋時究竟是從大頭(Big-endian)敲開還是從小頭(Little-endian)敲開。為了這件事情,前後爆發了六次戰爭,一個皇帝送了命,另一個皇帝丟了王位。
第一個位元組在前,就是"大頭方式"(Big endian)(人類思維,12 34 56 78),第二個位元組在前就是"小頭方式"(Little endian)(顛倒思維,78 56 34 12)。
那麼很自然的,就會出現一個問題:計算機怎麼知道某一個檔案到底採用哪一種方式編碼?
Unicode 規範定義,每一個檔案的最前面分別加入一個表示編碼順序的字元,這個字元的名字叫做"零寬度非換行空格"(zero width no-break space),用FEFF
表示。這正好是兩個位元組,而且FF
比FE
大1
。
如果一個文字檔案的頭兩個位元組是FE FF
,就表示該檔案採用大頭方式;如果頭兩個位元組是FF FE
,就表示該檔案採用小頭方式。
實際專案中遇到的問題
負責的是通過java呼叫python的資料介面,然而寫python的人不靠譜,說明用UTF-8,最後我排查出來,用的編碼方式不是UTF-8,浪費我一下午的時間。
說下排查過程,通過http請求的方式進行呼叫資料介面,java方面獲取資料是從reponse的輸入流中進行獲取,我從輸入流中獲取到完整的byte陣列,但是我通過UTF-8拿資料出來,會發生亂碼的現象,所以我乾脆寫了個工具類,對byte陣列進行所有字元編碼方式的驗證,最後也比較圖方便,通過看列印出來的資訊是不是亂碼來判斷。
//檢視byte陣列是什麼編碼格式
@Test
public void byteToCheck() throws UnsupportedEncodingException {
Map<String , Charset> map = Charset.availableCharsets();
Set<Map.Entry<String , Charset>> set = map.entrySet();
for(Map.Entry<String , Charset> entry : set){
checkEncoding(String.valueOf(entry.getValue()));
String s = String.valueOf(entry.getValue());
}
}
//檢查編碼格式
private void checkEncoding( String charset ) throws UnsupportedEncodingException {
//從位元組流中來的
byte[] byteArray = {123, 34, 99, 111, 100, 101, 34, 58, 32, 48, 44, 32, 34, 109, 101, 115, 115, 34, 58, 32, 34, -27, -68, -89, -27, -98, -126, -24,-82, -95, -25, -82, -105, -25, -88, -117, -27, -70, -113, -24, -65, -112, -24, -95, -116, -27, -121, -70, -23, -108, -103, 34, 44, 32, 34, 100, 97, 116, 97, 34, 58, 32, 123, 34, 103, 97, 109, 97, 54, 34, 58, 32, 48, 44, 32, 34, 104, 111, 114, 105, 122, 111, 110, 116, 97, 108, 115, 116, 114, 101, 115, 115, 34, 58, 32, 48, 44, 32, 34, 112, 105, 99, 116, 117, 114, 101, 95, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 34, 104, 116, 116, 112, 58, 47, 47, 52, 55, 46, 49, 49, 51, 46, 49, 48, 52, 46, 50, 50, 57, 47, 112, 121, 116, 104, 111, 110, 47, -24, -82, -95, -25, -82, -105, -27, -121, -70, -23, -108, -103, -24, -65, -108, -27, -101, -98, -27, -101, -66, 46, 112, 110, 103, 34, 44, 32, 34, 115, 97, 103, 95, 109, 97, 120, 34, 58, 32, 48, 44, 32, 34, 97, 34, 58, 32, 48, 125, 125};
//列印編碼
System.out.println( charset+"編碼格式");
System.out.println(new String(byteArray,charset));
}
排查出來字元編碼是CESU-8。
參考資料
(3條訊息) JavaSE基礎(124) IO流讀寫亂碼問題(字元編碼)_鄭清的IT學習之路-CSDN部落格
字元編碼筆記:ASCII,Unicode 和 UTF-8 - 阮一峰的網路日誌
(4條訊息) 字串編碼:ASCII、GB系列、Unicode、UTF-8_yangyang的專欄-CSDN部落格