關於Unicode,字符集,字元編碼,每個程式設計師都應該知道的事

技術從未如此性感發表於2018-06-14

基本概念

字元[character]

字元代表了字母表中的字元,標點符號和其他的一些符號。在計算機中,文字是由字元組成的。

字符集合[character set]

由一套用於特定用途的字元組成,例如支援西歐語言的字符集合,支援中文的字符集合。字符集合只定義了符號和他們的語意,其實跟計算機沒有直接關係。

現實生活中,不同的語系有自己的字符集合,例如藏文有自己的字符集合,漢文有自己的字符集合。到計算機的世界中,也有各種字符集合,例如ASCII字符集合GB2312字符集合GBK字符集合。還有一個其他字符集合的超集--Unicode字符集定義了幾乎絕大部分現存語言需要的字元,是一種通用的字符集,來支援多語言環境(可以同時處理多種語言混合的情況)。各個國家和地區在制定編碼標準的時候,“字符集合”和“字元編碼”一般都是同時制定的。所以像ASCII字符集合一樣,它也同時代表了一種字元的編碼。

字元編碼[character encoding]

是一套規則,定義了在計算機記憶體中如何表示字元,是字符集中的每個字元與計算機記憶體中位元組之間的轉換關係,也可以認為是把字元數字化,規定每個“字元”分別用一個位元組還是多個位元組儲存,用哪些位元組來儲存。例如ASCII編碼[你沒看錯,它既是一種字符集合,也是一種字元編碼],定義了英文字母和符號在計算機中的表示方式,是用一個位元組來表示。Unicode字符集合,有好幾種字元編碼方式,例如變長度編碼的UTF8UTF16等。中文字符集也有很多字元編碼,例如上文提到的GB2312編碼,GBK編碼等。

知乎上的這篇介紹字元編碼,字型,iconv的文章很贊,內容淺顯易懂。還有一篇很有名的有關Unicode和字符集的文章可以看看:The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!),網上有中文版。

UCS和ISO 10646標準

ISO 10646標準定義了通用字符集UCS[Universal Character Set],是其他所有字符集合的超集。它保證了和其他字符集合之間可以來回轉換,不會丟失資訊。

UCS不僅給每個字元做了編碼,而且還定義了一個官方的名稱。用來表示一個UCS或者Unicode的十六進位制數字通常是用"U+"來作為字首的,例如用"U+0041"來表示拉丁文中的大寫字母A。

UCS[Universial Character Set]和Unicode的關係

簡單粗暴的總結一下,就是兩撥人搞的同一套標準。具體經過如下:

在1980年代後期,有獨立的兩撥人想建立一個通用的字符集合。一個是國際化標準組織ISO[Internaltional Organization for Standardization],另外一個是最初成員大部分是美國多語言軟體服務提供商的財團發起的Unicode專案。幸運的是在1991年左右,兩個專案的成員都意識到世界不需要兩個統一的字符集。於是他們一起合作制定了一個字元表。雖然兩個專案至今仍然存在並獨立釋出各自的標準,但是Unicode財團和國際化標準組織都已經同意會讓Unicode和ISO 10646標準互相相容並會在未來緊密協作。具體兩者之間的區別,見這裡

什麼是UTF8

Unicode/UCS只是字符集合,雖然為每個字元分配了一個唯一的整數值,但具體怎麼用位元組來表示每個字元,是由字元編碼決定的。Unicode的字元編碼方式有UTF-8, UTF-16, UTF-32。由於UTF-16和UTF-32編碼中包含"\0",或者"/"這樣對於檔名和其他C語言庫函式來說具有特殊意義的字元,所以不適合在Unix下用來做檔名稱,文字檔案和環境變數的Unicode編碼。UTF-8沒有這樣的問題,它有很多優點:可以向前相容ASCII碼,是變長的編碼,由於編碼沒有狀態,所以很容易重新同步,在傳輸過程中丟失了一些位元組後,具有魯棒性。

POSIX語系[locale]機制

語系[locale]就是軟體執行時的語言環境,它是語言和文化規則的一個集合,包含字元編碼,日期/時間的表示方式,字元排序的規則等。語系的名稱通常是由ISO 639-1規定的語言[language]和ISO 3166-1規定的國家程式碼[country code]以及額外的字元編碼名稱[character encoding]共同組成,例如zh_TW.UTF-8語系,zh代表語言是漢語,TW是臺灣地區,UTF-8是字元編碼。而zh_CN.GBK中,CN是指中國大陸地區,採用GBK編碼。

Linux下語系由幾個類別的環境變數組成,指定了在軟體中跟語言慣例相關的行為資訊。例如LC_CTYPE決定字元編碼方式,LC_COLLATE決定字元排序的規則。LANG環境變數用來設定所有類別的預設語系,但是LC_*這些變數能夠覆蓋每個單獨的類別。

理解了上述概念,咋們就可以去實踐一下了。

實戰

C語言對Unicode和UTF-8的支援

多位元組字元和寬字元

C語言中用單獨的一個char型別的變數是無法唯一地表示像漢語這樣的自然語言的。C語言標準支援兩種不同的方式來處理擴充套件的自然語言編碼方式:寬字元[wide characters]和多位元組字元[multibyte characters]。

  1. 寬字元是一種內部表示方式,每個字元是用一個單獨的wchar_t型別來表示的。
  2. 多位元組字元是用來做輸入和輸出的,每個字元用C語言中char型別的序列來表示。所以每個字元會用一個或多個(最多MB_LEN_MAX)位元組來表示

wchar_t這種型別是從GNU glibc 2.2開始引入的,目的是在執行時用單個的物件來表示字元,跟當前使用的語系無關。ISO C99標準要求通過巨集__STDC_ISO_10646__來告訴程式支援wchar_t型別,並且保證所有的寬字元處理函式都會把寬字元當作Unicode字元。C語言中處理寬字元的函式多數是在處理char型別字元的函式名基礎上,新增了"w"或者是把"str"替換成"wcs",例如wprintf(),wscpy()等。字串常量之前新增L字首就可以告訴讓編譯器用wchar_t型別來儲存字串常量,例如printf("%ls\n", L"Schöne Grüße"),如果用寬字元來表示字串,此時的字串長度就是以wchar_t為單位的,而不是位元組;

2011版的C和C++標準都各自引入了固定大小的字元型別char16_tchar32_t來明確提供16位和32位Unicode編碼格式,讓wchar_t成為實現相關的型別。ISO 10646:2003 Unicode 4.0標準說:

wchar_t型別的寬度是由編譯器指定的,可以小到只有8位。因此對於需要在C或C++編譯器之間可移植的程式不應該使用wchar_t來儲存Unicode文字。wchar_t型別的目的是儲存編譯器定義的寬字元,有可能不是用Unicode編碼的。

多位元組字元的字元編碼方式,是由當前系統的語系[locale]來決定的,例如當前語系中字元編碼是UTF-8,那麼多位元組字元編碼就是UTF-8。因此語系也控制著寬字元和多位元組之間的轉換。

glibc2.2及更高版本完整地實現ISO C語言多位元組轉換函式(mbsrtowcs(), wcsrtomb()等)。這些函式用來在wchar_t和任何語系相關的多位元組編碼,包括UTF-8,ISO 8859-1等之間進行轉化。

建議是使用這些函式中可重啟動的[restartable,函式名中有字母r],是多執行緒安全的函式,例如wcsrtombs()mbsrtowcs()
使用這些函式的好處是:

  • 是跟廠商無關的標準
  • 函式會根據使用者的語系做正確的事情。程式需要做的是在程式開頭呼叫setlocale(LC_ALL, "")來根據環境變數來設定使用者語系

例如可以寫出如下程式碼:

#include <stdio.h>
#include <locale.h>

int main()
{
    if (!setlocale(LC_CTYPE, "")) {
        fprintf(stderr, "Can't set the specified locale! "
          "Check LANG, LC_CTYPE, LC_ALL.\n");
        return 1;
    }
    printf("%ls\n", L"Schöne Grüße");
    return 0;
} 

setlocale(LC_CTYPE, "")函式,會依次測試環境變數 LC_ALLLC_CTYPE和 LANG的值,如果有值,就用這個值來決定用哪個語系資料來載入LC_CTYPE這個分類(控制著多位元組轉換的函式)。

printf中的%ls格式說明符是用來指定把寬字元形式的字串引數轉化成由語系決定的多位元組編碼來輸出。printf函式是不知道輸出的字元的編碼方式的,它會把傳給它的位元組原封不動地輸出出去。在顯示的時候,作業系統會根據當前的語系來將這些位元組解碼到對應的字元,所以只有當傳給printf的字元編碼方式和使用者環境變數指定的字元編碼方式相同,用printf列印出的字元才不會亂碼。

使用這些函式的壞處:

  • 有些函式是非執行緒安全的,因為兩次函式呼叫之間有隱藏的內部狀態
  • 不能同時支援多種語系或編碼方式

通過上述的分析可以看到,如果全部都使用C語言庫中多位元組的函式來進行外部字元編碼和程式內部使用的wchar_t型別之間的轉換,那麼C語言庫會根據環境變數LC_CTYPE的值來選擇正確的字元編碼,你的程式甚至不用顯示地知道當前多位元組編碼是什麼。

然而,有一些情況下你可能不會全部都用C語言庫中的多位元組函式,此時程式不得不知道當前語系是什麼。此時需要首先在程式開始處呼叫setlocale(LC_TYPE, ""函式來根據環境變數設定語系。之後利用函式nl_langinfo(CODESET)函式來獲得當前語系指定的字元編碼的名稱。

C語言如何書寫採用了某種字元編碼的字串常量

對於一坨位元組資料來說,字元編碼就相當於是有色眼鏡一樣,我們可以戴上UTF-8編碼的眼鏡去解讀這片位元組資料,也可以戴上GBK編碼的眼鏡去解讀它。只有當我們採用了跟寫入時的編碼一致的編碼去解讀,才能讀取出有意義的字串,否則可能就是亂碼了。

轉義序列

轉義序列[escape sequences]:轉義是以多個字元的有序組合來表示原本很難直接表示出來的字元的技術。轉義序列指在轉義時使用的有序字元組合。
需要了解C語言中如下的幾個轉義方式:
'\798':值為十進位制值798的字元
'\x7D':值為十六進位制7D的字元
'\u0041':代表字元名稱中名為U+0041的這個Unicode字元,可能最終編譯器會用幾個位元組來儲存這個字元。這種方式只有C99以後才支援。由編譯器來決定具體用什麼方式儲存。

有了這幾個轉義字元這樣就很容易書寫出特定編碼的字串了,例如"我是Jack47",採用各種編碼形式的值如下:
char gbk_name[] = "\xced2\xcac7Jack47";
char unicode_name[] = "\u6211\u662FJack47"
char utf8_name[] = "\xe6\x88\x91\xe6\x98\xafJack47"

上述的這種方式,是直接把編碼後的位元組寫入到了陣列裡,是一種"硬編碼"[hard code]的方式。

知道了上述的知識後,問題就來了,當前軟體要支援UTF8,要如何修改?

如何修改軟體來支援UTF8

有兩種辦法,可以這樣劃分:

1.  軟轉換:資料在所有地方都是以UTF-8的形式儲存的。
2.  硬轉換:程式讀取的輸入是UTF-8資料,在程式內部轉換成寬字元後進行處理,只有在最終輸出的時候轉換成UTF-8編碼。在內部一個字元是一個固定大小的記憶體物件。

也可以這樣劃分:

1. 硬編碼的方法
把UTF-8相關的資訊硬編碼到程式中。這樣能夠在某些場景下顯著提高程式執行效率。這或許是那些只需要支援ASCII和UTF-8編碼的程式的最好辦法。

2. 取決於語系的方法
C語言提供了可以處理任意特定語系,採用多位元組編碼的字串的處理函式。依賴於這些函式的程式設計師可以不用感知到UTF-8編碼的實際細節。通過僅僅改變語系設定,就可以自動支援其他的多位元組編碼(例如EUC)。

如果使用了UTF-8或者其他類似的多位元組編碼,需要程式設計師清楚地區分以下概念:

1. 位元組[Byte]
2. 字元[Character]
3. 顯示時候的寬度

如何在不同編碼間轉換

可以使用iconv函式在兩個不同的編碼之間進行轉換,例如從GBK編碼轉換到UTF-8編碼。

Java與Unicode

Java語言內部使用的就是Unicode編碼。char型別表示一個Unicode字元[這是跟C語言不一樣的地方],java.lang.String類表示一個從Unicode字元構建的字串。

java.io.DataInputjava.io.DataOutput介面分別有叫做readUTFwriteUTF的方法。但記住他們使用的不是UTF-8;他們用的是修改後的UTF-8編碼:NUL字元不是用一個位元組的0x00來表示,而是用兩個位元組的0xC0 0x80來表示的,在最後新增一個位元組的0x00。這樣編碼,字串包含NUL字元而不需要增加表示字串長度的字首欄位--這樣C語言<string.h>中定義的strlen()strcpy這些函式就可以用來操作這些資料了。

一些練習

  1. 如何處理輸入的中文引數,例如中文引數的字元個數列印出來?
  2. 在json串中遇到了這樣的字串,是什麼意思呢?"\u82f9\u679c\u624b\u673a"

參考資料

  1. UTF-8 and Unicode FAQ for Unix/Linux

在POSIX系統上(Linux, Unix)如何使用Unicode/UTF-8的一站式資訊的文章,內容豐富,比較長,可以挑著看。

  1. C語言中轉義字元

  2. C語言中的反斜線轉義符

  3. FreeBSD 多位元組操作

  4. Making your programs Unicode aware

  5. 中文編碼雜談




相關文章