MySQL 4.1 字符集支援的原理(轉)

BSDLite發表於2007-08-11
MySQL 4.1 字符集支援的原理(轉)[@more@]下面要寫的是一篇非常無聊的東西,充斥了大量各式各樣的編碼、轉換、客戶端、伺服器端、連線……呃,我自己都不願意去看它,但想一想,寫下來還是有點意義的,原因有四:

MySQL 4.1 對多語言的支援有了很大變化 (這導致了問題的出現);
儘管大部分的地方 (包括個人使用和主機提供商),MySQL 3 仍然占主導地位;但 MySQL 4.1 是 MySQL 官方推薦的資料庫,已經有主機提供商開始提供並將會越來越多;
許多 PHP 程式以 MySQL 作為預設的資料庫管理軟體,但它們一般不區分 MySQL 4.1 與 4.1 以下版本的區別,籠統地稱“MySQL 3.xx.xx 以上版本”就滿足安裝需求了;
因為 latin1 在許多地方 (下邊會詳細描述具體是哪些地方) 作為預設的字符集,成功的矇蔽了許多 PHP 程式的開發者和使用者,掩蓋了在中文等語言環境下會出現的問題;
簡單的說,MySQL 自身的變化和使用 MySQL 的 PHP 程式對此忽略,導致了問題的出現和複雜化,而由於大部分使用者使用的是英文,使這種問題不被重視。這裡提到的 PHP 程式,主要就 WordPress 而言。

MySQL 4.1 字符集支援的原理

MySQL 4.1 對於字符集的指定可以細化到一臺機器上安裝的 MySQL,其中的一個資料庫,其中的一張表,其中的一欄,應該用什麼字符集。但是,傳統的 Web 程式在建立資料庫和資料表時並沒有使用那麼複雜的配置,它們用的是預設的配置,那麼,預設的配置從何而來呢?

編譯 MySQL 時,指定了一個預設的字符集,這個字符集是 latin1;
安裝 MySQL 時,可以在配置檔案 (my.ini) 中指定一個預設的的字符集,如果沒指定,這個值繼承自編譯時指定的;
啟動 mysqld 時,可以在命令列引數中指定一個預設的的字符集,如果沒指定,這個值繼承自配置檔案中的;
此時 character_set_server 被設定為這個預設的字符集;
當建立一個新的資料庫時,除非明確指定,這個資料庫的字符集被預設設定為 character_set_server;
當選定了一個資料庫時,character_set_database 被設定為這個資料庫預設的字符集;
在這個資料庫裡建立一張表時,表預設的字符集被設定為 character_set_database,也就是這個資料庫預設的字符集;
當在表內設定一欄時,除非明確指定,否則此欄預設的字符集就是表預設的字符集;
這個字符集就是資料庫中實際儲存資料採用的字符集,mysqldump 出來的內容就是這個字符集下的;
簡單的總結一下,如果什麼地方都不修改,那麼所有的資料庫的所有表的所有欄位的都用 latin1 儲存,不過我們如果安裝 MySQL,一般都會選擇多語言支援,也就是說,安裝程式會自動在配置檔案中把 default_character_set 設定為 UTF-8,這保證了預設情況下,所有的資料庫的所有表的所有欄位的都用 UTF-8 儲存。

當一個 PHP 程式與 MySQL 建立連線後,這個程式傳送給 MySQL 的資料採用的是什麼字符集?MySQL 無從得知 (它最多隻能猜測),所以 MySQL 4.1 要求客戶端必須指定這個字符集,也就是 character_set_client,MySQL 的怪異之處在於,得到的這個字符集並不立即轉換為儲存在資料庫中的那個字符集,而是先轉換為 character_set_connection 變數指定的一個字符集;這個 connection 層究竟有什麼用我不大明白,但轉換為 character_set_connection 的這個字符集之後,還要轉換為資料庫預設的字符集,也就是說要經過兩次轉換;當這個資料被輸出時,又要由資料庫預設的字符集轉換為 character_set_results 指定的字符集。

一個典型的環境

典型的環境以我自己的電腦上安裝的 MySQL 4.1 為例,我自己的電腦上安裝著 Apache 2,PHP 5 和 WordPress 1.5.1.3,MySQL 配置檔案中指定了 default_character_set 為 utf8。於是問題出現了:

WordPress 按照預設情況安裝,所以所有的表都用 UTF-8 儲存資料;
WordPress 預設採用的瀏覽字符集是 UTF-8 (Options->Reading 中設定),因此所有 WP 頁面的 meta 中會說明 charset 是 utf-8;
所以瀏覽器會以 utf-8 方式顯示所有的 WP 頁面;這樣一來 Write 的所有 Post,和 Comment 都會以 UTF-8 格式從瀏覽器傳送給 Apache,再由 Apache 交給 PHP;
所以 WP 從所有的表單中得到的資料都是 utf-8 編碼的;WP 不加轉換的直接把這些資料傳送給 MySQL;
MySQL 預設設定的 character_set_client 和 character_set_connection 都是 latin1,此時怪異的事情發生了,實際上是 utf-8 格式的資料,被“當作 latin1”轉換成……居然還是轉換成 latin1,然後再由這個 latin1 轉換成 utf-8,這麼兩次轉換,有一部分 utf-8 的字元就丟失了,變成 ??,最後輸出的時候 character_set_results 預設是 latin1,也就輸出為奇怪的東西了。
最神奇的還不是這個,如果 WordPress 中設定以 GB2312 格式閱讀,那麼 WP 傳送給 MySQL 的 GB2312 編碼的資料,被“當作 latin1”轉換後,存進資料庫的是一種奇怪的格式 (真的是奇怪的格式,mysqldump 出來就能發現,無論當作 utf-8 還是當作 gb2312 來讀都是亂碼),但如果這種格式以 latin1 輸出出來,居然又能變回 GB2312!

這會導致什麼現象呢?WP 如果使用 MySQL 4.1 資料庫,把編碼改用 GB2312 就正常了,可惜,這種正常只是貌似正常。

如何解決問題

如果你已經不耐煩了 (幾乎是肯定的),google 一下,會發現絕大部分的解答是,query 之前先執行一下:SET NAMES 'utf8',沒錯,這是解決方案,但本文的目的是說明,這為什麼是解決方案。

要保證結果正確,必須保證資料表採用的格式是正確的,也就是說,至少能夠存放所有的漢字,那麼我們只有兩種選擇,gbk 或者 utf-8,下面討論 utf-8 的情況。

因為配置檔案設定的 default_character_set 是 utf8,資料表預設採用的就是 utf-8 建立的。這也應該是所有采用 MySQL 4.1 的主機提供商應該採用的配置。所以我們要保證的只是客戶端與 MySQL 互動之間指定編碼的正確。

這隻有兩種可能,客戶端以 gb2312 格式傳送資料,或者以 utf-8 格式傳送資料。

如果以 gb2312 格式傳送:

SET character_set_client='gb2312'
SET character_set_connection='utf8' 或者
SET character_set_connection='gb2312'
都是可以的,都能夠保證資料在編碼轉換中不出現丟失,也就是保證儲存入資料庫的是正確的內容。

怎麼保證取出的是正確的內容呢?考慮到絕大部分客戶端 (包括 WP),傳送資料的編碼也就是它所希望收到資料的編碼,所以:

SET character_set_results='gb2312'
可以保證取出給瀏覽器顯示的格式就是 gb2312。

如果是第二種情況,客戶端以 utf-8 格式傳送 (WP 的預設情況),可以採用下述配置:

SET character_set_client='utf8'
SET character_set_connection='utf8'
SET character_set_results='utf8'
這個配置就等價於 SET NAMES 'utf8'。

WP 應該作什麼修改

還是那句話,客戶端要發給資料庫什麼編碼的資料,資料庫是不可能確切知道的,只能讓客戶端自己說明白,所以,WP 是必須傳送正確的 SET... 給 MySQL 的。怎麼傳送最合適呢?臺灣的 pLog 同仁給出了一些建議:

首先,測試伺服器是否 >= 4.1,編譯時是否加入了 UTF-8 支援;是則繼續
然後測試資料庫以什麼格式儲存 ($dbEncoding);
SET NAMES $dbEncoding
對於第二點,WP 的情況是不同的,按照上面的典型配置,只要用 WP,肯定資料庫是用 UTF-8 儲存的,所以要根據使用者設定的以 GB2312 還是 UTF-8 瀏覽來判斷 (bloginfo('charset')),但這個值是要連線資料庫以後才能得到的,所以效率最高的方式是連線資料庫之後,根據這個配置設定一次 SET NAMES,而不必每次查詢之前都設定一遍。

我的修改方式是這樣的,在 wp_includes/wp-db.php 中增加:

function set_charset($charset)
{
// check mysql version first.
$serverVersion = mysql_get_server_info($this->dbh);
$version = explode('.', $serverVersion);
if ($version[0] < 4) return;

// check if utf8 support was compiled in
$result = mysql_query("SHOW CHARACTER SET like 'utf8'",
$this->dbh);
if (mysql_num_rows($result) < = 0) return;

if ($charset == 'utf-8' || $charset == 'UTF-8')
$charset = 'utf8';
@mysql_query("SET NAMES '$charset'", $this->dbh);
}
在 wp-settings.php 的 require (ABSPATH . WPINC . '/vars.php'); 後增加:

$wpdb->set_charset(get_bloginfo('charset'));
Posted in : Server-Side Author : jjgod

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-947289/,如需轉載,請註明出處,否則將追究法律責任。

相關文章