1. 程式人生 > >Mysql 的字元編碼機制、中文亂碼問題及解決方案

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&amp;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的理論,這樣遇到錯誤的時候分析也有理論基礎。還有的是建議看英文文件的,並不是說中文的不好,只是認為人總會犯錯的,翻譯過程難免有錯漏,哥今天看中文的手冊就發現有一處翻譯錯誤,而且是嚴重誤導性的,上網翻英文版的又發現中文版的漏翻了一些……