Mysql 的字元編碼機制、中文亂碼問題及解決方案
相信很多朋友都會對字元編碼敬而遠之,但一發生亂碼問題卻頭大不已,本文結合前人的經驗及Mysql手冊中的解釋,用具體的操作和例子,旨在瞭解mysql的字元編碼機制以及亂碼問題的解決。
【問題現象】
網頁xxx.php用EditPlus另存為UTF8格式,
MySQL在my.ini(linux系統中配置檔案為my.cnf)裡設定[ client ] 和 [ mysqld ] 都設定為default-character-set=utf8,
建表時加了CREATE TABLE `xxx ` (myname varchar(255)) ENGINE=MyISAM DEFAULT CHARSET=utf8,
用xxx.php執行insert/update/select出來的都是中文,
貌似沒問題,但是用phpMyAdmin看select是亂碼,用第三方工具軟體(如SQLyog)看select也是亂碼,mysqldump也是亂碼,很不爽。當然,如果你建表的時候,選擇了binary/varbinary/blob型別,不會發現亂碼,因為指定的是二進位制儲存,MySQL儲存資料時就沒有編碼的概念了。
【查詢問題】
雖然在my.ini裡設定default-character-set=utf8,但是執行以下命令時有新發現:
mysql> SHOW VARIABLES LIKE 'character%';
+----------------------------------------+-------------------------
| Variable_name| Value
+----------------------------------------+-------------------------
| character_set_client | latin1
| character_set_connection | latin1
| character_set_database | utf8
| character_set_filesystem | binary
| character_set_results| latin1
| character_set_server| utf8
| character_set_system| utf8
| character_sets_dir| D:\mysql\share\charsets\
+----------------------------------------+-------------------------
8 rows in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'collation_%';
+---------------------------------------+------------------
| Variable_name| Value
+---------------------------------------+------------------
| collation_connection | latin1_swedish_ci
| collation_database| utf8_general_ci
| collation_server| utf8_general_ci
+--------------------------------------+------------------
3 rows in set (0.00 sec)
發現Value列裡面不全是utf8,仍然有部分是latin1,比如其中的client和connection。
其中本文會多次提及以下幾個概念:
character_set_client:客戶端傳送的查詢中所使用的字符集,下文簡稱client
character_set_connection:伺服器接收到查詢後,會將查詢從character_set_client系統變數所標識的編碼轉換到character_set_connection,下文簡稱connection
character_set_results:伺服器傳送結果集或返回錯誤資訊到客戶端所使用的字符集,下文簡稱results
而這幾個字符集設定的關係為:
資訊輸入路徑:client→connection→server
資訊輸出路徑:server→connection→results
換句話說,每個路徑要經過3次改變字符集編碼。以出現亂碼的輸出為例,按照上表中的字符集設定:server裡utf8的資料,傳入connection轉為latin1,傳入results轉為latin1,頁面如果設定為UTF-8的話,又把results的latin1轉為UTF-8。如果兩種字符集不相容,比如latin1和utf8,轉化過程就為不可逆的,破壞性的。所以就轉不回來了。
再來一個輸入路徑的例子:從xxx.php頁面上輸入漢字,因為xxx.php是UTF8編碼的,所以xxx.php以UTF8格式轉換輸入的漢字,然後以UTF8提交給mysql,但是mysql的client和connection都是latin1的,而server是UTF8的,所以mysql儲存時,先將xxx.php提交的漢字,轉成latin1的格式,再轉成UTF8字元格式存在表中。如果此時我們用第三方軟體或者phpMyAdmin去select檢視此表,而表中儲存的資料是被latin1過的UTF8字元,出來的時候是以UTF8格式取的,當然看起來時亂碼了。解決方法就是讓所有過程都是UTF8的就可以了。
【解決問題】
一、從my.ini(linux系統為my.cnf)下手
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
collation-server = utf8_unicode_ci
init-connect='SET NAMES utf8'
character-set-server = utf8
以上2個section都要加default-character-set=utf8,而預設這兩項都為latin1
然後重啟mysql,執行
mysql> SHOW VARIABLES LIKE 'character%';
mysql> SHOW VARIABLES LIKE 'collation_%';
確保所有的Value項都是utf8即可。
二、設定資料庫(database)、表(table)或欄位(column)的字符集(character set)及校對規則(collate)。
例如:建表時加utf8,表字段的Collation可加可不加,不加時預設是utf8_general_ci了。
CREATE TABLE `tablename4` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`varchar1` varchar(255) DEFAULT NULL,
`varbinary1` varbinary(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
這裡要說明一下:
1、在服務端,主要分為四個層次,由底到高依次為:server, database, table, column,每一次都可設定其字符集(character set)及校對(collate),如果高層的字符集有設定,則按高層的來進行字元編碼,如果本層無設定編碼,則採用下一層次的字符集。
因此,如果設定了database級的字符集,則table級的字符集可設可不設,如果既設定了database級又設定了table級,則按table級來儲存。
2、字符集(character set)和校對的關係。
字符集是一套符號和編碼。校對規則是在字符集內用於比較字元的一套規則。
用一個簡單的例子來解釋,如,有四個字母,A,B,C,D,我們為每個字母賦予一個數值:‘A’=0,‘B’= 1,‘a’= 2,‘b’= 3。字母‘A’是一個符號,數字0是‘A’的編碼,這四個字母和它們的編碼組合在一起是一個字符集。而針對某一字符集的校對規則可以有多種,例如按字母所代表的數值大小來排列,或者大小寫不敏感的排列等等。
MySQL按照下面的方式選擇字符集和 校對規則:
· 如果指定了CHARACTER SET X和COLLATE Y,那麼採用CHARACTER SET X和COLLATE Y。
· 如果指定了CHARACTER SET X而沒有指定COLLATE Y,那麼採用CHARACTER SET X和CHARACTER SET X的預設校對規則。
如果指定了COLLATE Y而沒有指定CHARACTER SET X,那麼採用COLLATE Y相對應的CHARACTER SET和COLLATE Y。
· 否則,採用低一級伺服器字符集和伺服器校對規則。
三、頁面檔案儲存時選擇utf-8編碼,頁面上加上utf-8的編碼方式:
對於靜態頁面,在<head>內加上:<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
對於PHP檔案:header('conten-type:text/html;charset=utf-8');
對於JSP、Servlet:
a) 設定web容器的編碼格式。為你的servlet的doGet或doPost方法開始處加入如下程式碼:
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
b) 為每個jsp頁面指定其編碼格式。<%@ page pageEncoding="utf-8"%>
c) 在連線資料庫用的URL後加入:useUnicode=true;characterEncoding=utf-8 如:
url="jdbc:mysql:///db1?useUnicode=true&characterEncoding=utf-8",
如果是xml檔案中 url="jdbc:mysql:///db1?useUnicode=true&characterEncoding=utf-8",
四、在執行CRUD操作前先執行一下mysql_query("set names utf8");
網上很多人說要執行set names utf8,我想如果每次資料庫連線都要set names一下,原本的一句sql語句變成了兩句,很不合理嘛!到底要不要set names utf8還有待測試,因為我剛翻了Mysql5.1的文件,裡邊有寫道:
SET NAMES 'x'語句與這三個語句等價:
mysql>SET character_set_client =x;
mysql>SET character_set_results =x;
mysql>SET character_set_connection =x;
而剛才在my.ini中所設定的CLIENT SECTION中的:default-character-set=utf8就已經同時修改了client、results及connection的值為utf8了。
今天剛實驗了一下,頁面也已經設定為UTF8的話,不需要再set names一下,在資料庫客戶端SQLyog也同樣不需要!因為mysql初始化時已經從my.ini裡讀取到default_character_set=utf8並將以上三個變數都設定為utf8了。但如果頁面是用別的編碼的話,必須要set names為與之一致的編碼。
測試程式碼xxx.php如下:
<?php
header('conten-type:text/html;charset=utf-8');
mysql_connect("localhost", "root", "password") or die("Could not connect: " . mysql_error());
mysql_select_db("test");
mysql_query("set names utf8");//可以去掉這句!
$str = "CHN 軟體開發有限公司,JPN ソフトウェア開発株式會社,KOR 소프트웨어 개발 유한 공사,RUS Суд программного обеспечения".time();
$sql = "insert into tablename4 (varchar1, varbinary1 ) values ('".$str."','".$str."')";
echo $sql."<hr>";
mysql_query($sql);
$result = mysql_query("SELECT id, varchar1 ,varbinary1 FROM tablename4");
while ($row = mysql_fetch_array($result, MYSQL_BOTH)) {
printf ("ID: %s , varchar1: %s, varbinary1: %s<br>", $row[0], $row["varchar1"], $row["varbinary1"]);
}
mysql_free_result($result);
?>
如此設定之後,無論是在php頁面插入任何utf8字元,在php頁面裡取出來的,在phpMyAdmin裡取出來的,在mysql的第三方客戶端軟體裡取出來的,都是一樣的漢字了,不會再發現亂碼,mysqldump出來的也是漢字。OK,問題解決。
【CMD那些事兒】
首先,在中文windows系統下,在cmd.exe裡執行mysql.exe,有點特別。因為預設情況下,中文windows系統cmd.exe裡的字元編碼是cp936即GBK,不能顯示全部UTF8字元,所以在字元終端裡看到亂碼是正常現象,不要奇怪,這個問題在類Unix系統的shell終端裡可以解決的。
其次,由於CMD的輸入輸出格式不能改變,中文系統為GBK(可以將CMD與網頁瀏覽器對比一下,瀏覽器會根據網頁檔案的字元編碼來改變輸入輸出的字元編碼),因此,如果希望在CMD裡敲insert、select等輸入輸出中帶有中文字元的語句時,必須保證myqsl的client、connection和results的字元編碼為GBK或gb2312,方法是輸入命令:set names gbk。如果是從外部SQL檔案以Source方式匯入到資料庫的話,則要保證client、connection的字元編碼與外部SQL檔案的編碼方式一致(可用記事本開啟-->另存為
來檢視檔案的編碼格式),如果檔案的編碼格式不是GBK,例如UTF-8,則要在source之前先set names utf8; 這樣,就能正確將中文資料匯入至資料庫了(但如果此時在CMD裡select一下,仍然會顯示亂碼,原因是此時的results編碼格式為utf8,不過沒關係,儲存在mysql的資料是正確的,只是查詢結果集以utf8方式返回到編碼格式為GBK的CMD中導致亂碼而已),有點囉嗦,希望明白,呵呵!
這裡可是印象深刻啊,今天幫一個實習小妹子解決一個匯入資料庫亂碼問題,我一拿到資料庫檔案,首先檢查表語句,確認用的是UTF8編碼,然後我開始用cmd命令列方式匯入資料庫(我的資料庫中的client, connection, result, database, server的字符集都為UTF8),我先set names utf8; 然後source匯入,沒報錯,select發現中文亂碼。於是我檢視SQL檔案的編碼格式為UTF-8,心想,沒錯了啦!
百思不得期解,於是上網得知中文Windows系統下的cmd.exe的輸入格式都為GBK,於是我又set names gbk;再一次source匯入,報錯:
ERROR 1406 (22001): Data too long for column 'XXX' at row XX
ERROR 1366 (HY000): Incorrect string value: '\xAB\xA5...' for column 'XXX' at row XX
突然想起SQL檔案為UTF-8格式,嘗試將其改為ANSI,再次source, 無報錯,SELECT也顯示中文了。
細心想了一下,自認為的解釋是:
第一次匯入:set names utf8; 然後source匯入,沒報錯,select發現中文亂碼
原因:沒報錯是因為我的SQL檔案本身為UTF-8編碼,而我又set names utf8, 即client, connection, result 都為utf8, 編碼一致,可以認為資料正確儲存到資料庫了,而CMD中select為亂碼的原因為CMD的編碼無法改變,僅為GBK,儘管這時候client, connection, result都為UTF8編碼,但最終顯示的格式卻是GBK!編碼不一致,當然出現亂碼了,但我猜想這種情況下,資料庫裡的資料是沒問題的!明天再實驗一下!
經今早實驗,我把資料庫drop掉再重新按這種方式匯入,沒報錯,雖然在CMD裡select仍然發現亂碼,但在SQLyog等客戶端是正常顯示的,證明我的猜想是對的!
第二次匯入:set names gbk;再一次source匯入,報錯。
原因:這時SQL檔案的編碼格式為UTF-8,而由於set names gbk導致 client, connection, result都改變為GBK,你宣告client的輸入格式為GBK,卻拿個UTF-8格式的檔案給我匯入,輸入格式不一致導致報錯!
第三次匯入:先修改SQL檔案編碼為ANSI,最後source, 無報錯,select也是正常的。
原因:我在第二次匯入時set names gbk; 這時client, connection, result都為gbk,而且SQL檔案格式也為相容GBK,於是就成功了!
最後總結一下:同時多看看MYSQL手冊,瞭解一下MYSQL的理論,這樣遇到錯誤的時候分析也有理論基礎。還有的是建議看英文文件的,並不是說中文的不好,只是認為人總會犯錯的,翻譯過程難免有錯漏,哥今天看中文的手冊就發現有一處翻譯錯誤,而且是嚴重誤導性的,上網翻英文版的又發現中文版的漏翻了一些……