文章目錄
四種編碼
1. 網頁檔案儲存體時使用的編碼,比如我們使用vim作為編碼時,可以通過設定fileencoding屬性來設定儲存的檔案使用的編碼,set filetype=utf-8,即將檔案儲存為UTF-8編碼。
2. meta標籤當中設定的編碼,有http-equiv=”Content-Type”content=”text/html; charset=utf-8”這樣的屬性,則表示設定當前頁的編碼為utf-8。
3. 瀏覽器查看時使用的編碼,即瀏覽器當中的查看=>為編碼選項當中選中的編碼。
4. 瀏覽器端提交使用者資料時使用的編碼。
原理
1. 網頁檔案儲存體編碼,是網頁最為重要的編碼。如果網頁檔案為靜態HTML檔案,則Web Server將直接發送該檔案至用戶端的瀏覽器;如果網頁檔案為動態產生的HTML檔案,則Web Server會根據動態指令檔儲存的編碼來產生相應編碼的資料,而這些資料將成為發送到Client Browser的HTML檔案。
例如在一個以gbk編碼存放的PHP指令碼當中,使用echo ‘我愛你’,則會產生資料CE D2 B0 AE C4 E3六個位元組的資料,這六個位元組的資料是‘我愛你’的GBK編碼,而如果在一個以utf-8編碼存放的PHP指令碼當中,執行echo ‘我愛你’,則會產生資料E6 88 91 E7 88 B1 E4 BD A0九個位元組的資料,這九個位元組的資料是‘我愛你’的UTF-8編碼。
2. HTML 4.01 Specification當中說明:在META標籤的Content-Type值當中使用的charset用於表明當前是傳送的HTML文檔的編碼,並且說明一個conforming的browser正確的處理這個屬性。但是實際上的情況是,大部分的browser並不會把這個屬性當會事,經測試Firefox 10, Chrome 17並不會follow這個屬性,所以會出現一個明明是UTF-8編碼的HTML檔案,並且在meta
charset當中設計成為了UTF-8,但是還是會出現亂碼的原因。現在(2012-4-2)作出的測試(本地,使用FILE或者HTTP協議開啟)發會現browser(IE 9.0+FF11+Chrome17)會按照這個屬性來解析HTML檔案。
3. 瀏覽器查看編碼是瀏覽器將Web Server傳輸過來的資料解碼來使用的編碼。出來亂碼的原因即在於此,如果一個HTML發過來的是GBK編碼的,而Web Browser使用UTF-8去解碼這個檔案,那麼如果該檔案當中含有中文等字元,則會產生亂碼。
4. 經過測試發現,瀏覽器端提交使用者資料時使用的編碼,只取決於當前瀏覽器查看網頁使用的編碼,與HTML網頁本身的檔案的編碼沒有任何關係。
總結
服務端傳輸過來的HTML檔案的編碼主要由服務端HTML檔案或者指令檔的儲存編碼決定,瀏覽器端傳輸的資料的編碼只由瀏覽器端的查看編碼決定。
另外,HTTP包頭當中的Content-Type屬性當中的charset也可以表明服務端傳輸的資料的編碼,但是一般的Web Server並不會發送這個charset屬性。
最後,一般的現代的瀏覽器都具備一個編碼自動檢查功能,瀏覽器會根據當前收到的資料來檢查編碼。
應用
理解為什麼php的move_uploaded_file有時候不支援中文檔案名稱?
問題再現
如果存在這樣一個圖片上傳的幕後處理模組,
<?php//這裡假設用戶端是以UTF-8編碼發送的資料,即查看上傳檔案網頁時,使用的是UTF-8$old_name = $_FILES['file']['name']; $new_name = 'e:\web\\' . $old_name;;move_uploaded_file($_FILES['file']['tmp_name'], $new_name);move_uploaded_file($_FILES['file']['tmp_name'], 'e:\web\study\哈哈.jpg');?>
如果該模組(upload.php)使用UTF-8作為檔案儲存體編碼,那麼代碼中的兩種上傳檔案的方式都應該避免,最好是能夠隨機產生上傳後的新檔案名稱,並且新產生的檔案名稱最好不包括中文。
因為:
第一,一般的Web Server(如httpd)並不能夠很好的處理包括中文的路徑名處理,也就是說其實apache httpd對於上傳上來的utf-8編碼的檔案名稱並不能夠作出正確的處理,其結果是$_FILES['file']['name']會出現亂碼。這樣,首先舊檔案名稱就是錯的。
其次,php的move_uploaded_file並不能夠根據當前的檔案儲存體編碼來處理檔案路徑。move_uploaded_file會把轉入的參數作為當前locale編碼來處理(即GBK),這樣的結果就是再次出現亂碼。
例如 'e:\web\study\哈哈.jpg'本身作為UTF-8的編碼是
65 3a 5c 77 65 62 5c 73 74 75 64 79 5c e5 93 88 e5 93 88 2e 6a 70 67
而move_uploaded_file會將其作為GBK編碼來看待,其結果就是"e:\web\study\鍝堝搱.jpg",這樣的結果,因為e5 93是'鍝',接下來'堝'是88 e5,最後'搱'是93 88,也就是說'哈哈'的六個UTF-8位元組,被解釋為三個GBK編碼的漢字。其結果就是上傳的檔案變成了"e:\web\study\鍝堝搱.jpg",而不是原本的哈哈.jpg。更有時候,會出現UTF-8編碼的字不能夠被解釋為GBK時,系統會預設的添加一個?號作為預設字元,其結果就是上傳失敗。
所以,如果你一定想使用中文檔案名稱,那麼在UTF-8編碼儲存的PHP檔案當中,一定要先使用iconv將utf-8編碼的路徑轉換為當前locale(gbk)編碼,然後再調用move_uploaded_file。
深入探索
那麼為什麼move_uploaded_file會將utf-8編碼的檔案路徑,認為是gbk編碼的呢?這是不是PHP的一個bug呢?
我們來實際看一下php的原始碼,
/* {{{ proto bool move_uploaded_file(string path, string new_path) Move a file if and only if it was created by an upload */PHP_FUNCTION(move_uploaded_file){char *path, *new_path;int path_len, new_path_len;zend_bool successful = 0;#ifndef PHP_WIN32int oldmask; int ret;#endifif (!SG(rfc1867_uploaded_files)) {RETURN_FALSE;}if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &path, &path_len, &new_path, &new_path_len) == FAILURE) {return;}if (!zend_hash_exists(SG(rfc1867_uploaded_files), path, path_len + 1)) {RETURN_FALSE;}if (PG(safe_mode) && (!php_checkuid(new_path, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {RETURN_FALSE;}if (php_check_open_basedir(new_path TSRMLS_CC)) {RETURN_FALSE;}if (strlen(path) != path_len) {RETURN_FALSE;}if (strlen(new_path) != new_path_len) {RETURN_FALSE;}VCWD_UNLINK(new_path);if (VCWD_RENAME(path, new_path) == 0) {successful = 1;#ifndef PHP_WIN32oldmask = umask(077);umask(oldmask);ret = VCWD_CHMOD(new_path, 0666 & ~oldmask);if (ret == -1) {php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s", strerror(errno));}#endif} else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR TSRMLS_CC) == SUCCESS) {VCWD_UNLINK(path);successful = 1;}if (successful) {zend_hash_del(SG(rfc1867_uploaded_files), path, path_len + 1);} else {php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to move '%s' to '%s'", path, new_path);}RETURN_BOOL(successful);}/* }}} */
可以看到做主要工作的是VCWD_RENAME,
#define VCWD_RENAME(oldname, newname) virtual_rename(oldname, newname TSRMLS_CC)
而virtual_name,
CWD_API int virtual_rename(char *oldname, char *newname TSRMLS_DC) /* {{{ */{cwd_state old_state;cwd_state new_state;int retval;int cch, cb;LPWSTR wstr;LPSTR mbstr;CWD_STATE_COPY(&old_state, &CWDG(cwd));if (virtual_file_ex(&old_state, oldname, NULL, CWD_EXPAND)) {CWD_STATE_FREE(&old_state);return -1;}oldname = old_state.cwd;CWD_STATE_COPY(&new_state, &CWDG(cwd));if (virtual_file_ex(&new_state, newname, NULL, CWD_EXPAND)) {CWD_STATE_FREE(&old_state);CWD_STATE_FREE(&new_state);return -1;}newname = new_state.cwd;/* rename on windows will fail if newname already exists. MoveFileEx has to be used */#ifdef TSRM_WIN32/* MoveFileEx returns 0 on failure, other way 'round for this function */retval = (MoveFileEx(oldname, mbstr, MOVEFILE_REPLACE_EXISTING|MOVEFILE_COPY_ALLOWED) == 0) ? -1 : 0;if (retval == -1) {php_error_docref(NULL TSRMLS_CC, 2, "movefileex failed");}#elseretval = rename(oldname, newname);#endifCWD_STATE_FREE(&old_state);CWD_STATE_FREE(&new_state);return retval;}/* }}} */
可以看到最終move_uploaded_file是調用MoveFileEx來實現檔案的上傳,也就是將UTF-8編碼的路徑名當成GBK編碼來處理是的MoveFileEx函數,那麼為什麼會出現這種結果呢?
因為PHP預設的所有Windows API的調用都是使用的ANSI版本,也就是說MoveFileEx即MoveFileExA,其參數自然就是GBK編碼的字串(最終系統通過MultiByteToWideChar來將GBK編碼的字串轉換成為UTF-16 LE字串,來調用MoveFileExW)。
所以,如果要在底層通過寫入程式碼來解決這個問題,可以在MoveFileEx之前添加如下代碼:
/* first convert utf-8 to utf16-le */cch = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)newname, strlen(newname) + 1, NULL, 0);wstr = (LPWSTR)malloc(cch * sizeof(wchar_t));MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)newname, strlen(newname) + 1, wstr, cch * sizeof(wchar_t));/* then convert utf16-le to gbk */cb = WideCharToMultiByte(CP_ACP, 0, wstr, cch, NULL, 0, NULL, NULL);mbstr = (LPSTR)malloc(cb);WideCharToMultiByte(CP_ACP, 0, wstr, cch, mbstr, cb, NULL, NULL);free(wstr);
將UTF-8編碼的字串,轉換為GBK。
從這裡可以看出來,在PHP或者其他系統當中盡量不要使用中文作為檔案名稱(如果可以的話),因為中文編碼的檔案操作很容易出現相容性問題。