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