十分鐘搞清字符集和字符編碼

什么是字符集

在介紹字符集之前德频,我們先了解下為什么要有字符集扣汪。我們在計算機(jī)屏幕上看到的是實體化的文字,而在計算機(jī)存儲介質(zhì)中存放的實際是二進(jìn)制的比特流亡脸。那么在這兩者之間的轉(zhuǎn)換規(guī)則就需要一個統(tǒng)一的標(biāo)準(zhǔn)校镐,否則把我們的U盤插到老板的電腦上亿扁,文檔就亂碼了;小伙伴QQ上傳過來的文件鸟廓,在我們本地打開又亂碼了从祝。于是為了實現(xiàn)轉(zhuǎn)換標(biāo)準(zhǔn)襟己,各種字符集標(biāo)準(zhǔn)就出現(xiàn)了。簡單的說字符集就規(guī)定了某個文字對應(yīng)的二進(jìn)制數(shù)字存放方式(編碼)和某串二進(jìn)制數(shù)值代表了哪個文字(解碼)的轉(zhuǎn)換關(guān)系牍陌。 那么為什么會有那么多字符集標(biāo)準(zhǔn)呢擎浴?這個問題實際非常容易回答。問問自己為什么我們的插頭拿到英國就不能用了呢毒涧?為什么顯示器同時有DVI贮预,VGA,HDMI链嘀,DP這么多接口呢?很多規(guī)范和標(biāo)準(zhǔn)在最初制定時并不會意識到這將會是以后全球普適的準(zhǔn)則档玻,或者處于組織本身利益就想從本質(zhì)上區(qū)別于現(xiàn)有標(biāo)準(zhǔn)怀泊。于是,就產(chǎn)生了那么多具有相同效果但又不相互兼容的標(biāo)準(zhǔn)了误趴。 說了那么多我們來看一個實際例子霹琼,下面就是這個字在各種編碼下的十六進(jìn)制和二進(jìn)制編碼結(jié)果,怎么樣有沒有一種很屌的感覺凉当?

字符集 16進(jìn)制編碼 對應(yīng)的二進(jìn)制數(shù)據(jù)
UTF-8 0xE5B18C 1110 0101 1011 0001 1000 1100
UTF-16 0x5C4C 1011 1000 1001 1000
GBK 0x8CC5 1000 1100 1100 0101

什么是字符編碼

字符集只是一個規(guī)則集合的名字枣申,對應(yīng)到真實生活中,字符集就是對某種語言的稱呼看杭。例如:英語忠藤,漢語,日語楼雹。對于一個字符集來說要正確編碼轉(zhuǎn)碼一個字符需要三個關(guān)鍵元素:字庫表(character repertoire)模孩、編碼字符集(coded character set)、字符編碼(character encoding form)贮缅。其中字庫表是一個相當(dāng)于所有可讀或者可顯示字符的數(shù)據(jù)庫榨咐,字庫表決定了整個字符集能夠展現(xiàn)表示的所有字符的范圍。編碼字符集谴供,即用一個編碼值code point來表示一個字符在字庫中的位置块茁。字符編碼,將編碼字符集和實際存儲數(shù)值之間的轉(zhuǎn)換關(guān)系桂肌。一般來說都會直接將code point的值作為編碼后的值直接存儲数焊。例如在ASCII中A在表中排第65位,而編碼后A的數(shù)值是0100 0001也即十進(jìn)制的65的二進(jìn)制轉(zhuǎn)換結(jié)果崎场。 看到這里昌跌,可能很多讀者都會有和我當(dāng)初一樣的疑問:字庫表編碼字符集看來是必不可少的,那既然字庫表中的每一個字符都有一個自己的序號照雁,直接把序號作為存儲內(nèi)容就好了蚕愤。為什么還要多此一舉通過字符編碼把序號轉(zhuǎn)換成另外一種存儲格式呢答恶?其實原因也比較容易理解:統(tǒng)一字庫表的目的是為了能夠涵蓋世界上所有的字符,但實際使用過程中會發(fā)現(xiàn)真正用的上的字符相對整個字庫表來說比例非常低萍诱。例如中文地區(qū)的程序幾乎不會需要日語字符悬嗓,而一些英語國家甚至簡單的ASCII字庫表就能滿足基本需求。而如果把每個字符都用字庫表中的序號來存儲的話裕坊,每個字符就需要3個字節(jié)(這里以Unicode字庫為例)包竹,這樣對于原本用僅占一個字符的ASCII編碼的英語地區(qū)國家顯然是一個額外成本(存儲體積是原來的三倍)超棺。算的直接一些劫乱,同樣一塊硬盤,用ASCII可以存1500篇文章樊销,而用3字節(jié)Unicode序號存儲只能存500篇饵蒂。于是就出現(xiàn)了UTF-8這樣的變長編碼声诸。在UTF-8編碼中原本只需要一個字節(jié)的ASCII字符,仍然只占一個字節(jié)退盯。而像中文及日語這樣的復(fù)雜字符就需要2個到3個字節(jié)來存儲彼乌。


UTF-8和Unicode的關(guān)系

看完上面兩個概念解釋,那么解釋UTF-8和Unicode的關(guān)系就比較簡單了渊迁。Unicode就是上文中提到的編碼字符集慰照,而UTF-8就是字符編碼,即Unicode規(guī)則字庫的一種實現(xiàn)形式琉朽。隨著互聯(lián)網(wǎng)的發(fā)展毒租,對同一字庫集的要求越來越迫切,Unicode標(biāo)準(zhǔn)也就自然而然的出現(xiàn)箱叁。它幾乎涵蓋了各個國家語言可能出現(xiàn)的符號和文字蝌衔,并將為他們編號。詳見:Unicode on Wikipedia蝌蹂。Unicode的編號從0000開始一直到10FFFF共分為17個Plane噩斟,每個Plane中有65536個字符。而UTF-8則只實現(xiàn)了第一個Plane孤个,可見UTF-8雖然是一個當(dāng)今接受度最廣的字符集編碼剃允,但是它并沒有涵蓋整個Unicode的字庫,這也造成了它在某些場景下對于特殊字符的處理困難(下文會有提到)齐鲤。


UTF-8編碼簡介

為了更好的理解后面的實際應(yīng)用斥废,我們這里簡單的介紹下UTF-8的編碼實現(xiàn)方法。即UTF-8的物理存儲和Unicode序號的轉(zhuǎn)換關(guān)系给郊。 UTF-8編碼為變長編碼牡肉。最小編碼單位(code unit)為一個字節(jié)。一個字節(jié)的前1-3個bit為描述性部分淆九,后面為實際序號部分统锤。

  • 如果一個字節(jié)的第一位為0毛俏,那么代表當(dāng)前字符為單字節(jié)字符,占用一個字節(jié)的空間饲窿。0之后的所有部分(7個bit)代表在Unicode中的序號煌寇。
  • 如果一個字節(jié)以110開頭,那么代表當(dāng)前字符為雙字節(jié)字符逾雄,占用2個字節(jié)的空間阀溶。110之后的所有部分(5個bit)加上后一個字節(jié)的除10外的部分(6個bit)代表在Unicode中的序號。且第二個字節(jié)以10開頭
  • 如果一個字節(jié)以1110開頭鸦泳,那么代表當(dāng)前字符為三字節(jié)字符银锻,占用3個字節(jié)的空間。110之后的所有部分(5個bit)加上后兩個字節(jié)的除10外的部分(12個bit)代表在Unicode中的序號做鹰。且第二击纬、第三個字節(jié)以10開頭
  • 如果一個字節(jié)以10開頭,那么代表當(dāng)前字節(jié)為多字節(jié)字符的第二個字節(jié)誊垢。10之后的所有部分(6個bit)和之前的部分一同組成在Unicode中的序號掉弛。

具體每個字節(jié)的特征可見下表症见,其中x代表序號部分喂走,把各個字節(jié)中的所有x部分拼接在一起就組成了在Unicode字庫中的序號

Byte 1 Byte 2 Byte3
0xxx xxxx
110x xxxx 10xx xxxx
1110 xxxx 10xx xxxx 10xx xxxx

我們分別看三個從一個字節(jié)到三個字節(jié)的UTF-8編碼例子:

| 實際字符 | 在Unicode字庫序號的十六進(jìn)制 | 在Unicode字庫序號的二進(jìn)制 | UTF-8編碼后的二進(jìn)制 | UTF-8編碼后的十六進(jìn)制 |
| $ | 0024 | 010 0100 | 0010 0100 | 24 |
| ¢ | 00A2 | 000 1010 0010 | 1100 0010 1010 0010 | C2 A2 |
| € | 20AC | 0010 0000 1010 1100 | 1110 0010 1000 0010 1010 1100 | E2 82 AC |

細(xì)心的讀者不難從以上的簡單介紹中得出以下規(guī)律:

  • 3個字節(jié)的UTF-8十六進(jìn)制編碼一定是以E開頭的
  • 2個字節(jié)的UTF-8十六進(jìn)制編碼一定是以CD開頭的
  • 1個字節(jié)的UTF-8十六進(jìn)制編碼一定是以比8小的數(shù)字開頭的

為什么會出現(xiàn)亂碼

亂碼也就是英文常說的mojibake(由日語的文字化け音譯)。 簡單的說亂碼的出現(xiàn)是因為:編碼和解碼時用了不同或者不兼容的字符集谋作。對應(yīng)到真實生活中芋肠,就好比是一個英國人為了表示祝福在紙上寫了bless(編碼過程)。而一個法國人拿到了這張紙遵蚜,由于在法語中bless表示受傷的意思帖池,所以認(rèn)為他想表達(dá)的是受傷(解碼過程)。這個就是一個現(xiàn)實生活中的亂碼情況吭净。在計算機(jī)科學(xué)中一樣睡汹,一個用UTF-8編碼后的字符,用GBK去解碼寂殉。由于兩個字符集的字庫表不一樣囚巴,同一個漢字在兩個字符表的位置也不同,最終就會出現(xiàn)亂碼友扰。 我們來看一個例子:假設(shè)我們用UTF-8編碼存儲很屌兩個字彤叉,會有如下轉(zhuǎn)換:

字符 UTF-8編碼后的十六進(jìn)制
E5BE88
E5B18C

于是我們得到了E5BE88E5B18C這么一串?dāng)?shù)值。而顯示時我們用GBK解碼進(jìn)行展示村怪,通過查表我們獲得以下信息:

兩個字節(jié)的十六進(jìn)制數(shù)值 GBK解碼后對應(yīng)的字符
E5BE
88E5
B18C

解碼后我們就得到了寰堝睂這么一個錯誤的結(jié)果秽浇,更要命的是連字符個數(shù)都變了。


如何識別亂碼的本來想要表達(dá)的文字

要從亂碼字符中反解出原來的正確文字需要對各個字符集編碼規(guī)則有較為深刻的掌握甚负。但是原理很簡單柬焕,這里用最常見的UTF-8被錯誤用GBK展示時的亂碼為例审残,來說明具體反解和識別過程。

第1步 編碼

假設(shè)我們在頁面上看到寰堝睂這樣的亂碼击喂,而又得知我們的瀏覽器當(dāng)前使用GBK編碼维苔。那么第一步我們就能先通過GBK把亂碼編碼成二進(jìn)制表達(dá)式。當(dāng)然查表編碼效率很低懂昂,我們也可以用以下SQL語句直接通過MySQL客戶端來做編碼工作:

mysql [localhost] {msandbox} > select hex(convert('寰堝睂' using gbk));
+-------------------------------------+
| hex(convert('寰堝睂' using gbk))    |
+-------------------------------------+
| E5BE88E5B18C                        |
+-------------------------------------+
1 row in set (0.01 sec)

第2步 識別

現(xiàn)在我們得到了解碼后的二進(jìn)制字符串E5BE88E5B18C介时。然后我們將它按字節(jié)拆開。

Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6
E5 BE 88 E5 B1 8C

然后套用之前UTF-8編碼介紹章節(jié)中總結(jié)出的規(guī)律凌彬,就不難發(fā)現(xiàn)這6個字節(jié)的數(shù)據(jù)符合UTF-8編碼規(guī)則沸柔。如果整個數(shù)據(jù)流都符合這個規(guī)則的話,我們就能大膽假設(shè)亂碼之前的編碼字符集是UTF-8

第3步 解碼

然后我們就能拿著E5BE88E5B18C用UTF-8解碼铲敛,查看亂碼前的文字了褐澎。當(dāng)然我們可以不查表直接通過SQL獲得結(jié)果:

mysql [localhost] {msandbox} ((none)) > select convert(0xE5BE88E5B18C using utf8);
+------------------------------------+
| convert(0xE5BE88E5B18C using utf8) |
+------------------------------------+
| 很屌                               |
+------------------------------------+
1 row in set (0.00 sec)

常見問題處理之Emoji

所謂Emoji就是一種在Unicode位于\u1F601-\u1F64F區(qū)段的字符。這個顯然超過了目前常用的UTF-8字符集的編碼范圍\u0000-\uFFFF伐蒋。Emoji表情隨著IOS的普及和微信的支持越來越常見工三。下面就是幾個常見的Emoji: [圖片上傳失敗...(image-a1f431-1658902623315)]

[圖片上傳失敗...(image-194f4-1658902623315)]

[圖片上傳失敗...(image-6ca08e-1658902623315)]

那么Emoji字符表情會對我們平時的開發(fā)運維帶來什么影響呢?最常見的問題就在于將他存入MySQL數(shù)據(jù)庫的時候先鱼。一般來說MySQL數(shù)據(jù)庫的默認(rèn)字符集都會配置成UTF-8(三字節(jié))俭正,而utf8mb4在5.5以后才被支持,也很少會有DBA主動將系統(tǒng)默認(rèn)字符集改成utf8mb4焙畔。那么問題就來了掸读,當(dāng)我們把一個需要4字節(jié)UTF-8編碼才能表示的字符存入數(shù)據(jù)庫的時候就會報錯:ERROR 1366: Incorrect string value: '\xF0\x9D\x8C\x86' for column 。 如果認(rèn)真閱讀了上面的解釋宏多,那么這個報錯也就不難看懂了儿惫。我們試圖將一串Bytes插入到一列中,而這串Bytes的第一個字節(jié)是\xF0意味著這是一個四字節(jié)的UTF-8編碼伸但。但是當(dāng)MySQL表和列字符集配置為UTF-8的時候是無法存儲這樣的字符的肾请,所以報了錯。 那么遇到這種情況我們?nèi)绾谓鉀Q呢更胖?有兩種方式:升級MySQL到5.6或更高版本铛铁,并且將表字符集切換至utf8mb4。第二種方法就是在把內(nèi)容存入到數(shù)據(jù)庫之前做一次過濾函喉,將Emoji字符替換成一段特殊的文字編碼避归,然后再存入數(shù)據(jù)庫中。之后從數(shù)據(jù)庫獲取或者前端展示時再將這段特殊文字編碼轉(zhuǎn)換成Emoji顯示管呵。第二種方法我們假設(shè)用-*-1F601-*-來替代4字節(jié)的Emoji梳毙,那么具體實現(xiàn)python代碼可以參見Stackoverflow上的回答

reference

如何配置Python默認(rèn)字符集 字符編碼筆記:ASCII,Unicode和UTF-8 Unicode中文編碼表 Emoji Unicode Table Every Developer Should Know About The Encoding

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捐下,一起剝皮案震驚了整個濱河市账锹,隨后出現(xiàn)的幾起案子萌业,更是在濱河造成了極大的恐慌,老刑警劉巖奸柬,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件生年,死亡現(xiàn)場離奇詭異,居然都是意外死亡廓奕,警方通過查閱死者的電腦和手機(jī)抱婉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桌粉,“玉大人蒸绩,你說我怎么就攤上這事×蹇希” “怎么了患亿?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長押逼。 經(jīng)常有香客問我步藕,道長,這世上最難降的妖魔是什么挑格? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任咙冗,我火速辦了婚禮,結(jié)果婚禮上恕齐,老公的妹妹穿的比我還像新娘乞娄。我一直安慰自己瞬逊,他們只是感情好显歧,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著确镊,像睡著了一般士骤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蕾域,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天拷肌,我揣著相機(jī)與錄音,去河邊找鬼旨巷。 笑死巨缘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的采呐。 我是一名探鬼主播若锁,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼斧吐!你這毒婦竟也來了又固?” 一聲冷哼從身側(cè)響起仲器,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仰冠,沒想到半個月后乏冀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡洋只,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年辆沦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片识虚。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡众辨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舷礼,到底是詐尸還是另有隱情鹃彻,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布妻献,位于F島的核電站蛛株,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏育拨。R本人自食惡果不足惜谨履,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熬丧。 院中可真熱鬧笋粟,春花似錦、人聲如沸析蝴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闷畸。三九已至尝盼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間佑菩,已是汗流浹背盾沫。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留殿漠,地道東北人赴精。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像绞幌,于是被迫代替她去往敵國和親蕾哟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 一、亂碼 首先渐苏,所有信息在計算機(jī)上都是以二進(jìn)制形式存儲掀潮。而當(dāng)出現(xiàn)亂碼的時候往往是將這些信息以字符的形式表現(xiàn)之后。這...
    假鞋子閱讀 1,335評論 0 0
  • ** 本文轉(zhuǎn)載自 CENALULU`S TECH BLOG琼富,學(xué)習(xí)使用仪吧,侵刪。 * 本文將簡述字符集鞠眉,字符編碼的概念...
    王康_Wang閱讀 311評論 0 0
  • 字符與字符編碼字符字符和字節(jié)不太一樣薯鼠,任何一個文字或符號都是一個字符,但所占字節(jié)不一定械蹋,不同的編碼導(dǎo)致一個字符所占...
    hexm01閱讀 1,389評論 0 1
  • 亂碼 亂碼是怎么出現(xiàn)的呢出皇?對同一組二進(jìn)制數(shù)據(jù),不同的編碼會解析出不同的字符哗戈,用對了編碼郊艘,解析出來的字符組成的文字是...
    __Jasmine__閱讀 618評論 0 0
  • 在軟件的編碼和實現(xiàn)中,我們可能會碰到個一個比較頭疼的問題--編碼唯咬,不同字符間的編碼和解碼纱注,你確定了解各種字符的編碼...
    Java小鋪閱讀 2,523評論 0 5