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

原文: 十分鐘搞清字符集和字符編碼

本博客已經遷移至:http://cenalulu.github.io/html

本篇博文已經遷移,閱讀全文請點擊:http://cenalulu.github.io/linux/character-encoding/前端

本文將簡述字符集,字符編碼的概念。以及在遭遇亂碼時的一些經常使用診斷技巧python

背景:字符集和編碼無疑是IT菜鳥甚至是各類大神的頭痛問題。當遇到紛繁複雜的字符集,各類火星文和亂碼時,問題的定位每每變得很是困難。本文就將會從原理方面對字符集和編碼作個簡單的科普介紹,同時也會介紹一些通用的亂碼故障定位的方法以方便讀者之後可以更從容的定位相關問題。在正式介紹以前,先作個小申明:若是你但願很是精確的理解各個名詞的解釋,那麼能夠查閱wikipedia。本文是博主經過本身理解消化後並轉化成易懂淺顯的表述後的介紹。mysql


什麼是字符集

在介紹字符集以前,咱們先了解下爲何要有字符集。咱們在計算機屏幕上看到的是實體化的文字,而在計算機存儲介質中存放的實際是二進制的比特流。那麼在這二者之間的轉換規則就須要一個統一的標準,不然把咱們的U盤查到老闆的電腦上文檔就亂碼了,小夥伴QQ上傳過來的文件在咱們本地打開又亂碼了。(PS:這裏科普下亂碼的英文native說法是mojibake)。因而爲了實現轉換標準,各類字符集標準就出現了。簡單的說字符集就規定了某個文字對應的二進制數字存放方式(編碼)和某串二進制數值表明了哪一個文字(解碼)的轉換關係。
那麼爲何會有那麼多字符集標準呢?這個問題實際很是容易回答。問問本身爲何咱們的插頭拿到英國就不能用了呢?爲何顯示器同時有DVI,VGA,HDMI,DP這麼多接口呢?不少規範和標準在最初制定時並不會意識到這將會是之後全球普適的準則,或者處於組織自己利益就想從本質上區別於現有標準。因而,就產生了那麼多具備相同效果但又不相互兼容的標準了。
說了那麼多咱們來看一個實際例子,下面就是這個字在各類編碼下的十六進制和二進制編碼結果,怎麼樣有沒有一種很屌的感受?linux

字符集 16進制編碼
UTF-8 0xE5B18C
UTF-16 0x5C4C
GBK 0x8CC5

什麼是字符編碼

字符集只是一個規則集合的名字,對應到真實生活中,字符集就是對某種語言的稱呼。例如:英語,漢語,日語。而如何用英語來表達你狠屌的意思就是英語詞法語法所須要具體描述的內容了。而對於一個字符集來講要正確編碼轉碼一個字符須要三個關鍵元素:字庫表(character repertoire)、編碼字符集(coded character set)、字符編碼(character encoding form)。其中字庫表是一個至關於全部可讀或者可顯示字符的數據庫,字庫表決定了整個字符集可以展示表示的全部字符的範圍。編碼字符集,即用一個編碼值code point來表示一個字符在字庫中的位置。字符編碼,將編碼字符集和實際存儲數值之間的轉換關係。通常來講都會直接將code point的值做爲編碼後的值直接存儲。例如在ASCII中A在表中排第65位,而編碼後A的數值是0100 0001也即十進制的65的二進制轉換結果。
看到這裏,可能不少讀者都會有和我當初同樣的疑問:字庫表編碼字符集看來是必不可少的,那既然字庫表中的每個字符都有一個本身的序號,直接把序號做爲存儲內容就行了。爲何還要畫蛇添足經過字符編碼把序號轉換成另一種存儲格式呢?其實緣由也比較容易理解:統一字庫表的目的是爲了可以涵蓋世界上全部的字符,但實際使用過程當中會發現真正用的上的字符相對整個字庫表來講比例很是低。例如中文地區的程序幾乎不會須要日語字符,而一些英語國家甚至簡單的ASCII字庫表就能知足基本需求。而若是把每一個字符都用字庫表中的序號來存儲的話,每一個字符就須要3個字節(這裏以Unicode字庫爲例),這樣對於本來用僅佔一個字符的ASCII編碼的英語地區國家顯然是一個額外成本(存儲體積是原來的三倍)。算的直接一些,一樣一塊硬盤,用ASCII能夠存1500篇文章,而用3字節Unicode序號存儲只能存500篇。因而就出現了UTF-8這樣的變長編碼。在UTF-8編碼中本來只須要一個字節的ASCII字符,仍然只佔一個字節。而像中文及日語這樣的複雜字符就須要2個到3個字節來存儲。git


UTF-8和Unicode的關係

看完上面兩個概念解釋,那麼解釋UTF-8和Unicode的關係就比較簡單了。Unicode就是上文中提到的編碼字符集,而UTF-8就是字符編碼,即Unicode規則字庫的一種實現形式。隨着互聯網的發展,對同一字庫集的要求愈來愈迫切,Unicode標準也就天然而然的出現。它幾乎涵蓋了各個國家語言可能出現的符號和文字,並將爲他們編號。詳見:Unicode on Wikipedia。Unicode的編號從0000開始一直到10FFFF共分爲16個Plane,每一個Plane中有65536個字符。而UTF-8則只實現了第一個Plane,可見UTF-8雖然是一個當今接受度最廣的字符集編碼,可是它並無涵蓋整個Unicode的字庫,這也形成了它在某些場景下對於特殊字符的處理困難(下文會有提到)。github


UTF-8編碼簡介

爲了更好的理解後面的實際應用,咱們這裏簡單的介紹下UTF-8的編碼實現方法。即UTF-8的物理存儲和Unicode序號的轉換關係。
UTF-8編碼爲變長編碼。最小編碼單位(code unit)爲一個字節。一個字節的前1-3個bit爲描述性部分,後面爲實際序號部分。sql

  • 若是一個字節的第一位爲0,那麼表明當前字符爲單字節字符,佔用一個字節的空間。0以後的全部部分(7個bit)表明在Unicode中的序號。
  • 若是一個字節以110開頭,那麼表明當前字符爲雙字節字符,佔用2個字節的空間。110以後的全部部分(7個bit)表明在Unicode中的序號。且第二個字節以10開頭
  • 若是一個字節以1110開頭,那麼表明當前字符爲三字節字符,佔用2個字節的空間。110以後的全部部分(7個bit)表明在Unicode中的序號。且第2、第三個字節以10開頭
  • 若是一個字節以10開頭,那麼表明當前字節爲多字節字符的第二個字節。10以後的全部部分(6個bit)表明在Unicode中的序號。

具體每一個字節的特徵可見下表,其中x表明序號部分,把各個字節中的全部x部分拼接在一塊兒就組成了在Unicode字庫中的序號數據庫

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

咱們分別看三個從一個字節到三個字節的UTF-8編碼例子:瀏覽器

實際字符|在Unicode字庫序號的十六進制|在Unicode字庫序號的二進制|UTF-8編碼後的二進制|UTF-8編碼後的十六進制
$|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

細心的讀者不難從以上的簡單介紹中得出如下規律:

  • 3個字節的UTF-8十六進制編碼必定是以E開頭的
  • 2個字節的UTF-8十六進制編碼必定是以CD開頭的
  • 1個字節的UTF-8十六進制編碼必定是以比8小的數字開頭的

爲何會出現亂碼

簡單的說亂碼的出現是由於:編碼和解碼時用了不一樣或者不兼容的字符集。對應到真實生活中,就比如是一個英國人爲了表示祝福在紙上寫了bless(編碼過程)。而一個法國人拿到了這張紙,因爲在法語中bless表示受傷的意思,因此認爲他想表達的是受傷(解碼過程)。這個就是一個現實生活中的亂碼狀況。在計算機科學中同樣,一個用UTF-8編碼後的字符,用GBK去解碼。因爲兩個字符集的字庫表不同,同一個漢字在兩個字符表的位置也不一樣,最終就會出現亂碼。
咱們來看一個例子:假設咱們用UTF-8編碼存儲很屌兩個字,會有以下轉換:

字符 UTF-8編碼後的十六進制
E5BE88
E5B18C

因而咱們獲得了E5BE88E5B18C這麼一串數值。而顯示時咱們用GBK解碼進行展現,經過查表咱們得到如下信息:

兩個字節的十六進制數值 GBK解碼後對應的字符
E5BE
88E5
B18C

解碼後咱們就獲得了寰堝睂這麼一個錯誤的結果,更要命的是連字符個數都變了。


如何識別亂碼的原本想要表達的文字

要從亂碼字符中反解出原來的正確文字須要對各個字符集編碼規則有較爲深入的掌握。可是原理很簡單,這裏用最多見的UTF-8被錯誤用GBK展現時的亂碼爲例,來講明具體反解和識別過程。

第1步 編碼

假設咱們在頁面上看到寰堝睂這樣的亂碼,而又得知咱們的瀏覽器當前使用GBK編碼。那麼第一步咱們就能先經過GBK把亂碼編碼成二進制表達式。固然查表編碼效率很低,咱們也能夠用如下SQL語句直接經過MySQL客戶端來作編碼工做:
{% highlight mysql %}
{% raw %}
mysql [localhost] {msandbox} > select hex(convert('寰堝睂' using gbk));
+-------------------------------------+
| hex(convert('寰堝睂' using gbk)) |
+-------------------------------------+
| E5BE88E5B18C |
+-------------------------------------+
1 row in set (0.01 sec)
{% endraw %}
{% endhighlight %}

第2步 識別

如今咱們獲得瞭解碼後的二進制字符串E5BE88E5B18C。而後咱們將它按字節拆開。

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

而後套用以前UTF-8編碼介紹章節中總結出的規律,就不難發現這6個字節的數據符合UTF-8編碼規則。若是整個數據流都符合這個規則的話,咱們就能大膽假設亂碼以前的編碼字符集是UTF-8

第3步 解碼

而後咱們就能拿着E5BE88E5B18C用UTF-8解碼,查看亂碼前的文字了。固然咱們能夠不查表直接經過SQL得到結果:
{% highlight mysql %}
{% raw %}
mysql [localhost] {msandbox} ((none)) > select convert(0xE5BE88E5B18C using utf8);
+------------------------------------+
| convert(0xE5BE88E5B18C using utf8) |
+------------------------------------+
| 很屌 |
+------------------------------------+
1 row in set (0.00 sec)
{% endraw %}
{% endhighlight %}

常見問題處理之Emoji

所謂Emoji就是一種在Unicode位於\u1F601-\u1F64F區段的字符。這個顯然超過了目前經常使用的UTF-8字符集的編碼範圍\u0000-\uFFFF。Emoji表情隨着IOS的普及和微信的支持愈來愈常見。下面就是幾個常見的Emoji:
emoji1
emoji2
emoji3
那麼Emoji字符表情會對咱們平時的開發運維帶來什麼影響呢?最多見的問題就在於將他存入MySQL數據庫的時候。通常來講MySQL數據庫的默認字符集都會配置成UTF-8(三字節),而utf8mb4在5.5之後才被支持,也不多會有DBA主動將系統默認字符集改爲utf8mb4。那麼問題就來了,當咱們把一個須要4字節UTF-8編碼才能表示的字符存入數據庫的時候就會報錯:ERROR 1366: Incorrect string value: '\xF0\x9D\x8C\x86' for column 。 若是認真閱讀了上面的解釋,那麼這個報錯也就不難看懂了。咱們試圖將一串Bytes插入到一列中,而這串Bytes的第一個字節是\xF0意味着這是一個四字節的UTF-8編碼。可是當MySQL表和列字符集配置爲UTF-8的時候是沒法存儲這樣的字符的,因此報了錯。
那麼遇到這種狀況咱們如何解決呢?有兩種方式:升級MySQL到5.6或更高版本,而且將表字符集切換至utf8mb4。第二種方法就是在把內容存入到數據庫以前作一次過濾,將Emoji字符替換成一段特殊的文字編碼,而後再存入數據庫中。以後從數據庫獲取或者前端展現時再將這段特殊文字編碼轉換成Emoji顯示。第二種方法咱們假設用-*-1F601-*-來替代4字節的Emoji,那麼具體實現python代碼以下

reference

如何配置Python默認字符集
字符編碼筆記:ASCII,Unicode和UTF-8
Unicode中文編碼表
Emoji Unicode Table

相關文章
相關標籤/搜索