字符編碼和字符集到底有什麼區別?Unicode和UTF-8是什麼關係?

前言

想必大家編寫代碼時肯定和我一樣,也遇到過漢字亂碼的問題。特別是,有時候和上下游對接接口,不能統一編碼格式的話,一堆亂碼問題,讓人頭皮發麻。

那麼爲什麼會有這麼多的亂碼問題?

什麼是字符編碼?什麼是字符集?他們之間有什麼區別和聯繫?

什麼是 Unicode ? Unicode 和我們常說的 UTF-8 又有什麼關係?

字符編碼和解碼

要想搞清楚上面的問題,首先我們要知道,在計算機中,不管是一段文字、一張圖片還是一段視頻,最終都是以二進制的方式來存儲。也就是最終都會轉化爲 0001 1011 0010 0110 這樣的格式。

換句話說,計算機只認識 0 和 1 這樣的數字,並不能直接存儲字符。所以我們需要告訴它什麼樣的字符對應的是什麼數字。

例如,我們的業務中有記錄客戶端的客戶行爲日誌,然後導出文件來分析,字段間會以 ESC 來分隔。

我在編寫代碼的時候,就需要定義一下這個ESC 字符應該對應什麼數字,這樣計算機才能識別並存儲。

比如我把它定爲 0001 1011,這樣計算機就把 ESC 這個字符存了下來。等我下次需要查看的時候,根據對應關係把它解出來就可以了。

上邊的兩個過程就對應字符的編碼和解碼過程。

字符編碼就是把字符按一定的規則,轉換成數字。字符解碼是編碼的逆過程,即把數字按規則轉換成字符。

這樣看來,貌似沒有什麼問題。

但是,這是我自己定義的編碼規則,我同桌阿霄就不樂意了。他非要認爲 ESC 應該定義爲 1101 1000,好傢伙正好和我定義的二進制數字順序相反。

那結果肯定不用說了,我把 0001 1011 這串數字給他之後,按照他的編碼規則來解,肯定是 &$#!這樣的東西。

所以,亂碼問題說到底,就是編碼和解碼的規則對應不上導致的。

ASCII 碼

爲了避免我和阿霄因爲編碼問題打起來,美國國家標準學會(AMERICAN NATIONAL STANDARDS INSTITUTE) ANSI 組織發話了。

停、停、停。不就是個編碼問題嗎,這種小事犯不着動手,我定義一個統一的規則,大家都按照我的規則來編碼和解碼不就好了嘛。

於是,ASCII 碼出現了,它定義了一個常用字符集,用來表示字符和數字的對應關係,如下表。

ASCII 碼全稱:美國信息交換標準代碼 (American Standard Code for Information Interchange)

圖片來自百度圖片

我一查表,ESC 字符不就對應 27 嗎,對應的二進制就是 0001 1011 。我去,沒想到我定義的規則竟和 ANSI 不謀而合。

同桌阿霄把掄在空中的拳頭收了起來,默默地回去敲代碼了。

ASCII 碼擴展碼

在使用英語的國家,ASCII 碼就足夠用了。但是,在其他歐洲發達國家比如法國,使用的語言是法語,有類似於這樣的 á 符號,ASCII 碼就不能表示了。那怎麼辦呢?

我們看上表就會發現,ASCII 碼錶的表示範圍是十進制 0~127,也就是二進制 0000 00000111 1111 。其實只是用了後邊的 7 位,第一位都是 0 。

而計算機二進制中一個字節是 8 個位,現在只用了 7 位。不行啊太浪費了,要充分利用第一個高位,擴展一下,這樣多了一位,能表示的字符範圍就多了一倍。(2的8次方=256)

這樣一些歐洲其他國家,也能在計算機中表示自己的文字了。

後來,隨着計算機的普及,中國的用戶也多了起來。卻發現,一個字節只能表示 256 個字符,遠遠不能滿足我們的要求。

於是,就出現了 GB2312 編碼,它使用了兩個字節來表示一個漢字。但是,並沒有把所有的位都用完,前面一個字節範圍 0xA1 ~ 0xF7 (即 10110001 ~ 11110111),後面一個字節範圍 0xA1 ~ 0xFE (即 10110001 ~ 11111110) 。 這樣就能表示簡體漢字 6763 個。

GB2312 是國家標準總局發佈的《信息交換用漢字編碼字符集》,也可以說是簡體中文的字符集。

但是,臺灣和香港等使用繁體字的地區怎麼辦。於是,就有了大五碼 Big5 編碼來存儲繁體。高字節(第一個字節)表示範圍 0x81~0xFE,低字節(第二個字節)表示範圍 0x40 ~ 0x7E,以及0xA1 ~ 0xFE 。

需要注意的是,GB2312 是簡體中文,Big5 是繁體中文。如果用其中一種編碼文字去讀另外一種編碼文字就會亂碼。所以,就出來了 GBK 編碼,把簡體中文和繁體中文,以及一些 GB2312 不支持的人名(如歷代總理有的名字用 GB2312 打不出來),還有一些我們不認識的古漢語都包含進去,共 2 萬多個字符。

再然後,我們發現少數民族像藏文,蒙古文這些少數民族的語言,GBK 也支持不了,就再進行擴展,出現了 GB18030 。又多了幾千個少數民族的文字。

所以,我們使用的 GB 國標系列文字都是在 ASCII 碼之上擴展的,它們是依次向下兼容的。表示文字範圍從小到大爲 GB2312 = Big5 < GBK < GB18030 。

Unicode 字符集

我們在打開一個文檔之前,就必須要知道它的編碼格式,否則用錯誤的方式解碼就會出現亂碼情況。

設想,如果一個文本中,有多種類型文字,包括中文,韓語,德語,日語,應該用哪種編碼方式?貌似怎麼處理都會有亂碼問題,那怎麼辦呢?

ISO(國際標準化組織)說:這好辦啊,我把地球上,只要是人們使用的,所有語言和符號都囊括其中,爲每個字符都指定一個唯一的字符碼,這樣就沒有亂碼問題了。於是 Unicode 出現了,又叫統一碼,萬國碼。

如上圖表,漢字“一”對應的 unicode 碼是 \u4e00。 我們通常在字符碼前加個 \u代表這是 unicode 碼。4e00 是十六進制表示。

也有很多在線轉碼工具供我們使用,如:http://tool.chinaz.com/tools/unicode.aspx

Unicode 編碼方案

首先強調一下以下幾個概念的區別:

  1. 字符:就是我們看到的一個字母或一個漢字、一個標點符號都叫字符。如上邊的漢字“一”就是一個字符。
  2. 字符碼:在指定的字符集中,一個字符對應唯一一個數字,這個數字就叫字符碼。如上邊的字符“一”,在 Unicode 字符集中,對應的字符碼爲 \u4e00
  3. 字符集:規定了字符和字符碼之間的對應關係。
  4. 字符編碼:規定了一個字符碼在計算機中如何存儲。

需要注意的是,Unicode 只是一個字符集,它規定了每個字符對應的唯一字符碼,卻沒有規定這個字符碼在計算機中怎樣存儲(也就是它的字符編碼格式)。

例如,上邊的漢字“一”,它的 Unicode 字符碼爲 \u4e00,轉換成二進制就是 100 1110 0000 0000 。可以看到,它有 15 位二進制數,至少需要兩個字節來存儲。

這只是簡單的漢字,如果其他複雜的字符有可能會需要 三、四 個字節或者更多字節來存儲。

那麼到底應該用幾個字節來存儲呢?

於是 UTF-32 編碼 制定了標準,一個字符就用四個字節來表示。這樣編碼和解碼都方便,固定取 32 位二進制就行了。

但是這樣又引來一個問題。比如 A 字符其實只需要一個字節就可以存儲了。如果必須要用四個字節來存儲,那麼前邊三個字節都要補 0 ,這樣勢必會造成空間的浪費。

於是 UTF-16 編碼(一個字符用兩個字節或者四個字節)和我們熟悉的 UTF-8 編碼格式就出現了。

這裏我們重點介紹 UTF-8 。它使用一種變長的編碼方式,可以使用 1~4 個字節來表示一個字符。根據不同的字符變換長度。

變長聽起來很美好,但是它的不固定性,就讓計算機懵逼了。比如,計算機怎麼知道這四個字節代表的是一個字符,還是四個字符,亦或是兩個字符呢?

於是,UTF-8 規定了以下編碼規則,來避免以上問題。

  • 對於單字節的符號,第一位設爲0,後邊 7 位對應這個字符的ASCII碼值。因此,像“A"這樣的英文字母,UTF-8 編碼和 ASCII 編碼是相同的。
  • 對於大於一個字節的符號,假設爲 n 字節,那麼第一個字節的前 n 位都設爲 1,這樣有幾個 1 就說明有幾個字節。然後,第 n+1 位設爲0 。後邊的字節,前兩位都設爲10 ,剩餘的其他二進制位都用這個字符的 Unicode 碼填充(從後向前填充,不夠補0)。
字節個數 Unicode符號範圍(16進制) UTF-8 編碼格式(二進制)
1(單字節) 0000 0000 ~ 0000 007F 0xxxxxx
2 0000 0080 ~ 0000 07FF 110xxxxx 10xxxxxx
3 0000 0800 ~ 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 0001 0000 ~ 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

剛開始看上表,可能比較懵逼。其實,Unicode 符號表示的範圍最大爲四個字節,因此二進制爲 4*8=32 位。我們知道,二進制轉換十六進制時,以四位爲一個單位轉換,因此,對應的十六進制爲 32/4=8 位。

上表中的 Unicode 符號範圍是以 16 進製表示,可以看到就是 8 位的。

我們還是以漢字 “一” 爲例,16進製表示爲 4e00,補全所有位,其實就是 0000 4E00 (不區分大小寫)。因此,查上表發現,它處在三個字節的 Unicode 範圍內(0000 0800 < 0000 4e00 < 0000 FFFF)。

所以,它用 UTF-8 來編碼,就是三個字節的,即格式是這樣的 1110xxxx 10xxxxxx 10xxxxxx

4e00 轉換爲二進制爲 100 1110 0000 0000,二進制位從後向前依次填充到上述格式中的x位置(也是從後向前填充)。

於是,就得出漢字 “一” 的 UTF-8 編碼後的二進制表示爲:1110 0100 1011 1000 1000 0000

其實,可以發現,漢字的二進制爲 15 位,前邊補零一位即爲 16 位 0100 1110 0000 0000。而三個字節的 UTF-8 編碼格式中的 x 個數也爲 3*8 - (4+2+2) = 16 位,正好一一對應。

那麼,我們這一通推算,是否正確呢。可以在程序中打印這個字符的二進制格式,以及UTF-8編碼後的二進制。程序如下,

public class Test {
    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println("字符'一'的二進制爲:" + Integer.toBinaryString('一'));
        System.out.println("========");
        String str = "一";
        System.out.println("轉換爲UTF-8編碼格式的二進制爲:"+ toBinary(str,"utf-8"));
    }
​
    public static String toBinary(String str, String encode) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        byte[] bytes = str.getBytes(encode);
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            sb.append(Integer.toBinaryString(b & 0xFF));
        }
        return sb.toString();
    }
​
}

打印結果爲:

字符'一'的二進制爲:100111000000000
========
轉換爲UTF-8編碼格式的二進制爲:111001001011100010000000

PS:通常的,我們發現常用的漢字以 16 進製表示都在 0000 0800 ~ 0000 FFFF 範圍內。因此,漢字在 UTF-8 編碼下通常佔用三個字節。

細心的同學可能發現了,我上邊轉換的漢字可以用 char 類型來存儲,這是爲什麼呢?

這是因爲,在 Java 中,默認使用的字符集就是 Unicode,可以容納 100 多萬個字符,其中就包括漢字。

我們使用的絕對大多數漢字,都在0000 0800 ~ 0000 FFFF 這個範圍內,可以看出來前邊的四位十六進制都用不到(都是0000),因此,只需要後邊的四位十六進制位,轉換爲二進制就是 4*4=16 位,只佔用了兩個字節(16/8=2)。而 char 在 Java 中佔用兩個字節,完全可以用來存儲漢字。

總結

最後,來解答下文章開頭的問題。

亂碼的問題,究其根本原因,其實是編碼和解碼時的規則不一樣導致的。

字符編碼和字符集是兩個不同的概念。一句話表示:字符集定義了字符到數字的映射關係,字符編碼定義了這個數字如何在計算機中表達(存儲)。

對於 ASCII 和 GB 系列,他們既是字符集也是字符編碼。GB 兼容 ASCII 碼。

而對於 Unicode 來說,字符集是 Unicode,而字符編碼可以是 UTF-8,UTF-16 和 UTF-32 。所以,我們平時常用的 UTF-8 編碼其實只是 Unicode 的一種編碼實現方式而已。

本期內容你學會了嗎,把學會打在評論區。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章