1??????????????? 概述
本文主要包括以下幾個方面:編碼基本知識, JAVA ,系統軟件, URL ,工具軟件等。在下面的描述中,將以 " 中文 " 兩個字為例,經查表可以知道其 GB2312 編碼是 "d6d0 cec4" , Unicode 編碼為 "4e2d 6587" , UTF 編碼就是 "e4b8ad e69687" 。注意,這兩個字沒有 ISO8859-1 編碼,但可以用 ISO8859-1 編碼來 " 表示 " 。
2??????????????? 編碼基本知識
最早的編碼是 ISO8859-1 ,和 ASCII 編碼相似。但為了方便表示各種各樣的語言,逐漸出現了很多標準編碼,重要的有如下幾個。
2.1????????? ISO8859-1
屬于單字節編碼,最多能表示的字符范圍是 0-255 ,應用于英文系列。比如,字母 'a' 的編碼為 0x61=97 。很明顯, ISO8859-1 編碼表示的字符范圍很窄,無法表示中文字符。但是,由于是單字節編碼,和計算機最基礎的表示單位一致,所以很多時候,仍舊使用 ISO8859-1 編碼來表示。而且在很多協議上,默認使用該編碼。比如,雖然 " 中文 " 兩個字不存在 ISO8859-1 編碼,以 GB2312 編碼為例,應該是 "d6d0 cec4" 兩個字符,使用 ISO8859-1 編碼的時候則將它拆開為 4 個字節來表示: "d6 d0 ce c4" (事實上,在進行存儲的時候,也是以字節為單位處理的)。而如果是 UTF 編碼,則是 6 個字節 "e4 b8 ad e6 96 87" 。很明顯,這種表示方法還需要以另一種編碼為基礎。
2.2????????? GB2312/GBK
這就是漢字的國標碼,專門用來表示漢字,是雙字節編碼,而英文字母和 ISO8859-1 一致(兼容 ISO8859-1 編碼)。其中 GBK 編碼能夠用來同時表示繁體字和簡體字,而 GB2312 只能表示簡體字, GBK 是兼容 GB2312 編碼的。
2.3????????? Unicode
這是最統一的編碼,可以用來表示所有語言的字符,而且是定長雙字節(也有四字節的)編碼,包括英文字母在內。所以可以說它是不兼容 ISO8859-1 編碼的,也不兼容任何編碼。不過,相對于 ISO8859-1 編碼來說, Unicode 編碼只是在前面增加了一個 0 字節,比如字母 'a' 為 "00 61" 。
需要說明的是,定長編碼便于計算機處理(注意 GB2312/GBK 不是定長編碼),而 Unicode 又可以用來表示所有字符,所以在很多軟件內部是使用 Unicode 編碼來處理的,比如 JAVA 。
2.4????????? UTF
考慮到 Unicode 編碼不兼容 ISO8859-1 編碼,而且容易占用更多的空間:因為對于英文字母, Unicode 也需要兩個字節來表示。所以 Unicode 不便于傳輸和存儲。因此而產生了 UTF 編碼, UTF 編碼兼容 ISO8859-1 編碼,同時也可以用來表示所有語言的字符,不過, UTF 編碼是不定長編碼,每一個字符的長度從 1-6 個字節不等。另外, UTF 編碼自帶簡單的校驗功能。一般來講,英文字母都是用一個字節表示,而漢字使用三個字節。
注意,雖然說 UTF 是為了使用更少的空間而使用的,但那只是相對于 Unicode 編碼來說,如果已經知道是漢字,則使用 GB2312/GBK 無疑是最節省的。不過另一方面,值得說明的是,雖然 UTF 編碼對漢字使用 3 個字節,但即使對于漢字網頁, UTF 編碼也會比 Unicode 編碼節省,因為網頁中包含了很多的英文字符。
3??????????????? JAVA 對字符的處理
在 JAVA 應用軟件中,會有多處涉及到字符集編碼,有些地方需要進行正確的設置,有些地方需要進行一定程度的處理。
3.1????????? .getBytes(String charset)
這是 JAVA 字符串處理的一個標準函數,其作用是將字符串所表示的字符按照 charset 編碼,并以字節方式表示。注意字符串在 JAVA 內存中總是按 Unicode 編碼存儲的。比如 " 中文 " ,正常情況下(即沒有錯誤的時候)存儲為 "4e2d 6587" ,如果 charset 為 "GBK" ,則被編碼為 "d6d0 cec4" ,然后返回字節 "d6 d0 ce c4" 。如果 charset 為 "UTF8" 則最后是 "e4 b8 ad e6 96 87" 。如果是 "ISO8859-1" ,則由于無法編碼,最后返回 "3f3f" (兩個問號)。
3.2????????? new String(String charset)
這是 JAVA 字符串處理的另一個標準函數,和上一個函數的作用相反,將字節數組按照 charset 編碼進行組合識別,最后轉換為 Unicode 存儲。參考上述 getBytes 的例子, "GBK" 和 "UTF8" 都可以得出正確的結果 "4e2d 6587" ,但 ISO8859-1 最后變成了 "003f003f" (兩個問號)。因為 UTF8 可以用來表示 / 編碼所有字符,所以 new String( str.getBytes( "UTF8" ), "UTF8" ) == str ,即完全可逆。
3.3????????? setCharacterEncoding()
該函數用來設置 http 請求或者相應的編碼。對于 request ,是指提交內容的編碼,指定后可以通過 getParameter() 則直接獲得正確的字符串,如果不指定,則默認使用 ISO8859-1 編碼,需要進一步處理。參見下述 " 表單輸入 " 。值得注意的是在執行 setCharacterEncoding() 之前,不能執行任何 getParameter() 。 JAVA doc 上說明: This method must be called prior to reading request parameters or reading input using getReader() 。而且,該指定只對 POST 方法有效,對 GET 方法無效。分析原因,應該是在執行第一個 getParameter() 的時候, JAVA 將會按照編碼分析所有的提交內容,而后續的 getParameter() 不再進行分析,所以 setCharacterEncoding() 無效。而對于 GET 方法提交表單是,提交的內容在 URL 中,一開始就已經按照編碼分析所有的提交內容, setCharacterEncoding() 自然就無效。對于 response ,則是指定輸出內容的編碼,同時,該設置會傳遞給瀏覽器,告訴瀏覽器輸出內容所采用的編碼。
3.4????????? 處理過程
下面分析兩個有代表性的例子,說明 JAVA 對編碼有關問題的處理方法。
????????? 表單輸入
User input? *(GBK:d6d0 cec4)? browser? *(GBK:d6d0 cec4)? web server? ISO8859-1(00d6 00d 000ce 00c4)? class ,需要在 class 中進行處理: getbytes("ISO8859-1") 為 d6 d0 ce c4 , new String("GBK") 為 d6d0 cec4 ,內存中以 Unicode 編碼則為 4e2d 6587 。
1.????? 用戶輸入的編碼方式和頁面指定的編碼有關,也和用戶的操作系統有關,所以是不確定的,上例以 GBK 為例。
2.????? 從 browser 到 web server ,可以在表單中指定提交內容時使用的字符集,否則會使用頁面指定的編碼。而如果在 url 中直接用 ? 的方式輸入參數,則其編碼往往是操作系統本身的編碼,因為這時和頁面無關。上述仍舊以 GBK 編碼為例。
3.????? Web server 接收到的是字節流,默認時( getParameter )會以 ISO8859-1 編碼處理之,結果是不正確的,所以需要進行處理。但如果預先設置了編碼(通過 request. setCharacterEncoding () ),則能夠直接獲取到正確的結果。
4.????? 在頁面中指定編碼是個好習慣,否則可能失去控制,無法指定正確的編碼。
????????? 文件編譯
假設文件是 GBK 編碼保存的,而編譯有兩種編碼選擇: GBK 或者 ISO8859-1 ,前者是中文 windows 的默認編碼,后者是 linux 的默認編碼,當然也可以在編譯時指定編碼。
Jsp? *(GBK:d6d0 cec4)? JAVA file? *(GBK:d6d0 cec4)? compiler read? uincode(GBK: 4e2d 6587; ISO8859-1: 00d6 00d 000ce 00c4)? compiler write? UTF(GBK: e4b8ad e69687; ISO8859-1: *)? compiled file? Unicode(GBK: 4e2d 6587; ISO8859-1: 00d6 00d 000ce 00c4)? class 。所以用 GBK 編碼保存,而用 ISO8859-1 編譯的結果是不正確的。
class? Unicode(4e2d 6587)? system.out / jsp.out? GBK(d6d0 cec4)? os console / browser 。
1.????? 文件可以以多種編碼方式保存,中文 windows 下,默認為 ANSI/GBK 。
2.????? 編譯器讀取文件時,需要得到文件的編碼,如果未指定,則使用系統默認編碼。一般 class 文件,是以系統默認編碼保存的,所以編譯不會出問題,但對于 jsp 文件,如果在中文 windows 下編輯保存,而部署在英文 linux 下運行 / 編譯,則會出現問題。所以需要在 jsp 文件中用 pageEncoding 指定編碼。
3.????? JAVA 編譯的時候會轉換成統一的 Unicode 編碼處理,最后保存的時候再轉換為 UTF 編碼。
4.????? 當系統輸出字符的時候,會按指定編碼輸出,對于中文 windows 下, System.out 將使用 GBK 編碼,而對于 response (瀏覽器),則使用 jsp 文件頭指定的 contentType ,或者可以直接為 response 指定編碼。同時,會告訴 browser 網頁的編碼。如果未指定,則會使用 ISO8859-1 編碼。對于中文,應該為 browser 指定輸出字符串的編碼。
5.????? browser 顯示網頁的時候,首先使用 response 中指定的編碼( jsp 文件頭指定的 contentType 最終也反映在 response 上),如果未指定,則會使用網頁中 meta 項指定中的 contentType 。
4??????????????? 幾處設置
對于 web 應用程序,和編碼有關的設置或者函數如下。
?????????? JSP 編譯
指定文件的存儲編碼,很明顯,該設置應該置于文件的開頭。例如: <%@page pageEncoding="GBK"%> 。另外,對于一般 class 文件,可以在編譯的時候指定編碼。
????????? JSP 輸出
指定文件輸出到 browser 是使用的編碼,該設置也應該置于文件的開頭。例如: <%@ page contentType="text/html; charset= GBK" %> 。該設置和 response.setCharacterEncoding("GBK") 等效。
????????? META 設置
指定網頁使用的編碼,該設置對靜態網頁尤其有作用。因為靜態網頁無法采用 jsp 的設置,而且也無法執行 response.setCharacterEncoding() 。例如: <META http-equiv="Content-Type" content="text/html; charset=GBK" />
如果同時采用了 jsp 輸出和 meta 設置兩種編碼指定方式,則 jsp 指定的優先。因為 jsp 指定的直接體現在 response 中。需要注意的是, apache 有一個設置可以給無編碼指定的網頁指定編碼,該指定等同于 jsp 的編碼指定方式,所以會覆蓋靜態網頁中的 meta 指定。所以有人建議關閉該設置。
????????? FORM 設置
當瀏覽器提交表單的時候,可以指定相應的編碼。例如: <form accept-charset= "GB2312"> 。一般不必不使用該設置,瀏覽器會直接使用網頁的編碼。
5??????????????? 系統軟件
下面討論幾個相關的系統軟件。
5.1?????????? mysql 數據庫
很明顯,要支持多語言,應該將數據庫的編碼設置成 UTF 或者 Unicode ,而 UTF 更適合與存儲。但是,如果中文數據中包含的英文字母很少,其實 Unicode 更為適合 . 數據庫的編碼可以通過 mysql 的配置文件設置,例如 default-character-set=UTF8 。還可以在數據庫鏈接 URL 中設置,例如: useUnicode=true&characterEncoding=UTF-8 。注意這兩者應該保持一致,在新的 sql 版本里,在數據庫鏈接 URL 里可以不進行設置,但也不能是錯誤的設置。
5.2????????? apache
appache 和編碼有關的配置在 httpd.conf 中,例如 AddDefaultCharset UTF-8 。如前所述,該功能會將所有靜態頁面的編碼設置為 UTF-8 ,最好關閉該功能。
另外, apache 還有單獨的模塊來處理網頁響應頭,其中也可能對編碼進行設置。
5.3????????? linux 默認編碼
這里所說的 linux 默認編碼,是指運行時的環境變量。兩個重要的環境變量是 LC_ALL 和 LANG ,默認編碼會影響到 JAVA URLEncode 的行為,下面有描述。建議都設置為 "zh_CN.UTF-8" 。
5.4????????? 其它
為了支持中文文件名, linux 在加載磁盤時應該指定字符集,例如: mount /dev/hda5 /mnt/hda5/ -t ntfs -o iocharset=GB2312 。另外,如前所述,使用 GET 方法提交的信息不支持 request.setCharacterEncoding() ,但可以通過 tomcat 的配置文件指定字符集,在 tomcat 的 server.xml 文件中,形如: <Connector ... URIEncoding="GBK"/> 。這種方法將統一設置所有請求,而不能針對具體頁面進行設置,也不一定和 browser 使用的編碼相同,所以有時候并不是所期望的。
6??????????????? URL 地址
URL 地址中含有中文字符是很麻煩的,前面描述過使用 GET 方法提交表單的情況,使用 GET 方法時,參數就是包含在 URL 中。
6.1????????? URL 編碼
對于 URL 中的一些特殊字符,瀏覽器會自動進行編碼。這些字符除了 "/?&" 等外,還包括 Unicode 字符,比如漢子。這時的編碼比較特殊。
IE 有一個選項 " 總是使用 UTF-8 發送 URL" ,當該選項有效時, IE 將會對特殊字符進行 UTF-8 編碼,同時進行 URL 編碼。如果改選項無效,則使用默認編碼 "GBK" ,并且不進行 URL 編碼。但是,對于 URL 后面的參數,則總是不進行編碼,相當于 UTF-8 選項無效。比如 " 中文 .html?a= 中文 " ,當 UTF-8 選項有效時,將發送鏈接 "%e4%b8%ad%e6%96%87.html?a=\x4e\x2d\x65\x87" ;而 UTF-8 選項無效時,將發送鏈接 "\x4e\x2d\x65\x87.html?a=\x4e\x2d\x65\x87" 。注意后者前面的 " 中文 " 兩個字只有 4 個字節,而前者卻有 18 個字節,這主要是 URL 編碼的原因。
當 web server 接收到該鏈接時,將會進行 URL 解碼,即去掉 "%" ,同時按照 ISO8859-1 編碼(上面已經描述,可以使用 URLEncoding 來設置成其它編碼)識別。上述例子的結果分別是 "\ue4\ub8\uad\ue6\u96\u87.html?a=\u4e\u2d\u65\u87" 和 "\u4e\u2d\u65\u87.html?a=\u4e\u2d\u65\u87" ,注意前者前面的 " 中文 " 兩個字恢復成了 6 個字符。這里用 "\u" ,表示是 Unicode 。
所以,由于客戶端設置的不同,相同的鏈接,在服務器上得到了不同結果。這個問題不少人都遇到,卻沒有很好的解決辦法。所以有的網站會建議用戶嘗試關閉 UTF-8 選項。不過,下面會描述一個更好的處理辦法。
6.2????????? rewrite
熟悉的人都知道, apache 有一個功能強大的 rewrite 模塊,這里不描述其功能。需要說明的是該模塊會自動將 URL 解碼(去除 % ),即完成上述 web server 的部分功能。有相關文檔介紹說可以使用 [NE] 參數來關閉該功能,但我試驗并未成功,可能是因為版本(我使用的是 apache 2.0.54 )問題。另外,當參數中含有 "?& " 等符號的時候,該功能將導致系統得不到正常結果。 rewrite 本身似乎完全是采用字節處理的方式,而不考慮字符串的編碼,所以不會帶來編碼問題。
6.3????????? URLEncode.encode()
這是 JAVA 本身提供對的 URL 編碼函數,完成的工作和上述 UTF-8 選項有效時瀏覽器所做的工作相似。值得說明的是, JAVA 已經不贊成不指定編碼來使用該方法( deprecated )。應該在使用的時候增加編碼指定。當不指定編碼的時候,該方法使用系統默認編碼,這會導致軟件運行結果得不確定。比如對于 " 中文 " ,當系統默認編碼為 "GB2312" 時,結果是 "%4e%2d%65%87" ,而默認編碼為 "UTF-8" ,結果卻是 "%e4%b8%ad%e6%96%87" ,后續程序將難以處理。另外,這兒說的系統默認編碼是由運行時的環境變量 LC_ALL 和 LANG 等決定的,曾經出現過 Web Server 重啟后就出現亂碼的問題,最后才郁悶的發現是因為修改修改了這兩個環境變量。建議統一指定為 "UTF-8" 編碼,可能需要修改相應的程序。
6.4????????? 一個解決方案
上面說起過,因為瀏覽器設置的不同,對于同一個鏈接, web server 收到的是不同內容,而軟件系統有無法知道這中間的區別,所以這一協議目前還存在缺陷。
針對具體問題,不應該僥幸認為所有客戶的 IE 設置都是 UTF-8 有效的,也不應該粗暴的建議用戶修改 IE 設置,要知道,用戶不可能去記住每一個 web server 的設置。所以,接下來的解決辦法就只能是讓自己的程序多一點智能:根據內容來分析編碼是否 UTF-8 。
比較幸運的是 UTF-8 編碼相當有規律,所以可以通過分析傳輸過來的鏈接內容,來判斷是否是正確的 UTF-8 字符,如果是,則以 UTF-8 處理之,如果不是,則使用客戶默認編碼(比如 "GBK" ),下面是一個判斷是否 UTF-8 的例子,如果你了解相應規律,就容易理解。
public static boolean isValidUTF8(byte[] b,int aMaxCount){
int lLen=b.length,lCharCount=0;
for(int i=0;i<lLen && lCharCount<aMaxCount;++lCharCount){
byte lByte=b[i++];//to fast operation, ++ now, ready for the following for(;;)
??? if(lByte>=0) continue;//>=0 is normal ascii
??????? if(lByte<(byte)0xc0 || lByte>(byte)0xfd) return false;
?????? int lCount=lByte>(byte)0xfc?5:lByte>(byte)0xf8?4
???????????????????? :lByte>(byte)0xf0?3:lByte>(byte)0xe0?2:1;
?????? if(i+lCount>lLen) return false;
?????? for(int j=0;j<lCount;++j,++i) if(b[i]>=(byte)0xc0) return false;
?}
?return true;
}
?
?
?
相應地,一個使用上述方法的例子如下:
public static String getUrlParam(String aStr,String aDefaultCharset)
throws UnsupportedEncodingException{
?????? if(aStr==null) return null;
?????? byte[] lBytes=aStr.getBytes("ISO-8859-1");
?????? return new String(lBytes,StringUtil.isValidUTF8(lBytes)?"UTF8":aDefaultCharset);
}
?
?
不過,該方法也存在缺陷,如下兩方面:
1.????? 沒有包括對用戶默認編碼的識別,這可以根據請求信息的語言來判斷,但不一定正確,因為我們有時候也會輸入一些韓文,或者其他文字。
2.????? 可能會錯誤判斷 UTF-8 字符,一個例子是 " 學習 " 兩個字,其 GBK 編碼是 " \xd1\xa7\xcf\xb0" ,如果使用上述 isValidUTF8 方法判斷,將返回 true 。可以考慮使用更嚴格的判斷方法,不過估計效果不大。
有一個例子可以證明 google 也遇到了上述問題,而且也采用了和上述相似的處理方法,比如,如果在地址欄中輸入 "
最后,應該補充說明一下,如果不使用 rewrite 規則,或者通過表單提交數據,其實并不一定會遇到上述問題,因為這時可以在提交數據時指定希望的編碼。另外,中文文件名確實會帶來問題,應該謹慎使用。
7??????????????? 其它
下面描述一些和編碼有關的其他問題。
7.1????????? SecureCRT
除了瀏覽器和控制臺與編碼有關外,一些客戶端也很有關系。比如在使用 SecureCRT 連接 linux 時,應該讓 SecureCRT 的顯示編碼(不同的 session ,可以有不同的編碼設置)和 linux 的編碼環境變量保持一致。否則看到的一些幫助信息,就可能是亂碼。
另外, mysql 有自己的編碼設置,也應該保持和 SecureCRT 的顯示編碼一致。否則通過 SecureCRT 執行 sql 語句的時候,可能無法處理中文字符,查詢結果也會出現亂碼。
對于 UTF-8 文件,很多編輯器(比如記事本)會在文件開頭增加三個不可見的標志字節,如果作為 mysql 的輸入文件,則必須要去掉這三個字符。(用 linux 的 vi 保存可以去掉這三個字符)。一個有趣的現象是,在中文 windows 下,創建一個新 txt 文件,用記事本打開,輸入 " 連通 " 兩個字,保存,再打開,你會發現兩個字沒了,只留下一個小黑點。
7.2????????? 過濾器
如果需要統一設置編碼,則通過 filter 進行設置是個不錯的選擇。在 filter class 中,可以統一為需要的請求或者回應設置編碼。參加上述 setCharacterEncoding() 。這個類 apache 已經給出了可以直接使用的例子 SetCharacterEncodingFilter 。
7.3????????? POST 和 GET
很明顯,以 POST 提交信息時, URL 有更好的可讀性,而且可以方便的使用 setCharacterEncoding() 來處理字符集問題。但 GET 方法形成的 URL 能夠更容易表達網頁的實際內容,也能夠用于收藏。
從統一的角度考慮問題,建議采用 GET 方法,這要求在程序中獲得參數是進行特殊處理,而無法使用 setCharacterEncoding() 的便利,如果不考慮 rewrite ,就不存在 IE 的 UTF-8 問題,可以考慮通過設置 URIEncoding 來方便獲取 URL 中的參數。
7.4????????? 簡繁體編碼轉換
GBK 同時包含簡體和繁體編碼,也就是說同一個字,由于編碼不同,在 GBK 編碼下屬于兩個字。有時候,為了正確取得完整的結果,應該將繁體和簡體進行統一。可以考慮將 UTF 、 GBK 中的所有繁體字,轉換為相應的簡體字, BIG5 編碼的數據,也應該轉化成相應的簡體字。當然,仍舊以 UTF 編碼存儲。
例如,對于 " 語言 語 言 " ,用 UTF 表示為 "\xE8\xAF\xAD\xE8\xA8\x80 \xE8\xAA\x9E\xE8\xA8\x80" ,進行簡繁體編碼轉換后應該是兩個相同的 "\xE8\xAF\xAD\xE8\xA8\x80>" 。