Unicode 也稱為統一碼、萬國碼;看名字就知道,Unicode 希望統一所有國家的字符編碼。
Unicode 於 1994 年正式公布第一個版本,現在的規模可以容納 100 多萬個符號,是一個很大的集合。
有興趣的讀取可以轉到 https://unicode-table.com/cn/ 查看 Unicode 包含的所有字符,以及各個國家的字符是如何分布的。
Windows、Linux、Mac OS 等常見操作系統都已經從底層(內核層面)開始支持 Unicode,大部分的網頁和軟件也使用 Unicode,Unicode 是大勢所趨。
不過由於歷史原因,目前的計算機仍然安裝了 ASCII 編碼以及 GB2312、GBK、Big5、Shift-JIS 等地區編碼,以支持不使用 Unicode 的軟件或者文檔。內核在處理字符時,一般會將地區編碼先轉換為 Unicode,再進行下一步處理。
Unicode 字符集是如何存儲的
本節我們多次說 Unicode 是一套字符集,而不是一套字符編碼,它們之間究竟有什么區別呢?
嚴格來說,字符集和字符編碼不是一個概念:
- 字符集定義了字符和二進制的對應關系,為每個字符分配了唯一的編號。可以將字符集理解成一個很大的表格,它列出了所有字符和二進制的對應關系,計算機顯示文字或者存儲文字,就是一個查表的過程。
- 而字符編碼規定了如何將字符的編號存儲到計算機中。如果使用了類似 GB2312 和 GBK 的變長存儲方案(不同的字符占用的字節數不一樣),那么為了區分一個字符到底使用了幾個字節,就不能將字符的編號直接存儲到計算機中,字符編號在存儲之前必須要經過轉換,在讀取時還要再逆向轉換一次,這套轉換方案就叫做字符編碼。
有的字符集在制定時就考慮到了編碼的問題,是和編碼結合在一起的,例如 ASCII、GB2312、GBK、BIG5 等,所以無論稱作字符集還是字符編碼都無所謂,也不好區分兩者的概念。而有的字符集只管制定字符的編號,至於怎么存儲,那是字符編碼的事情,Unicode 就是一個典型的例子,它只是定義了全球文字的唯一編號,我們還需要 UTF-8、UTF-16、UTF-32 這幾種編碼方案將 Unicode 存儲到計算機中。
Unicode 可以使用的編碼方案有三種,分別是:
- UTF-8:一種變長的編碼方案,使用 1~6 個字節來存儲;
- UTF-32:一種固定長度的編碼方案,不管字符編號大小,始終使用 4 個字節來存儲;
- UTF-16:介於 UTF-8 和 UTF-32 之間,使用 2 個或者 4 個字節來存儲,長度既固定又可變。
UTF 是 Unicode Transformation Format 的縮寫,意思是“Unicode轉換格式”,后面的數字表明至少使用多少個比特位(Bit)來存儲字符。
1) UTF-8
UTF-8 的編碼規則很簡單:
- 如果只有一個字節,那么最高的比特位為 0,這樣可以兼容 ASCII;
- 如果有多個字節,那么第一個字節從最高位開始,連續有幾個比特位的值為 1,就使用幾個字節編碼,剩下的字節均以 10 開頭。
具體的表現形式為:
- 0xxxxxxx:單字節編碼形式,這和 ASCII 編碼完全一樣,因此 UTF-8 是兼容 ASCII 的;
- 110xxxxx 10xxxxxx:雙字節編碼形式(第一個字節有兩個連續的 1);
- 1110xxxx 10xxxxxx 10xxxxxx:三字節編碼形式(第一個字節有三個連續的 1);
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字節編碼形式(第一個字節有四個連續的 1)。
xxx 就用來存儲 Unicode 中的字符編號。
下面是一些字符的 UTF-8 編碼實例(綠色部分表示本來的 Unicode 編號):
字符 | 字母N |
符號æ |
中文⻬ |
---|---|---|---|
Unicode 編號(二進制) | 01001110 | 11100110 | 00101110 11101100 |
Unicode 編號(十六進制) | 4E | E6 | 2E EC |
UTF-8 編碼(二進制) | 01001110 | 11000011 10100110 | 11100010 10111011 10101100 |
UTF-8 編碼(十六進制) | 4E | C3 A6 | E2 BB AC |
對於常用的字符,它的 Unicode 編號范圍是 0 ~ FFFF,用 1~3 個字節足以存儲,只有及其罕見,或者只有少數地區使用的字符才需要 4~6個字節存儲。
2) UTF-32
UTF-32 是固定長度的編碼,始終占用 4 個字節,足以容納所有的 Unicode 字符,所以直接存儲 Unicode 編號即可,不需要任何編碼轉換。浪費了空間,提高了效率。
3) UTF-16
UFT-16 比較奇葩,它使用 2 個或者 4 個字節來存儲。
對於 Unicode 編號范圍在 0 ~ FFFF 之間的字符,UTF-16 使用兩個字節存儲,並且直接存儲 Unicode 編號,不用進行編碼轉換,這跟 UTF-32 非常類似。
對於 Unicode 編號范圍在 10000~10FFFF 之間的字符,UTF-16 使用四個字節存儲,具體來說就是:將字符編號的所有比特位分成兩部分,較高的一些比特位用一個值介於 D800~DBFF 之間的雙字節存儲,較低的一些比特位(剩下的比特位)用一個值介於 DC00~DFFF 之間的雙字節存儲。
如果你不理解什么意思,請看下面的表格:
Unicode 編號范圍 (十六進制) |
具體的 Unicode 編號 (二進制) |
UTF-16 編碼 | 編碼后的 字節數 |
---|---|---|---|
0000 0000 ~ 0000 FFFF | xxxxxxxx xxxxxxxx | xxxxxxxx xxxxxxxx | 2 |
0001 0000---0010 FFFF | yyyy yyyy yyxx xxxx xxxx | 110110yy yyyyyyyy 110111xx xxxxxxxx | 4 |
位於 D800~0xDFFF 之間的 Unicode 編碼是特別為四字節的 UTF-16 編碼預留的,所以不應該在這個范圍內指定任何字符。如果你真的去查看 Unicode 字符集,會發現這個區間內確實沒有收錄任何字符。
UTF-16 要求在制定 Unicode 字符集時必須考慮到編碼問題,所以真正的 Unicode 字符集也不是隨意編排字符的。
對比以上三種編碼方案
首先,只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因為它們沒有單字節編碼。
1) UTF-8 使用盡量少的字節來存儲一個字符,不但能夠節省存儲空間,而且在網絡傳輸時也能節省流量,所以很多純文本類型的文件(例如各種編程語言的源文件、各種日志文件和配置文件等)以及絕大多數的網頁(例如百度、新浪、163等)都采用 UTF-8 編碼。
UTF-8 的缺點是效率低,不但在存儲和讀取時都要經過轉換,而且在處理字符串時也非常麻煩。例如,要在一個 UTF-8 編碼的字符串中找到第 10 個字符,就得從頭開始一個一個地檢索字符,這是一個很耗時的過程,因為 UTF-8 編碼的字符串中每個字符占用的字節數不一樣,如果不從頭遍歷每個字符,就不知道第 10 個字符位於第幾個字節處,就無法定位。
不過,隨着算法的逐年精進,UTF-8 字符串的定位效率也越來越高了,往往不再是槽點了。
2) UTF-32 是“以空間換效率”,正好彌補了 UTF-8 的缺點,UTF-32 的優勢就是效率高:UTF-32 在存儲和讀取字符時不需要任何轉換,在處理字符串時也能最快速地定位字符。例如,在一個 UTF-32 編碼的字符串中查找第 10 個字符,很容易計算出它位於第 37 個字節處,直接獲取就行,不用再逐個遍歷字符了,沒有比這更快的定位字符的方法了。
但是,UTF-32 的缺點也很明顯,就是太占用存儲空間了,在網絡傳輸時也會消耗很多流量。我們平常使用的字符編碼值一般都比較小,用一兩個字節存儲足以,用四個字節簡直是暴殄天物,甚至說是不能容忍的,所以 UTF-32 在應用上不如 UTF-8 和 UTF-16 廣泛。
3) UTF-16 可以看做是 UTF-8 和 UTF-32 的折中方案,它平衡了存儲空間和處理效率的矛盾。對於常用的字符,用兩個字節存儲足以,這個時候 UTF-16 是不需要轉換的,直接存儲字符的編碼值即可。
Windows 內核、.NET Framework、Cocoa、Java String 內部采用的都是 UTF-16 編碼。UTF-16 是幕后的功臣,我們在編輯源代碼和文檔時都是站在前台,所以一般感受不到,其實很多文本在后台處理時都已經轉換成了 UTF-16 編碼。
不過,UNIX 家族的操作系統(Linux、Mac OS、iOS 等)內核都采用 UTF-8 編碼,我們就不去爭論誰好誰壞了。
寬字符和窄字符(多字節字符)
有的編碼方式采用 1~n 個字節存儲,是變長的,例如 UTF-8、GB2312、GBK 等;如果一個字符使用了這種編碼方式,我們就將它稱為多字節字符,或者窄字符。
有的編碼方式是固定長度的,不管字符編號大小,始終采用 n 個字節存儲,例如 UTF-32、UTF-16 等;如果一個字符使用了這種編碼方式,我們就將它稱為寬字符。
Unicode 字符集可以使用窄字符的方式存儲,也可以使用寬字符的方式存儲;GB2312、GBK、Shift-JIS 等國家編碼一般都使用窄字符的方式存儲;ASCII 只有一個字節,無所謂窄字符和寬字符。