本文來自:http://www.cnblogs.com/cenalulu/p/4325693.htmlphp
要了解爲何會出現亂碼,咱們就先要了解從客戶端發起請求,到MySQL存儲數據,再到下次從表取回客戶端的過程當中,哪些環節會有編碼/解碼的行爲。爲了更好的解釋這個過程,博主製做了兩張流程圖,分別對應存入和取出兩個階段。html
上圖中有3次編碼/解碼的過程(紅色箭頭)。三個紅色箭頭分別對應:客戶端編碼,MySQL Server解碼,Client編碼向表編碼的轉換。其中Terminal能夠是一個Bash,一個web頁面又或者是一個APP。本文中咱們假定Bash是咱們的Terminal,即用戶端的輸入和展現界面。圖中每個框格對應的行爲以下:mysql
上圖有3次編碼/解碼的過程(紅色箭頭)。上圖中三個紅色箭頭分別對應:客戶端解碼展現,MySQL Server根據character-set-client
編碼,表編碼向character-set-client
編碼的轉換。linux
1. 存入和取出時對應環節的編碼不一致
這個會形成亂碼是顯而易見的。咱們把存入階段的三次編解碼使用的字符集編號爲C1,C2,C3(圖一從左到右);取出時的三個字符集依次編號爲C1',C2',C3'(從左到右)。那麼存入的時候bash C1
用的是UTF-8編碼,取出的時候,C1'
咱們卻使用了windows終端(默認是GBK編碼),那麼結果幾乎必定是亂碼。又或者存入MySQL的時候set names utf8
(C2
),而取出的時候卻使用了set names gbk
(C2'
),那麼結果也必然是亂碼web
2. 單個流程中三步的編碼不一致
即上面任意一幅圖中的同方向的三步中,只要兩步或者兩部以上的編碼有不一致就有可能出現編解碼錯誤。若是差別的兩個字符集之間沒法進行無損編碼轉換(下文會詳細介紹),那麼就必定會出現亂碼。例如:咱們的shell是UTF8編碼,MySQL的character-set-client
配置成了GBK,而表結構卻又是charset=utf8
,那麼毫無疑問的必定會出現亂碼。
這裏咱們就簡單演示下這種狀況sql
master [localhost] {msandbox} (test) > create table charset_test_utf8 (id int primary key auto_increment, char_col varchar(50)) charset = utf8; Query OK, 0 rows affected (0.04 sec) master [localhost] {msandbox} (test) > set names gbk; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > insert into charset_test_utf8 (char_col) values ('中文'); Query OK, 1 row affected, 1 warning (0.01 sec) master [localhost] {msandbox} (test) > show warnings; +---------+------+---------------------------------------------------------------------------+ | Level | Code | Message | +---------+------+---------------------------------------------------------------------------+ | Warning | 1366 | Incorrect string value: '\xAD\xE6\x96\x87' for column 'char_col' at row 1 | +---------+------+---------------------------------------------------------------------------+ 1 row in set (0.00 sec) master [localhost] {msandbox} (test) > select id,hex(char_col),char_col from charset_test_utf8; +----+----------------+----------+ | id | hex(char_col) | char_col | +----+----------------+----------+ | 1 | E6B6933FE69E83 | �?�� | +----+----------------+----------+ 1 row in set (0.01 sec)
既然系統之間是按照二進制流進行傳輸的,那直接把這串二進制流直接存入表文件就好啦。爲何在存儲以前還要進行兩次編解碼的操做呢?shell
insert
仍是update
。select left(col,2) from table
的語句,存儲引擎從文件讀入該column的值是E4B8ADE69687
。那麼這個時候若是咱們按照GBK把這個值分割成E4B8
,ADE6
,9687
三個字,並那麼返回客戶端的值就應該是E4B8ADE6
;若是按照UTF8分割成E4B8AD
,E69687
,那麼就應該返回E4B8ADE69687
兩個字。可見,若是在從數據文件讀入數據後,不進行編解碼的話在存儲引擎內部是沒法進行字符級別的操做的。在MySQL中最多見的亂碼問題的原由就是把錯進錯出
神話。所謂的錯進錯出就是,客戶端(web或shell)的字符編碼和最終表的字符編碼格式不一樣,可是隻要保證存和取兩次的字符集編碼一致就仍然可以得到沒有亂碼的輸出的這種現象。可是,錯進錯出並非對於任意兩種字符集編碼的組合都是有效的。咱們假設客戶端的編碼是C,MySQL表的字符集編碼是S。那麼爲了可以錯進錯出,須要知足如下兩個條件windows
MySQL接收請求時,從C編碼後的二進制流在被S解碼時可以無損
MySQL返回數據是,從S編碼後的二進制流在被C解碼時可以無損bash
那麼什麼是有損轉換,什麼是無損轉換呢?假設咱們要把用編碼A表示的字符X,轉化爲編碼B的表示形式,而編碼B的字形集中並無X這個字符,那麼此時咱們就稱這個轉換是有損的。那麼,爲何會出現兩個編碼所能表示字符集合的差別呢?若是你們看過博主以前的那篇 十分鐘搞清字符集和字符編碼,或者對字符編碼有基礎理解的話,就應該知道每一個字符集所支持的字符數量是有限的,而且各個字符集涵蓋的文字之間存在差別。UTF8和GBK所能表示的字符數量範圍以下網絡
8140
- FEFE
其中不包括**7E
,總共字符數在27000左右因爲UTF-8編碼能表示的字符數量遠超GBK。那麼咱們很容易就能找到一個從UTF8到GBK的有損編碼轉換。咱們用字符映射器(見下圖)找出了一個明顯就不在GBK編碼表中的字符,嘗試存入到GBK編碼的表中。並再次取出查看有損轉換的行爲
字符信息具體是:ਅ GURMUKHI LETTER A Unicode: U+0A05, UTF-8: E0 A8 85
在MySQL中存儲的具體狀況以下:
master [localhost] {msandbox} (test) > create table charset_test_gbk (id int primary key auto_increment, char_col varchar(50)) charset = gbk; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > set names utf8; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > insert into charset_test_gbk (char_col) values ('ਅ'); Query OK, 1 row affected, 1 warning (0.01 sec) master [localhost] {msandbox} (test) > show warnings; +---------+------+-----------------------------------------------------------------------+ | Level | Code | Message | +---------+------+-----------------------------------------------------------------------+ | Warning | 1366 | Incorrect string value: '\xE0\xA8\x85' for column 'char_col' at row 1 | +---------+------+-----------------------------------------------------------------------+ 1 row in set (0.00 sec) master [localhost] {msandbox} (test) > select id,hex(char_col),char_col,char_length(char_col) from charset_test_gbk; +----+---------------+----------+-----------------------+ | id | hex(char_col) | char_col | char_length(char_col) | +----+---------------+----------+-----------------------+ | 1 | 3F | ? | 1 | +----+---------------+----------+-----------------------+ 1 row in set (0.00 sec)
出錯的部分是在編解碼的第3步時發生的。具體見下圖
可見MySQL內部若是沒法找到一個UTF8字符所對應的GBK字符時,就會轉換成一個錯誤mark(這裏是問號)。而每一個字符集在程序實現的時候內部都約定了當出現這種狀況時的行爲和轉換規則。例如:UTF8中沒法找到對應字符時,若是不拋錯那麼就將該字符替換成�
(U+FFFD)
那麼是否是任何兩種字符集編碼之間的轉換都是有損的呢?並不是這樣,轉換是否有損取決於如下幾點:
關於第一點,剛纔已經經過實驗來解釋過了。這裏來解釋下形成有損轉換的第二個因素。從剛纔的例子咱們能夠看到因爲GBK在處理本身沒法表示的字符時的行爲是:用錯誤標識
替代,即0x3F
。而有些字符集(例如latin1)在遇到本身沒法表示的字符時,會保留原字符集的編碼數據,並跳過忽略該字符進而處理後面的數據。若是目標字符集具備這樣的特性,那麼就可以實現這節最開始提到的錯進錯出
的效果。
咱們來看下面這個例子
master [localhost] {msandbox} (test) > create table charset_test (id int primary key auto_increment, char_col varchar(50)) charset = latin1; Query OK, 0 rows affected (0.03 sec) master [localhost] {msandbox} (test) > set names latin1; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > insert into charset_test (char_col) values ('中文'); Query OK, 1 row affected (0.01 sec) master [localhost] {msandbox} (test) > select id,hex(char_col),char_col from charset_test; +----+---------------+----------+ | id | hex(char_col) | char_col | +----+---------------+----------+ | 2 | E4B8ADE69687 | 中文 | +----+---------------+----------+ 2 rows in set (0.00 sec)
具體流程圖以下。可見在被MySQL Server接收到之後實際上已經發生了編碼不一致的狀況。可是因爲Latin1字符集對於本身表述範圍外的字符不會作任何處理,而是保留原值。這樣的行爲也使得錯進錯出成爲了可能。
理解了上面的內容,要避免亂碼就顯得很容易了。只要作到「三位一體」,即客戶端,MySQL character-set-client,table charset三個字符集徹底一致就能夠保證必定不會有亂碼出現了。而對於已經出現亂碼,或者已經遭受有損轉碼的數據,如何修復相對來講就會有些困難。下一節咱們詳細介紹具體方法。
在介紹正確方法前,咱們先科普一下那些網上流傳的所謂的「正確方法」可能會形成的嚴重後果。
不管從語法仍是字面意思來看:ALTER TABLE ... CHARSET=xxx
無疑是最像包治亂碼的良藥了!而事實上,他對於你已經損壞的數據一點幫助也沒有,甚至連已經該表已經建立列的默認字符集都沒法改變。咱們看下面這個例子
master [localhost] {msandbox} (test) > show create table charset_test; +--------------+--------------------------------+ | Table | Create Table | +--------------+--------------------------------+ | charset_test | CREATE TABLE `charset_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `char_col` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1 | +--------------+--------------------------------+ 1 row in set (0.00 sec) master [localhost] {msandbox} (test) > alter table charset_test charset=gbk; Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0 master [localhost] {msandbox} (test) > show create table charset_test; +--------------+--------------------------------+ | Table | Create Table | +--------------+--------------------------------+ | charset_test | CREATE TABLE `charset_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `char_col` varchar(50) CHARACTER SET latin1 DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=gbk | +--------------+--------------------------------+ 1 row in set (0.00 sec)
可見該語法牢牢修改了表的默認字符集,即只對之後建立的列的默認字符集產生影響,而對已經存在的列和數據沒有變化。
ALTER TABLE … CONVERT TO CHARACTER SET …
的相較於方法一來講殺傷力更大,由於從 官方文檔的解釋 他的做用就是用於對一個表的數據進行編碼轉換。下面是文檔的一小段摘錄:
To change the table default character set and all character columns (CHAR, VARCHAR, TEXT) to a new character set, use a statement like this:
ALTER TABLE tbl_name
CONVERT TO CHARACTER SET charset_name [COLLATE collation_name];
而實際上,這句語法只適用於當前並無亂碼,而且不是經過錯進錯出
的方法保存的表。{: style="color: red"}。而對於已經由於錯進錯出而產生編碼錯誤的表,則會帶來更糟的結果。咱們用一個實際例子來解釋下,這句SQL實際作了什麼和他會形成的結果。假設咱們有一張編碼是latin1的表,且以前經過錯進錯出存入了UTF-8的數據,可是由於經過terminal仍然可以正常顯示。即上文錯進錯出章節中舉例的狀況。一段時間使用後咱們發現了這個錯誤,並打算把表的字符集編碼改爲UTF-8而且不影響原有數據的正常顯示。這種狀況下使用alter table convert to character set
會有這樣的後果:
master [localhost] {msandbox} (test) > create table charset_test_latin1 (id int primary key auto_increment, char_col varchar(50)) charset = latin1; Query OK, 0 rows affected (0.01 sec) master [localhost] {msandbox} (test) > set names latin1; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > insert into charset_test_latin1 (char_col) values ('這是中文'); Query OK, 1 row affected (0.01 sec) master [localhost] {msandbox} (test) > select id,hex(char_col),char_col,char_length(char_col) from charset_test_latin1; +----+--------------------------+--------------+-----------------------+ | id | hex(char_col) | char_col | char_length(char_col) | +----+--------------------------+--------------+-----------------------+ | 1 | E8BF99E698AFE4B8ADE69687 | 這是中文 | 12 | +----+--------------------------+--------------+-----------------------+ 1 row in set (0.01 sec) master [localhost] {msandbox} (test) > alter table charset_test_latin1 convert to character set utf8; Query OK, 1 row affected (0.04 sec) Records: 1 Duplicates: 0 Warnings: 0 master [localhost] {msandbox} (test) > set names utf8; Query OK, 0 rows affected (0.00 sec) master [localhost] {msandbox} (test) > select id,hex(char_col),char_col,char_length(char_col) from charset_test_latin1; +----+--------------------------------------------------------+-----------------------------+-----------------------+ | id | hex(char_col) | char_col | char_length(char_col) | +----+--------------------------------------------------------+-----------------------------+-----------------------+ | 1 | C3A8C2BFE284A2C3A6CB9CC2AFC3A4C2B8C2ADC3A6E28093E280A1 | 这是ä¸æ–‡ | 12 | +----+--------------------------------------------------------+-----------------------------+-----------------------+ 1 row in set (0.00 sec)
從這個例子咱們能夠看出,對於已經錯進錯出的數據表,這個命令不但沒有起到「撥亂反正」的效果,還會完全將數據糟蹋,連數據的二進制編碼都改變了。
這個方法比較笨,但也比較好操做和理解。簡單的說分爲如下三步:
仍是用上面那個例子舉例,咱們用UTF-8將數據「錯進」到latin1編碼的表中。如今須要將表編碼修改成UTF-8可使用如下命令
shell> mysqldump -u root -p -d --skip-set-charset --default-character-set=utf8 test charset_test_latin1 > data.sql #確保導出的文件用文本編輯器在UTF-8編碼下查看沒有亂碼 shell> mysql -uroot -p -e 'create table charset_test_latin1 (id int primary key auto_increment, char_col varchar(50)) charset = utf8' test shell> mysql -uroot -p --default-character-set=utf8 test < data.sql
這種方法比較取巧,用的是將二進制數據做爲中間數據的作法來實現的。因爲,MySQL再將有編碼意義的數據流,轉換爲無編碼意義的二進制數據的時候並不作實際的數據轉換。而從二進制數據準換爲帶編碼的數據時,又會用目標編碼作一次編碼轉換校驗。經過這兩個特性就至關於在MySQL內部模擬了一次「錯出」,將亂碼「撥亂反正」了。
仍是用上面那個例子舉例,咱們用UTF-8將數據「錯進」到latin1編碼的表中。如今須要將表編碼修改成UTF-8可使用如下命令
mysql> ALTER TABLE charset_test_latin1 MODIFY COLUMN char_col VARBINARY(50); mysql> ALTER TABLE charset_test_latin1 MODIFY COLUMN char_col varchar(50) character set utf8;