淺談Unicode編碼格式和程式碼中的應用
ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼)是基於拉丁字母的一套計算機編碼系統。它主要用於顯示現代英語,一共定義了 128 個字元,其中33個字元無法顯示(一些終端提供了擴充套件,使得這些字元可顯示為諸如笑臉、撲克牌花式等8-bit符號),且這33個字元多數都已是陳廢的控制字元。例如大寫的字母 A 是65
(這是十進位制數,對應二進位制是0100 0001
)。
ASCII的侷限在於只能顯示26個基本拉丁字母、阿拉伯數目字和英式標點符號,因此只能用於顯示現代美國英語(而且在處理英語當中,即使會違反拼寫規則,外來詞如naïve、café、élite等等時,所有重音符號都必須去掉)。雖然EASCII解決了部分西歐語言的顯示問題,但對更多其他語言依然無能為力。因此,現在的軟體系統大多采用Unicode" target="_blank" rel="nofollow,noindex">Unicode 。
Unicode碼
Unicode(中文:萬國碼、國際碼、統一碼、單一碼)是電腦科學領域裡的一項業界標準。它對世界上大部分的文字系統進行了整理、編碼,使得計算機可以用更為簡單的方式來呈現和處理文字。
Unicode早期版本中,CJK統一漢字區的範圍是0x4E00-0x9FA5
,包含20902個漢字。後來增加了22個字元,碼位是0x9FA6-0x9FBB
。所以我們在百度判斷中文的
程式碼一般都是判斷在0x4E00-0x9FA5
的範圍內,這是不完全正確的,因為在最新的Unicode 5.0的99089個字元中,有71226個字元與漢字有關,包括了很多相容漢字和擴充漢字,對於多語言版本的APP可能要注意這些。
Unicode 沒有規定字元對應的二進位制碼如何儲存。比如漢字“中”,它的 Unicode 碼點是0x4E2D
,對應的二進位制數是100111000101101
,二進位制數有 15 位,這也就說明了它至少需要 2 個位元組來表示。還有字母“A”,Unicode 碼點是0x0041
,對應的二進位制數是0100 0001
,二進位制數有8位,用1個位元組就可以表示了。
那麼計算機是如何知道1個位元組表示一個字元還是2個位元組表示一個字元呢?也許有人會覺得可以用Unicode 中最大的字元用 4 位元組來表示每一個字元,但是這樣肯定會造成了空間的極大浪費,如果是一個英文文件,那檔案大小就大出了 3 倍多了。為了解決 Unicode 的編碼問題, UTF-8 和 UTF-16 兩種當前比較流行的編碼方式誕生了。
UTF-8
UTF-8 是目前網際網路上使用最廣泛的一種 Unicode 編碼方式,它的最大特點就是可變長。它可以使用 1 - 4 個位元組表示一個字元,根據字元的不同變換長度。編碼規則如下:
- 對於單個位元組的字元,第一位設為 0,後面的 7 位對應這個字元的 Unicode 碼點。因此,對於英文中的 0 - 127 號字元,與 ASCII 碼完全相同。這意味著 ASCII 碼那個年代的文件用 UTF-8 編碼開啟完全沒有問題。
- 對於需要使用 N 個位元組來表示的字元(N > 1),第一個位元組的前 N 位都設為 1,第 N + 1 位設為0,剩餘的 N - 1 個位元組的前兩位都設位 10,剩下的二進位制位則使用這個字元的 Unicode 碼點來填充。
碼點的位數 | 碼點起值 | 碼點終值 | 位元組序列 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
---|---|---|---|---|---|---|---|---|---|
7 | U+0000 | U+007F | 1 | 0xxxxxxx | |||||
11 | U+0080 | U+07FF | 2 | 110xxxxx | 10xxxxxx | ||||
16 | U+0800 | U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | |||
21 | U+10000 | U+1FFFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
26 | U+200000 | U+3FFFFFF | 5 | 111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
31 | U+4000000 | U+7FFFFFFF | 6 | 1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
根據上面編碼規則對照表,就很容易進行 UTF-8 編碼和解碼。以漢字“中”為例,“中”的 Unicode 碼點是0x4E2D
(100 1110 0010 1101
),通過上面的對照表可以發現,0x0000 4E2D
位於第三行的範圍,那麼得出其格式為1110xxxx 10xxxxxx 10xxxxxx
。接著,從“中”的二進位制數最後一位開始,從後向前依次填充對應格式中的 x,多出的 x 用 0 補上。這樣,就得到了“漢”的 UTF-8 編碼為11100100 10111000 10101101
,轉換成十六進位制就是0xE4 0xB8 0xAD
。
解碼的過程也十分簡單:如果一個位元組的第一位是 0 ,則說明這個位元組對應一個字元;如果一個位元組的第一位1,那麼連續有多少個 1,就表示該字元佔用多少個位元組,知道了該字元佔用N個位元組就可以篇歷後面以10開頭的N個字元,最終解析出該字元。
UTF-16
Unicode的編碼空間從U+0000到U+10FFFF,共有2^17
個碼位可用來對映字元. Unicode的編碼空間可以劃分為17個平面(plane),每個平面包含2^16
(65,536)個碼位。17個平面的碼位可表示為從U+xx0000到U+xxFFFF,其中xx表示十六進位制值從0x00
到0x10
,共計17個平面。第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0)。其他平面稱為輔助平面(Supplementary Planes)。
基本平面(BMP)的字元位共有2^16
個,從U+0000到U+FFFF,包含了最常用的字元。其中從 U+D800 到 U+DFFF 是一個空段,即這些碼點不對應任何字元。因此,這個空段可以用來對映輔助平面的字元。
輔助平面(Supplementary Planes)的字元位共有2^20
個,從U+10000到U+10FFFF,因此表示這些字元至少需要 20 個二進位制位。UTF-16 將這 20 個二進位制位分成兩半,前 10 位對映在 U+D800 到 U+DBFF,稱為高位(H),後 10 位對映在 U+DC00 到 U+DFFF,稱為低位(L)。這意味著,一個輔助平面的字元,被拆成兩個基本平面的字元表示。
因此,當我們遇到兩個位元組,發現它的碼位在 U+D800 到 U+DBFF 之間,就可以斷定,緊跟在後面的兩個位元組的碼位,應該在 U+DC00 到 U+DFFF 之間,這四個位元組必須放在一起解讀。
以漢字””為例,漢字””的 Unicode 碼點為0x20BB7
,該碼點顯然超出了基本平面的範圍(0x0000 - 0xFFFF
),因此需要使用四個位元組表示。首先用0x20BB7 - 0x10000 = 0x10BB7
計算出超出的部分,然後將其用 20 個二進位制位表示(不足前面補 0 ),結果為0001000010 1110110111
。接著,將前 10 位對映到 U+D800 到 U+DBFF 之間,後 10 位對映到 U+DC00 到 U+DFFF 即可。前10位是0001000010
,對應十六進位制是0x42
,對映到U+D800後就是0xD842
,同理後10位是1110110111
,對應十六進位制是0x03B7
,對映到U+DC00 後就是0xDFB7
。因此得出漢字””的 UTF-16 編碼為0xD842 0xDFB7
。
程式碼中處理Unicode碼
java預設是使用UTF-16編碼處理字元的,很多字元處理的函式都定義在Character
類中,以上文中的漢字“中”、“”為例,獲取字元的碼位 :
val str = "中" // str.length = 1 val str2 = "" // str2.length = 2 // 獲取碼位 : println(Character.codePointAt(str, 0).toString(16)) // 4E2D println(Character.codePointAt(str2, 0).toString(16)) // 20BB7
列印結果與預想的一致,我們也可以用Character. charCount
來判斷一個碼位佔用的字元數,通過上方分析可知漢字””佔用四個位元組,這裡要注意函式名是charCount
,說明計算的是char的數量,一個char佔兩個位元組,所以””的Character.charCount
列印的是2,而 “中”的Character.charCount
列印的是1。
// 判斷字元字元數 println(Character.charCount(Character.codePointAt("中", 0))) // 1 println(Character.charCount(Character.codePointAt("", 0))) // 2
那麼Character
這個類又是怎樣知道一個字元佔兩個位元組還是四個位元組呢?我們看看Character.charCount
函式的原始碼 :
// Character.java public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000; public static int charCount(int codePoint) { return codePoint >= MIN_SUPPLEMENTARY_CODE_POINT ? 2 : 1; }
從中可以發現 ,當碼位大於等於0x010000
時就是2個char(四個位元組),否則是1個char(兩個位元組),而通過上文分析我們知道碼位大於等於0x010000
就是在輔助平面,這時候是由一個高位和一個低位組合的,在輔助平面上的字元至少佔用四個位元組,我們可以列印漢字””的每一個char :
"中".map { String.format("%x ", it.toInt()).toUpperCase() }.reduce { acc, s -> acc + s }.run { println(this) // 4E2D } "".map { String.format("%x ", it.toInt()).toUpperCase() }.reduce { acc, s -> acc + s }.run { println(this) // D842 DFB7 }
通過列印結果,可知漢字”中”的UTF-16就是0x4E2D
,漢字””的UTF-16就是0xD842 0xDFB7
。另外,Character
也提供了兩個函式來判斷一個字元是高位還是低位,因為“中”由兩個位元組組成,不需要高位低位,所以Character.isHighSurrogate
、Character.isLowSurrogate
均列印false。 :
// 判斷高位、低位 : "中".forEachIndexed { index, c -> println("中[$index] isHighSurrogate =${Character.isHighSurrogate(c)} , isLowSurrogate =${Character.isLowSurrogate(c)}") } "".forEachIndexed { index, c -> println("[$index] isHighSurrogate =${Character.isHighSurrogate(c)} , isLowSurrogate =${Character.isLowSurrogate(c)}") } //列印結果 : //中[0] isHighSurrogate =false , isLowSurrogate =false //[0] isHighSurrogate =true , isLowSurrogate =false //[1] isHighSurrogate =false , isLowSurrogate =true
另外如果我們要把UTF-16編碼轉化成UTF-8編碼,可以這樣做 :
// UTF-16轉化成UTF-8編碼 : "中".toByteArray().map { String.format("%x ", it).toUpperCase() }.reduce { acc, s -> acc + s }.run { println(this) // E4 B8 AD } "".toByteArray().map { String.format("%x ", it).toUpperCase() }.reduce { acc, s -> acc + s }.run { println(this) // F0 A0 AE B7 }
通過列印結果,可知漢字””的UTF-8就是0xE4 0xB8 0xAD
,漢字””的UTF-8就是0xF0 0xA0 0xAE 0xB7
。同理把UTF-8編碼也可以轉化成UTF-16編碼,如下所示:
// UTF-8轉化成UTF-16編碼 : val bytes1 = byteArrayOf(0xE4.toByte(), 0xB8.toByte(), 0xAD.toByte()) println(String(bytes1, Charset.forName("utf-8"))) // "中" val bytes2 = byteArrayOf(0xF0.toByte(), 0xA0.toByte(), 0xAE.toByte(), 0xB7.toByte()) println(String(bytes2, Charset.forName("utf-8"))) // ""
我們可以直接用\u
的字首來顯示一個UTF-16編碼,從而直接列印我們所要的字元,比如漢字”中”的UTF-16就是0x4E2D
,漢字””的UTF-16就是0xD842 0xDFB7
,我們可以直接用\u4E2D
來列印“中”,用\uD842\uDFB7
來列印“”,程式碼如下:
println("\u4E2D") // "中" println("\uD842\uDFB7") // ""