字元編碼常識及問題解析

SHARECORE的部落格發表於2014-09-02

在面試的筆試題裡出了一道開放性的題:請簡述Unicode與UTF-8之間的關係。一道看似簡單的題,能給出滿意答案的卻寥寥無幾 ,確實挺失望的。所以今天就結合我以前做過的一個關於字元編碼的分享,總結一些與字元編碼相關的知識和問題。如果你這方面的知識已經掌握的足夠了,可以忽略這篇文字。但如果你沒法很好的回答我上面的面試題,或經常被亂碼的問題所困擾,還是不妨一讀。

基本常識

1.位和位元組

說起編碼,我們必須從最基礎的說起,位和位元組(別覺得這個過於簡單不值一說,我還真見過很多個不能區分這兩者的程式設計師)。位(bit)是指計算機裡存放的二進位制值(0/1),而8個位組合成的“位串”稱為一個位元組,容易算出,8個位的組合有256( 28 )個組合方式,其取值範圍是“00000000-11111111”,常用十六進位制來表示。比如“01000001”就是一個位元組,其對應的十六進位制值為“0x41”。

而我們通常所講的字元編碼,就是指定義一套規則,將真實世界裡的字母/字元與計算機的二進位制序列進行相互轉化。如我們可以針對上面的位元組定義如下的轉換規則:

即用字位序“01000001”來表示字母’A’。

2.拉丁字元

拉丁字元是當今世界使用最廣泛的符號了。通常我們說的拉丁字母,指的的是基礎拉丁字母,即指常見的”ABCD“等26個英文字母,這些字母與英語中一些常見的符號(如數字,標點符號)稱為基礎拉丁字元,這些基礎拉丁字元在使用英語的國家廣為流行,當然在中國,也被用來當作漢語拼音使用。在歐洲其它一些非英語國家,為滿足其語言需要,在基礎拉丁字元的基礎上,加上一些連字元,變音字元(如’Á’),形成了派生拉丁字母,其表示的字元範圍在各種語言有所不同,而完整意義上的拉丁字元是指這些變體字元與基礎拉丁字元的全集。是比基礎拉丁字符集大很多的一個集合。

編碼標準

前文提到,字元編碼是一套規則。既然是規則,就必須有標準。下面我就仔細說說常見的字元編碼標準。

1.拉丁編碼

ASCII的全稱是American Standard Code for Information Interchange(美國資訊交換標準程式碼)。顧名思義,這是現代計算機的發明國美國人設計的標準,而美國是一個英語國家,他們設定的ASCII編碼也只支援基礎拉丁字元。ASCII的設計也很簡單,用一個位元組(8個位)來表示一個字元,並保證最高位的取值永遠為’0’。即表示字元含義的位數為7位,不難算出其可表達字元數為27 =128個。這128個字元包括95個可列印的字元(涵蓋了26個英文字母的大小寫以及英文標點符號能)與33個控制字元(不可列印字元)。例如下表,就是幾個簡單的規則對應:

字元型別 字元 二進位制 16進位制 10進位制
可列印字元 A 01000001 0x41 65
可列印字元 a 01100001 0x61 97
控制字元 \r 00001101 0x0D 13
控制字元 \n 00001010 0xA 10

前面說到了,ASCII是美國人設計的,只能支援基礎拉丁字元,而當計算機發展到歐洲,歐洲其它不只是用的基礎拉丁字元的國家(即用更大的派生拉丁字符集)該怎麼辦呢?

當然,最簡單的辦法就是將美國人沒有用到的第8位也用上就好了,這樣能表達的字元個數就達到了28 =256個,相比較原來,增長了一倍, 這個編碼規則也常被稱為EASCII。EASCII基本解決了整個西歐的字元編碼問題。但是對於歐洲其它地方如北歐,東歐地區,256個字元還是不夠用,如是出現了ISO 8859,為解決256個字元不夠用的問題,ISO 8859採取的不再是單個獨立的編碼規則,而是由一系列的字符集(共15個)所組成,分別稱為ISO 8859-n(n=1,2,3…11,13…16,沒有12)。其每個字符集對應不同的語言,如ISO 8859-1對應西歐語言,ISO 8859-2對應中歐語言等。其中大家所熟悉的Latin-1就是ISO 8859-1的別名,它表示整個西歐的字符集範圍。 需要注意的一點的是,ISO 8859-n與ASCII是相容的,即其0000000(0x00)-01111111(0x7f)範圍段與ASCII保持一致,而10000000(0x80)-11111111(0xFF)範圍段被擴充套件用到不同的字符集。

2.中文編碼

以上我們接觸到的拉丁編碼,都是單位元組編碼,即用一個位元組來對應一個字元。但這一規則對於其它字符集更大的語言來說,並不適應,比如中文,而是出現了用多個位元組表示一個字元的編碼規則。常見的中文GB2312(國家簡體中文字符集)就是用兩個位元組來表示一個漢字(注意是表示一個漢字,對於拉丁字母,GB2312還是是用一個位元組來表示以相容ASCII)。我們用下表來說明各中文編碼之間的規則和相容性。

對於中文編碼,其規則實現上是很簡單的,一般都是簡單的字元查表即可,重要的是要注意其相互之間的相容性問題。如如果選擇BIG5字符集編碼,就不能很好的相容GB2312,當做繁轉簡時有可能導致個別字的衝突與不一致,但是GBK與GB2312之間就不存在這樣的問題。

3.Unicode

以上可以看到,針對不同的語言採用不同的編碼,有可能導致衝突與不相容性,如果我們開啟一份位元組序檔案,如果不知道其編碼規則,就無法正確解析其語義,這也是產生亂碼的根本原因。有沒有一種規則是全世界字元統一的呢?當然有,Unicode就是一種。為了能獨立表示世界上所有的字元,Unicode採用4個位元組表示一個字元,這樣理論上Unicode能表示的字元數就達到了231 = 2147483648 = 21 億左右個字元,完全可以涵蓋世界上一切語言所用的符號。我們以漢字”微信“兩字舉例說明:

  • 微 <-> \u5fae <-> 00000000 00000000 01011111 10101110
  • 信 <-> \u4fe1 <-> 00000000 00000000 01001111 11100001

容易從上面的例子裡看出,Unicode對所有的字元編碼均需要四個位元組,而這對於拉丁字母或漢字來說是浪費的,其前面三個或兩個位元組均是0,這對資訊儲存來說是極大的浪費。另外一個問題就是,如何區分Unicode與其它編碼這也是一個問題,比如計算機怎麼知道四個位元組表示一個Unicode中的字元,還是分別表示四個ASCII的字元呢?

以上兩個問題,困擾著Unicode,讓Unicode的推廣上一直面臨著困難。直至UTF-8作為Unicode的一種實現後,部分問題得到解決,才得以完成推廣使用。說到此,我們可以回答文章一開始提出的問題了,UTF-8是Unicode的一種實現方式,而Unicode是一個統一標準規範,Unicode的實現方式除了UTF-8還有其它的,比如UTF-16等。

話說當初大牛Ben Thomson吃飯時,在一張餐巾紙上,設計出了UTF-8,然後回到房間,實現了第一版的UTF-8。關於UTF-8的基本規則,其實簡單來說就兩條(來自阮一峰老師的總結):

  • 規則1:對於單位元組字元,位元組的第一位為0,後7位為這個符號的Unicode碼,所以對於拉丁字母,UTF-8與ASCII碼是一致的。
  • 規則2:對於n位元組(n>1)的字元,第一個位元組前n位都設為1,第n+1位為0,後面位元組的前兩位一律設為10,剩下沒有提及的位,全部為這個符號的Unicode編碼。

通過,根據以上規則,可以建立一個Unicode取值範圍與UTF-8位元組序表示的對應關係,如下表,

舉例來說,’微’的Unicode是’\u5fae’,二進位制表示是”00000000 00000000 01011111 10101110“,其取值就位於’0000 0800-0000 FFFF’之間,所以其UTF-8編碼為’11100101 10111110 10101110’ (加粗部分為固定編碼內容)。

通過以上簡單規則,UTF-8採取變位元組的方式,解決了我們前文提到的關於Unicode的兩大問題。同時,作為中文使用者需要注意的一點是Unicode(UTF-8)與GBK,GB2312這些漢字編碼規則是完全不相容的,也就是說這兩者之間不能通過任何演算法來進行轉換,如需轉換,一般通過GBK查表的方式來進行

常見問題及解答

1.windows Notepad中的編碼ANSI儲存選項,代表什麼含義?

ANSI是windows的預設的編碼方式,對於英文檔案是ASCII編碼,對於簡體中文檔案是GB2312編碼(只針對Windows簡體中文版,如果是繁體中文版會採用Big5碼)。所以,如果將一個UTF-8編碼的檔案,另存為ANSI的方式,對於中文部分會產生亂碼

2.什麼是UTF-8的BOM?

BOM的全稱是Byte Order Mark,BOM是微軟給UTF-8編碼加上的,用於標識檔案使用的是UTF-8編碼,即在UTF-8編碼的檔案起始位置,加入三個位元組“EE BB BF”。這是微軟特有的,標準並不推薦包含BOM的方式。採用加BOM的UTF-8編碼檔案,對於一些只支援標準UTF-8編碼的環境,可能導致問題。比如,在Go語言程式設計中,對於包含BOM的程式碼檔案,會導致編譯出錯。詳細可見我的這篇文章

3.為什麼資料庫Latin1字符集(單位元組)可以儲存中文呢?

其實不管需要使用幾個位元組來表示一個字元,但最小的儲存單位都是位元組,所以,只要能保證傳輸和儲存的位元組順序不會亂即可。作為資料庫,只是作為儲存的使用的話,只要能保證儲存的順序與寫入的順序一致,然後再按相同的位元組順序讀出即可,翻譯成語義字元的任務交給應用程式。比如’微’的UTF-8編碼是’0xE5 0xBE 0xAE’,那資料庫也儲存’0xE5 0xBE 0xAE’三個位元組,其它應用按順序從資料庫讀取,再按UTF-8編碼進行展現。這當然是一個看似完美的方案,但是隻要寫入,儲存,讀取過程中岔出任何別的編碼,都可能導致亂碼。

4.Mysql資料庫中多個字符集變數(其它資料庫其實也類似),它們之間分別是什麼關係?

我們分別解釋:

character_set_client:客戶端來源的資料使用的字符集,用於客戶端顯式告訴客戶端所傳送的語句中的的字元編碼。

character_set_connection:連線層的字元編碼,mysql一般用character_set_connection將客戶端的字元轉換為連線層表示的字元。

character_set_results:查詢結果從資料庫讀出後,將轉換為character_set_results返回給前端。

而我們常見的解決亂碼問題的操作:

其相當於將以上三個字符集統一全部設定為GBK,這三者一致時,一般就解決了亂碼問題。

character_set_database:當前選中資料庫的預設字符集,如當create table時沒有指定字符集,將預設選擇該字符集。

character_set_database已經character_set_system,一般用於資料庫系統內部的一些字元編碼,處理資料亂碼問題時,我們基本可以忽略。

5.什麼情況下,表示資訊丟失?

對於mysql資料庫,我們可以通過hex(colname)函式(其它資料庫也有類似的函式,一些文字檔案編輯器也具有這個功能),檢視實際儲存的位元組內容,如:

通過檢視儲存的位元組序,我們可以從根本上了解儲存的內容是什麼編碼了。而當發現儲存的內容全部是’3F’時,就表明儲存的內容由於編碼問題,資訊已經丟失了,無法再找回

之所以出現這種資訊丟失的情況,一般是將不能相互轉換的字符集之間做了轉換,比如我們在前文說到,UTF-8只能一個個位元組地變成Latin-1,但是根本不能轉換的,因為兩者之間沒有轉換規則,Unicode的字元對應範圍也根本不在Latin-1範圍內,所以只能用’?(0x3F)’代替了。

總結:

本文從基礎知識與實際中碰到的問題上,解析了字元編碼相關內容。而之所以要從頭介紹字元編碼的基礎知識,是為了更好的從原理上了解與解決日常碰到的編碼問題,只有從根本上了解了不同字符集的規則及其之間的關係與相容性,才能更好的解決碰到的亂碼問題,也能避免由於程式中不正確的編碼轉換導致的資訊丟失問題。

相關文章