1. 程式人生 > >聊聊gbk與utf8互轉的亂碼問題

聊聊gbk與utf8互轉的亂碼問題

作為一個程式設計師,亂碼問題,應該我們都有遇到,但對於這個問題,很難用一句話概括亂碼是怎麼一回事,具體的問題還需要具體分析。

我們知道在計算機記憶體中,儲存的是二進位制資料,在網路傳輸中,也是二進位制資料,但最終呈現給使用者的是字串,二進位制與字串的轉化就需要編碼、解碼的參與,如果世界上只有一種字元編碼方式,就不會有亂碼這一說了,但事實是,編碼的方式太多了,utf-8、utf-32、utf-16、gbk、gb2312、iso-8859-1、big5、unicode等等。由於每個編碼的規則不一樣,一般都不能用一種進行編碼,用另一種進行解碼。如utf-8中,一個字母用一個位元組表示,一個漢字用三個位元組表示,特殊的漢字用四個位元組表示,而

gbk中,一個字母用一個位元組表示,一個漢字用兩個位元組表示。

有一個說法,記憶體中儲存的二進位制是unicode碼,如果記憶體中的資料需要儲存或傳輸時,才會進行一次轉化,將unicode碼轉化成其它的編碼二進位制(有待考證)。個人覺得這種方式很合理,畢竟unicode碼中每個字元都有獨一無二的二進位制與之對應。

排查亂碼問題,難度在於是在哪個環節出了問題,但亂碼的本質都是一樣的,讀取二進位制的編碼和最初將字串轉化成二進位制的編碼方式不一致。

此處說明一個概念,編碼指將字串轉化成二進位制,解碼指將二進位制轉化成字串

UTF-8編碼,GBK解碼

在這我們討論一下,gbkutf-8互轉的亂碼問題,直接上程式碼。

package com.anjz.test;
import java.io.UnsupportedEncodingException;

public class CodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String str = "你好,世界";
		System.out.println("字串長度:"+str.length());
		
		byte[] utfBytes = str.getBytes("utf-8");
		System.out.println("utf-8需要"+utfBytes.length+"位元組儲存");
		
		byte[] gbkBytes = str.getBytes("gbk");
		System.out.println("gbk需要"+gbkBytes.length+"位元組儲存");
	}
}

以上程式碼執行打印出一下內容:

字串長度:5

utf-8需要15位元組儲存

gbk需要10位元組儲存

可以看出,utf-8儲存一個漢字,需要3個位元組,gbk儲存一個漢字,需要2個位元組。

現用單個字元測試。

package com.anjz.test;
import java.io.UnsupportedEncodingException;

public class CodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String str = "你";
		
		byte[] utfBytes = str.getBytes("utf-8");
		for(byte utfByte:utfBytes){
			//位元組對應的十進位制是負數,因java中的二進位制使用補碼錶示的,此處使用0xff 還原成int表示的資料,再轉化成16進位制
			System.out.print(Integer.toHexString((utfByte & 0xFF)) +",");
		}
		System.out.println();
		String utf2gbkStr = new String(str.getBytes("utf-8"),"gbk");
		System.out.println("utf-8轉化成gbk:"+utf2gbkStr);
		
		byte[] gbkBytes = utf2gbkStr.getBytes("gbk");
		for(byte gbkByte:gbkBytes){
			System.out.print(Integer.toHexString((gbkByte & 0xFF))+",");
		}
		
		System.out.println();
		String gbk2utfStr = new String(utf2gbkStr.getBytes("gbk"),"utf-8");
		System.out.println("gbk轉化成utf-8:"+gbk2utfStr);
	}
}

執行上面程式碼,得出的結果:

e4,bd,a0,

utf-8轉化成gbk:浣�

e4,bd,3f,

gbk轉化成utf-8:�?

用兩個字元測試,將上述程式碼String str = “你”改成String str = “你好”。執行程式碼,得出的結果:

e4,bd,a0,e5,a5,bd,

utf-8轉化成gbk:浣犲ソ

e4,bd,a0,e5,a5,bd,

gbk轉化成utf-8:你好

上述實驗中,utf-8轉化成gbk出現亂碼,這個很好理解,但是再還原回去,gbk轉化成utf-8,單箇中文字元依然是亂碼,兩個字元卻能正常顯示,這個到底是怎麼回事呢?

經過一番研究,想把這個事說明白,還需要從它們的編碼規則著手。

ISO-8859-1

單位元組編碼,向下相容ASCII,其編碼範圍是0x00-0xFF,0x00-0x7F之間完全和ASCII一致,0x80-0x9F之間是控制字元,0xA0-0xFF之間是文字元號。

GBK

採用單雙位元組變長編碼,英文使用單位元組編碼,完全相容ASCII字元編碼,中文部分採用雙位元組編碼。雙位元組其編碼範圍從8140至FEFE(剔除xx7F)。

單位元組:00000000 - 01111111

雙位元組:10000001 01000000 - 11111110 11111110 (剔除******** 01111111)

單位元組、雙位元組的區分通過高位元組高位區分,單位元組高位為0,雙位元組的高位元組高位為1。

UTF-8

可變長字元編碼,是unicode碼的具體實現,UTF-8用1到6個位元組編碼Unicode字元。

UTF-8編碼規則:如果只有一個位元組則其最高二進位制位為0;如果是多位元組,其第一個位元組從最高位開始,連續的二進位制位值為1的個數決定了其編碼的位元組數,其餘各位元組均以10開頭。 

1位元組 0xxxxxxx

2位元組 110xxxxx 10xxxxxx

3位元組 1110xxxx 10xxxxxx 10xxxxxx

4位元組 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

5位元組 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

6位元組 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

明白上述GBK和UTF-8的編碼規則,我們再分析一下,單箇中文字元是亂碼,兩個字元卻能正常顯示的問題。

“你”

UTF-8編碼對應的二進位制:11100100 10111101 10100000

將上述二進位制通過GBK進行解碼,根據GBK規則,第一個位元組高位為1,使用雙位元組編碼,

“11100100 10111101”解碼成“浣”,“10100000”對於GBK來說是非法的,就解碼成了一種特殊字元“�”。

看看能不能將“浣�”還原回“你”呢?

GBK編碼對應的二進位制:11100100 10111101 00111111

看到上述的二進位制,根本不符合UTF-8編碼規則,故用UTF-8進行解碼,是解碼成了一些特殊字元“�?”。

對於上述情況可以看出,一個二進位制,如果不符合當前的編碼規則,會被解碼成特殊字元,但此特殊字元再進行編碼,是回不到最初的二進位制的

用同樣的方式,分析“你好”為什麼最終可以正常顯示。

UTF-8編碼對應的二進位制:11100100 10111101 10100000 11100101 10100101 10111101

將上述二進位制通過GBK進行編碼,根據GBK規則,使用雙位元組編碼,“1100100 10111101”解碼成“浣”,“10100000 11100101”解碼成“犲”,“10100101 10111101”解碼成“ソ”。

看看能不能將“浣犲ソ”還原成“你好”呢?

GBK 編碼對應的二進位制:11100100 10111101 10100000 11100101 10100101 10111101

可以看出二進位制是可以被還原的,將此二進位制通過UTF-8解碼,肯定能變成“你好”。

可以看出,一個字串,通過UTF-8進行編碼,再通過GBK進行解碼,再將得到的字串進行GBK編碼,最後將得到的二進位制通過UTF-8解碼,能否還原到最初的字串,在於UTF-8編碼後得到的二進位制,是否符合GBK的編碼規則,如果符合,最終就可以還原,如果不符合,就不可還原。

GBK編碼,UTF-8解碼

package com.anjz.test;
import java.io.UnsupportedEncodingException;

public class CodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String str = "你好";
		
		byte[] gbkBytes = str.getBytes("gbk");
		for(byte gbkByte:gbkBytes){
			//位元組對應的十進位制是負數,因java中的二進位制使用補碼錶示的,此處使用0xff 還原成int表示的資料,再轉化成16進位制
			System.out.print(Integer.toHexString((gbkByte & 0xFF)) +",");
		}
		System.out.println();
		String gbk2utfStr = new String(str.getBytes("gbk"),"utf-8");
		System.out.println("gbk轉化成utf-8:"+gbk2utfStr);
		
		byte[] utfBytes = gbk2utfStr.getBytes("utf-8");
		for(byte utfByte:utfBytes){
			System.out.print(Integer.toHexString((utfByte & 0xFF))+",");
		}
		
		System.out.println();
		String utf2gbkStr = new String(gbk2utfStr.getBytes("utf-8"),"gbk");
		System.out.println("utf-8轉化成gbk:"+utf2gbkStr);
	}
}

執行上述程式碼,結果為:

c4,e3,ba,c3,

gbk轉化成utf-8:���

ef,bf,bd,ef,bf,bd,ef,bf,bd,

utf-8轉化成gbk:錕斤拷錕�

上述結果應該都在意料之中,我們通過上述的方法分析一下。

“你好”GBK編碼的二進位制:11000100 11100011 10111010 11000011

GBK編碼的二進位制資料,完全匹配不了UTF-8的編碼規則,最終UTF-8只能按如下方式匹配,檢視第一個位元組,開頭“110”,理論上匹配兩個位元組,但看下一個位元組,開頭卻不是“10”,最終“11000100”解碼成“�”,看第二個位元組開頭是“1110”,理論匹配三個位元組,看下個位元組符合,以“10”開頭,但下下個位元組開頭是“110”,不符合匹配,最終“11100011 10111010”解碼成“�”,同理“11000011”也解碼成“�”,這個符號都是為找不到對應規則隨意匹配的一個特殊字元。

“���”UTF-8編碼的二進位制為:11101111 10111111 10111101 11101111 10111111 10111101 11101111 10111111 10111101

這個二進位制和原先的二進位制不相同,根本轉化不到最初的字串,按照GBK的編碼規則,“11101111 10111111”編碼成“錕”,“10111101 11101111” 編碼成“斤”,“10111111 10111101”編碼成“拷”,“11101111 10111111”編碼成“錕”,“10111101”不符合GBK規則,編碼成特殊字元“�”。

理論上說,用GBK編碼,UTF-8解碼的字串是不能還原到最初的字串的,因UTF-8編碼規則的特殊性,GBK編出的二進位制,是很難匹配上的。

總結

理論上說,系統出現亂碼,將亂碼還原到最初的樣子,上述UTF-8編碼,GBK解碼,這個有時是可以還原的,有時是還原不了的,要看UTF-8編碼的二進位制是否都能符合GBK的編碼規則,但GBK編碼,UTF-8解碼,這個基本是條不歸路。

但實際中,有一種情況,是100%可以將亂碼還原成最初的字串。就是任意編碼格式編碼,ISO-8859-1解碼,這個主要因為ISO-8859-1是單位元組編碼,而且匹配所有單位元組情況,亂碼字串總是可以還原到最初的二進位制。

拓展一個小知識點:

關於進位制的表示有兩種方式,一種是字首表示法,一種是字尾表示法。

字首表示法

十六進位制:0x

十進位制:無字首

八進位制:0

二進位制:沒有表示符號

字尾表示法

B :二進位制數

Q :八進位制數

D :十進位制數

H :十六進位制數

對於十進位制數通常不加字尾,也即十進位制數後的字母 D 可省略


參考文章

工具:

相關推薦

聊聊gbkutf8亂碼問題

作為一個程式設計師,亂碼問題,應該我們都有遇到,但對於這個問題,很難用一句話概括亂碼是怎麼一回事,具體的問題還需要具體分析。 我們知道在計算機記憶體中,儲存的是二進位制資料,在網路傳輸中,也是二進位制資料,但最終呈現給使用者的是字串,二進位制與字串的轉化就需要編碼、解碼的參

GBKUNICODE

在一些應用場景,會出現這樣的需求:UTF-8 -> Unicode -> GBK,然而,Unicode與GBK沒有相對應的演算法可以直接轉換…… 前提:GBK與UNICODE沒有直接的對應關係,只能通過一張大表將兩者聯絡起來。 對於計算機等大型裝置來說

GBKUTF8編碼

string UTF8ToGBK(const std::string& strUTF8) { int len = MultiByteToWideChar(CP_UTF8, 0, strUT

php utf8 gbk 數組

bsp 直接 each 記錄 sel 工作 都是 cnblogs 裏的 這些都是工作中常用的 前幾年寫過 但沒有記錄的習慣,後邊有要用到麻煩,現在記錄下 以後直接拿來用 數組裏的 utf8_to_gbk 方法 是上一篇寫的 直接調用 public static

MFC中UTF8GB2312

 UTF8轉GB2312 CString CMyBrowserDlg::ConvertUTF8toGB2312(CString str) {     int len = str.GetLength

C# 圖片Base64

name oba mar base64 richtext sender bin binary ram /// <summary> /// 將圖片數據轉換為Base64字符串 /// </summary> /// <pa

UnicodeAnsi

form nic code 手動 char empty ret wchar art 1 BOOL CTool::AnsiToUnicode(const char *pSrc, CString &strResult) 2 { 3 #ifndef _UNICOD

BYTE[]OBJECT工具

apt lis [] hao123 互轉 dap music ada adapter %E8%87%AA%E5%B7%B1%E7%90%86%E8%A7%A3%E7%9A%84aDapTER%E6%B3%A8%E9%87%8A http://music.hao123.com

字符串json

ont 兼容ie value cti span == eval foreach clas 一、json 轉字符串 var str = "1:測試1-1;2:測試1-2;3:測試1-3"; //字符串 var str

JSONUtil(JAVA對象/Listjson,xmljson)

transpose boolean ngs final span arraylist setname clas pro 1 package com.chauvet.utils.json; 2 3 import java.io.BufferedReader;

JAVA beanXML的利器---XStream

pub 普通 ati mat his cit true 是我 package 最近在項目中遇到了JAVA bean 和XML互轉的需求, 本來準備循規蹈矩使用dom4j忽然想起來之前曾接觸過的XStream, 一番研究豁然開朗,利器啊利器, 下來就XStream的一些用法與

MySQL時間戳日期

style time 函數 time() mysql 轉換 class body () 1、UNIX時間戳轉換為日期用函數: FROM_UNIXTIME() select FROM_UNIXTIME(1156219870); 輸出:2006-08-22 12:

CString char*

mfc1.CString轉char* CString ctrpath; CStringA strPathA = CW2A(ctrpath.GetBuffer(), CP_THREAD_ACP); char*temp=strPathA.GetBuffer(); char*轉CString(註意是大寫

java 中文unicode

true format param cte _id col rms AI deb public class FontUtil { public static void main(String[] args) { System.out.pr

CLOB,BLOBString

sub ray AR length HA string int 系統 println String s1="1231dsdgasd的颯颯大"; Clob c = new SerialClob(s1.toCharArray());//String 轉 clob Bl

php數組xml

php數組與xml互轉類代碼: /** * @desc:xml與array互轉 * @author [Lee] <[<[email protected]>]> * @property * data 傳入的數據 * @method * arraytoxml 數組轉xml

C++ UTF8 Unicode

C++/MFC   UTF8 轉 Unicode char* U8ToUnicode(char* szU8) { //UTF8 to Unicode //預轉換,得到所需空間的大小 int wcsLen = ::MultiByteToWideCha

Jackson之String物件

public class JacksonTest { //配置ObjectMapper物件 private static final ObjectMapper objectMapper = new ObjectMapper() .configure(Dese

python中strlist

1、list轉str 假設有一個名為test_list的list,轉換後的str名為test_str 則轉換方法: test_str = "".join(test_list) 需要注意的是該方法需要list中的元素為字元型,若是整型,則需要先轉換為字元型後再轉為str型別。

java物件XML

1. 定義XML對應的java實體類(可巢狀) import java.io.Serializable; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorT