原文出處: Justin Huang 的博客(@Justin_Programer)
在面試的筆試題里出了一道開放性的題:請簡述Unicode與UTF-8之間的關(guān)系韵丑。一道看似簡單的題青伤,能給出滿意答案的卻寥寥無幾 汤纸,確實(shí)挺失望的傻粘。所以今天就結(jié)合我以前做過的一個(gè)關(guān)于字符編碼的分享,總結(jié)一些與字符編碼相關(guān)的知識和問題南片。如果你這方面的知識已經(jīng)掌握的足夠了赏僧,可以忽略這篇文字。但如果你沒法很好的回答我上面的面試題历涝,或經(jīng)常被亂碼的問題所困擾诅需,還是不妨一讀。
基本常識
1.位和字節(jié)
說起編碼荧库,我們必須從最基礎(chǔ)的說起堰塌,位和字節(jié)(別覺得這個(gè)過于簡單不值一說,我還真見過很多個(gè)不能區(qū)分這兩者的程序員)分衫。位(bit)是指計(jì)算機(jī)里存放的二進(jìn)制值(0/1)蔫仙,而8個(gè)位組合成的“位串”稱為一個(gè)字節(jié),容易算出丐箩,8個(gè)位的組合有256( 28 )個(gè)組合方式摇邦,其取值范圍是“00000000-11111111”,常用十六進(jìn)制來表示屎勘。比如“01000001”就是一個(gè)字節(jié)施籍,其對應(yīng)的十六進(jìn)制值為“0x41”。
而我們通常所講的字符編碼概漱,就是指定義一套規(guī)則丑慎,將真實(shí)世界里的字母/字符與計(jì)算機(jī)的二進(jìn)制序列進(jìn)行相互轉(zhuǎn)化。如我們可以針對上面的字節(jié)定義如下的轉(zhuǎn)換規(guī)則:
01000001(0x41)<-> 65 <-> 'A'
即用字位序“01000001”來表示字母’A’瓤摧。
2.拉丁字符
拉丁字符是當(dāng)今世界使用最廣泛的符號了竿裂。通常我們說的拉丁字母,指的的是基礎(chǔ)拉丁字母,即指常見的”ABCD“等26個(gè)英文字母照弥,這些字母與英語中一些常見的符號(如數(shù)字腻异,標(biāo)點(diǎn)符號)稱為基礎(chǔ)拉丁字符,這些基礎(chǔ)拉丁字符在使用英語的國家廣為流行这揣,當(dāng)然在中國悔常,也被用來當(dāng)作漢語拼音使用影斑。在歐洲其它一些非英語國家,為滿足其語言需要机打,在基礎(chǔ)拉丁字符的基礎(chǔ)上矫户,加上一些連字符,變音字符(如’á’)残邀,形成了派生拉丁字母皆辽,其表示的字符范圍在各種語言有所不同,而完整意義上的拉丁字符是指這些變體字符與基礎(chǔ)拉丁字符的全集芥挣。是比基礎(chǔ)拉丁字符集大很多的一個(gè)集合驱闷。
編碼標(biāo)準(zhǔn)
前文提到,字符編碼是一套規(guī)則九秀。既然是規(guī)則,就必須有標(biāo)準(zhǔn)粘我。下面我就仔細(xì)說說常見的字符編碼標(biāo)準(zhǔn)鼓蜒。
1.拉丁編碼
ASCII的全稱是American Standard Code for Information Interchange(美國信息交換標(biāo)準(zhǔn)代碼)。顧名思義征字,這是現(xiàn)代計(jì)算機(jī)的發(fā)明國美國人設(shè)計(jì)的標(biāo)準(zhǔn)都弹,而美國是一個(gè)英語國家,他們設(shè)定的ASCII編碼也只支持基礎(chǔ)拉丁字符匙姜。ASCII的設(shè)計(jì)也很簡單畅厢,用一個(gè)字節(jié)(8個(gè)位)來表示一個(gè)字符,并保證最高位的取值永遠(yuǎn)為’0’氮昧。即表示字符含義的位數(shù)為7位框杜,不難算出其可表達(dá)字符數(shù)為27 =128個(gè)。這128個(gè)字符包括95個(gè)可打印的字符(涵蓋了26個(gè)英文字母的大小寫以及英文標(biāo)點(diǎn)符號能)與33個(gè)控制字符(不可打印字符)袖肥。例如下表咪辱,就是幾個(gè)簡單的規(guī)則對應(yīng):
字符類型 | 字符 | 二進(jìn)制 | 16進(jìn)制 | 10進(jìn)制 |
---|---|---|---|---|
可打印字符 | A | 01000001 | 0x41 | 65 |
可打印字符 | a | 01100001 | 0x61 | 97 |
控制字符 | \r | 00001101 | 0x0D | 13 |
控制字符 | \n | 00001010 | 0xA | 10 |
前面說到了,ASCII是美國人設(shè)計(jì)的椎组,只能支持基礎(chǔ)拉丁字符油狂,而當(dāng)計(jì)算機(jī)發(fā)展到歐洲,歐洲其它不只是用的基礎(chǔ)拉丁字符的國家(即用更大的派生拉丁字符集)該怎么辦呢寸癌?
當(dāng)然专筷,最簡單的辦法就是將美國人沒有用到的第8位也用上就好了,這樣能表達(dá)的字符個(gè)數(shù)就達(dá)到了28 =256個(gè)蒸苇,相比較原來磷蛹,增長了一倍, 這個(gè)編碼規(guī)則也常被稱為EASCII溪烤。EASCII基本解決了整個(gè)西歐的字符編碼問題弦聂。但是對于歐洲其它地方如北歐鸟辅,東歐地區(qū),256個(gè)字符還是不夠用莺葫,如是出現(xiàn)了ISO 8859,為解決256個(gè)字符不夠用的問題匪凉,ISO 8859采取的不再是單個(gè)獨(dú)立的編碼規(guī)則,而是由一系列的字符集(共15個(gè))所組成捺檬,分別稱為ISO 8859-n(n=1,2,3…11,13…16,沒有12)再层。其每個(gè)字符集對應(yīng)不同的語言,如ISO 8859-1對應(yīng)西歐語言,ISO 8859-2對應(yīng)中歐語言等堡纬。其中大家所熟悉的Latin-1就是ISO 8859-1的別名,它表示整個(gè)西歐的字符集范圍聂受。 需要注意的一點(diǎn)的是,ISO 8859-n與ASCII是兼容的烤镐,即其0000000(0x00)-01111111(0x7f)范圍段與ASCII保持一致蛋济,而10000000(0x80)-11111111(0xFF)范圍段被擴(kuò)展用到不同的字符集。
2.中文編碼
以上我們接觸到的拉丁編碼炮叶,都是單字節(jié)編碼碗旅,即用一個(gè)字節(jié)來對應(yīng)一個(gè)字符。但這一規(guī)則對于其它字符集更大的語言來說镜悉,并不適應(yīng)祟辟,比如中文,而是出現(xiàn)了用多個(gè)字節(jié)表示一個(gè)字符的編碼規(guī)則侣肄。常見的中文GB2312(國家簡體中文字符集)就是用兩個(gè)字節(jié)來表示一個(gè)漢字(注意是表示一個(gè)漢字旧困,對于拉丁字母,GB2312還是是用一個(gè)字節(jié)來表示以兼容ASCII)稼锅。我們用下表來說明各中文編碼之間的規(guī)則和兼容性吼具。
對于中文編碼,其規(guī)則實(shí)現(xiàn)上是很簡單的矩距,一般都是簡單的字符查表即可馍悟,重要的是要注意其相互之間的兼容性問題。如如果選擇BIG5字符集編碼剩晴,就不能很好的兼容GB2312锣咒,當(dāng)做繁轉(zhuǎn)簡時(shí)有可能導(dǎo)致個(gè)別字的沖突與不一致,但是GBK與GB2312之間就不存在這樣的問題赞弥。
3.Unicode
以上可以看到毅整,針對不同的語言采用不同的編碼,有可能導(dǎo)致沖突與不兼容性绽左,如果我們打開一份字節(jié)序文件悼嫉,如果不知道其編碼規(guī)則,就無法正確解析其語義拼窥,這也是產(chǎn)生亂碼的根本原因戏蔑。有沒有一種規(guī)則是全世界字符統(tǒng)一的呢蹋凝?當(dāng)然有,Unicode就是一種总棵。為了能獨(dú)立表示世界上所有的字符鳍寂,Unicode采用4個(gè)字節(jié)表示一個(gè)字符,這樣理論上Unicode能表示的字符數(shù)就達(dá)到了231 = 2147483648 = 21 億左右個(gè)字符,完全可以涵蓋世界上一切語言所用的符號情龄。我們以漢字”微信“兩字舉例說明:
- 微 <-> \u5fae <-> 00000000 00000000 01011111 10101110
- 信 <-> \u4fe1 <-> 00000000 00000000 01001111 11100001
容易從上面的例子里看出迄汛,Unicode對所有的字符編碼均需要四個(gè)字節(jié),而這對于拉丁字母或漢字來說是浪費(fèi)的骤视,其前面三個(gè)或兩個(gè)字節(jié)均是0,這對信息存儲來說是極大的浪費(fèi)鞍爱。另外一個(gè)問題就是,如何區(qū)分Unicode與其它編碼這也是一個(gè)問題专酗,比如計(jì)算機(jī)怎么知道四個(gè)字節(jié)表示一個(gè)Unicode中的字符睹逃,還是分別表示四個(gè)ASCII的字符呢?
以上兩個(gè)問題祷肯,困擾著Unicode沉填,讓Unicode的推廣上一直面臨著困難。直至UTF-8作為Unicode的一種實(shí)現(xiàn)后躬柬,部分問題得到解決拜轨,才得以完成推廣使用抽减。說到此允青,我們可以回答文章一開始提出的問題了,UTF-8是Unicode的一種實(shí)現(xiàn)方式卵沉,而Unicode是一個(gè)統(tǒng)一標(biāo)準(zhǔn)規(guī)范颠锉,Unicode的實(shí)現(xiàn)方式除了UTF-8還有其它的,比如UTF-16等史汗。
話說當(dāng)初大牛Ben Thomson吃飯時(shí)琼掠,在一張餐巾紙上,設(shè)計(jì)出了UTF-8停撞,然后回到房間瓷蛙,實(shí)現(xiàn)了第一版的UTF-8。關(guān)于UTF-8的基本規(guī)則戈毒,其實(shí)簡單來說就兩條(來自阮一峰老師的總結(jié)):
規(guī)則1:對于單字節(jié)字符艰猬,字節(jié)的第一位為0,后7位為這個(gè)符號的Unicode碼埋市,所以對于拉丁字母冠桃,UTF-8與ASCII碼是一致的。
規(guī)則2:對于n字節(jié)(n>1)的字符道宅,第一個(gè)字節(jié)前n位都設(shè)為1食听,第n+1位為0胸蛛,后面字節(jié)的前兩位一律設(shè)為10,剩下沒有提及的位樱报,全部為這個(gè)符號的Unicode編碼葬项。
通過,根據(jù)以上規(guī)則肃弟,可以建立一個(gè)Unicode取值范圍與UTF-8字節(jié)序表示的對應(yīng)關(guān)系玷室,如下表,
舉例來說笤受,’微’的Unicode是’\u5fae’穷缤,二進(jìn)制表示是”00000000 00000000 01011111 10101110“,其取值就位于’0000 0800-0000 FFFF’之間箩兽,所以其UTF-8編碼為’11100101 10111110 10101110’ (加粗部分為固定編碼內(nèi)容)津肛。
通過以上簡單規(guī)則,UTF-8采取變字節(jié)的方式汗贫,解決了我們前文提到的關(guān)于Unicode的兩大問題身坐。同時(shí),作為中文使用者需要注意的一點(diǎn)是Unicode(UTF-8)與GBK落包,GB2312這些漢字編碼規(guī)則是完全不兼容的部蛇,也就是說這兩者之間不能通過任何算法來進(jìn)行轉(zhuǎn)換,如需轉(zhuǎn)換,一般通過GBK查表的方式來進(jìn)行咐蝇。
常見問題及解答
1.windows Notepad中的編碼ANSI保存選項(xiàng)涯鲁,代表什么含義?
ANSI是windows的默認(rèn)的編碼方式有序,對于英文文件是ASCII編碼抹腿,對于簡體中文文件是GB2312編碼(只針對Windows簡體中文版,如果是繁體中文版會采用Big5碼)旭寿。所以警绩,如果將一個(gè)UTF-8編碼的文件,另存為ANSI的方式盅称,對于中文部分會產(chǎn)生亂碼肩祥。
2.什么是UTF-8的BOM?
BOM的全稱是Byte Order Mark缩膝,BOM是微軟給UTF-8編碼加上的混狠,用于標(biāo)識文件使用的是UTF-8編碼,即在UTF-8編碼的文件起始位置逞盆,加入三個(gè)字節(jié)“EE BB BF”檀蹋。這是微軟特有的,標(biāo)準(zhǔn)并不推薦包含BOM的方式。采用加BOM的UTF-8編碼文件俯逾,對于一些只支持標(biāo)準(zhǔn)UTF-8編碼的環(huán)境贸桶,可能導(dǎo)致問題。比如桌肴,在Go語言編程中皇筛,對于包含BOM的代碼文件,會導(dǎo)致編譯出錯(cuò)坠七。詳細(xì)可見我的這篇文章水醋。
3.為什么數(shù)據(jù)庫Latin1字符集(單字節(jié))可以存儲中文呢?
其實(shí)不管需要使用幾個(gè)字節(jié)來表示一個(gè)字符彪置,但最小的存儲單位都是字節(jié),所以拄踪,只要能保證傳輸和存儲的字節(jié)順序不會亂即可。作為數(shù)據(jù)庫拳魁,只是作為存儲的使用的話惶桐,只要能保證存儲的順序與寫入的順序一致,然后再按相同的字節(jié)順序讀出即可潘懊,翻譯成語義字符的任務(wù)交給應(yīng)用程序姚糊。比如’微’的UTF-8編碼是’0xE5 0xBE 0xAE’,那數(shù)據(jù)庫也存儲’0xE5 0xBE 0xAE’三個(gè)字節(jié)授舟,其它應(yīng)用按順序從數(shù)據(jù)庫讀取救恨,再按UTF-8編碼進(jìn)行展現(xiàn)。這當(dāng)然是一個(gè)看似完美的方案释树,但是只要寫入肠槽,存儲,讀取過程中岔出任何別的編碼躏哩,都可能導(dǎo)致亂碼署浩。
4.Mysql數(shù)據(jù)庫中多個(gè)字符集變量(其它數(shù)據(jù)庫其實(shí)也類似)揉燃,它們之間分別是什么關(guān)系扫尺?
我們分別解釋:
character_set_client:客戶端來源的數(shù)據(jù)使用的字符集,用于客戶端顯式告訴客戶端所發(fā)送的語句中的的字符編碼炊汤。
character_set_connection:連接層的字符編碼正驻,mysql一般用character_set_connection將客戶端的字符轉(zhuǎn)換為連接層表示的字符。
character_set_results:查詢結(jié)果從數(shù)據(jù)庫讀出后抢腐,將轉(zhuǎn)換為character_set_results返回給前端姑曙。
而我們常見的解決亂碼問題的操作:
MySQL
mysql_query('SET NAMES GBK')
其相當(dāng)于將以上三個(gè)字符集統(tǒng)一全部設(shè)置為GBK,這三者一致時(shí)迈倍,一般就解決了亂碼問題伤靠。
character_set_database:當(dāng)前選中數(shù)據(jù)庫的默認(rèn)字符集,如當(dāng)create table時(shí)沒有指定字符集啼染,將默認(rèn)選擇該字符集宴合。
character_set_database已經(jīng)character_set_system焕梅,一般用于數(shù)據(jù)庫系統(tǒng)內(nèi)部的一些字符編碼,處理數(shù)據(jù)亂碼問題時(shí)卦洽,我們基本可以忽略贞言。
5.什么情況下,表示信息丟失阀蒂?
對于mysql數(shù)據(jù)庫该窗,我們可以通過hex(colname)函數(shù)(其它數(shù)據(jù)庫也有類似的函數(shù),一些文本文件編輯器也具有這個(gè)功能)蚤霞,查看實(shí)際存儲的字節(jié)內(nèi)容酗失,如:
通過查看存儲的字節(jié)序,我們可以從根本上了解存儲的內(nèi)容是什么編碼了昧绣。而當(dāng)發(fā)現(xiàn)存儲的內(nèi)容全部是’3F’時(shí)级零,就表明存儲的內(nèi)容由于編碼問題,信息已經(jīng)丟失了滞乙,無法再找回奏纪。
之所以出現(xiàn)這種信息丟失的情況,一般是將不能相互轉(zhuǎn)換的字符集之間做了轉(zhuǎn)換斩启,比如我們在前文說到序调,UTF-8只能一個(gè)個(gè)字節(jié)地變成Latin-1,但是根本不能轉(zhuǎn)換的兔簇,因?yàn)閮烧咧g沒有轉(zhuǎn)換規(guī)則发绢,Unicode的字符對應(yīng)范圍也根本不在Latin-1范圍內(nèi),所以只能用’?(0x3F)’代替了垄琐。
總結(jié):
本文從基礎(chǔ)知識與實(shí)際中碰到的問題上边酒,解析了字符編碼相關(guān)內(nèi)容。而之所以要從頭介紹字符編碼的基礎(chǔ)知識狸窘,是為了更好的從原理上了解與解決日常碰到的編碼問題墩朦,只有從根本上了解了不同字符集的規(guī)則及其之間的關(guān)系與兼容性,才能更好的解決碰到的亂碼問題翻擒,也能避免由于程序中不正確的編碼轉(zhuǎn)換導(dǎo)致的信息丟失問題氓涣。