章從實(shí)際的中文問(wèn)題中,分析問(wèn)題的根本原因,以及解決之道。
注意,本章雖然著重說(shuō)明“中文問(wèn)題”,但本章所推出的結(jié)論卻是適合于世界所有語(yǔ)言文字的。 概述我們?cè)趯?shí)際開發(fā)中碰到的中文問(wèn)題,真是形形色色,無(wú)法一一列舉。但是它們不是隨機(jī)產(chǎn)生的,而是有規(guī)律可循,有辦法解決的。 我們碰到最多的中文問(wèn)題,都發(fā)生在使用Java Servlet寫WEB應(yīng)用時(shí)。其次,使用Java Mail API發(fā)送e-mail也會(huì)有類似的問(wèn)題。從表象上區(qū)分,大致上有以下幾種:
要分析這些問(wèn)題的根本原因,首先要了解這些中文字符的輸入源,其次是了解這些字符被輸出到用戶瀏覽器經(jīng)過(guò)了哪些轉(zhuǎn)換和輸出環(huán)節(jié)。中文字符可以來(lái)源于:
中文字符被裝入內(nèi)存以后,還要經(jīng)過(guò)若干個(gè)轉(zhuǎn)換和輸出環(huán)節(jié),最后才能到達(dá)用戶的瀏覽器被用戶看到。
以上列舉的任何一個(gè)環(huán)節(jié)發(fā)生錯(cuò)誤,都可能產(chǎn)生“亂碼”現(xiàn)象。因此發(fā)生亂碼現(xiàn)象時(shí),不要慌,想想這個(gè)亂碼的文本是從哪里來(lái)的,又是以什么方式輸出的。 字符的輸入、轉(zhuǎn)換、輸出環(huán)節(jié)內(nèi)嵌在程序代碼中的中文因?yàn)?/span>Java源代碼(.java)本身是一個(gè)文本文件,所以和讀普通文本文件一樣,編譯器(javac)必須以字節(jié)流的方式讀入文件內(nèi)容,并以適當(dāng)?shù)木幋a轉(zhuǎn)換成Unicode字符而存儲(chǔ)在Java字節(jié)碼文件(.class)中。例如:Java源代碼文件中包含GBK編碼的中文字符,則使用下面的命令編譯:
如果不指定 -encoding參數(shù),javac會(huì)使用系統(tǒng)默認(rèn)的編碼:在中文Windows上,默認(rèn)是GBK,在英文Linux上,默認(rèn)是ISO-8859-1。因此,如果文件是在英文Linux下編譯而未指定-encoding,那么文件中的中文“我愛Alibaba”就會(huì)變成“ÎÒ°®Alibaba”了。從文本文件中讀入的字符正如前面的 TestDecoding例子所示,在讀入文本文件時(shí),需要指定正確的編碼。如果不指明編碼,那么Java就會(huì)使用系統(tǒng)默認(rèn)的編碼來(lái)轉(zhuǎn)換文件中的字節(jié)流。下列代碼往往會(huì)產(chǎn)生問(wèn)題:
從XML文件中讀入的字符XML標(biāo)準(zhǔn)極為嚴(yán)格地遵守Unicode標(biāo)準(zhǔn)。XML文件的字符編碼是定義在XML文件中,而不是定義在XML解析器中的。如果不明確指定,任何標(biāo)準(zhǔn)的XML解析總是以UTF-8的方式解碼XML文件??梢杂孟旅娴姆绞皆?/span>XML文件中指定字符編碼:
在解碼過(guò)程中,如果 XML解析器發(fā)現(xiàn)一個(gè)非法的字節(jié),不會(huì)像Java一樣,轉(zhuǎn)換成問(wèn)號(hào)“?”,而是立即報(bào)錯(cuò)。所以XML解析器一般總會(huì)取得正確的Unicode字符。注意 注意,XML規(guī)范并沒有定義GBK和GB18030編碼,因此不能在XML文件使用這兩種編碼。目前可以使用的中文編碼是GB2312和BIG5。相信這種情況以后會(huì)改變。如果確實(shí)想使用中文大字符集,請(qǐng)指定UTF-8作為XML文件的編碼。 數(shù)據(jù)庫(kù)首先,數(shù)據(jù)庫(kù)一般都可以設(shè)置以何種字符編碼方式存儲(chǔ)文本;其次,數(shù)據(jù)庫(kù)的客戶端 —— JDBC驅(qū)動(dòng) —— 必須設(shè)置成和數(shù)據(jù)庫(kù)的內(nèi)置字符編碼一致;最后,盡可能使用UTF-8存取文本數(shù)據(jù),因?yàn)檫@樣可以在數(shù)據(jù)庫(kù)中方便地存儲(chǔ)所有國(guó)家的文字。 注意 我們Alibaba的Oracle數(shù)據(jù)庫(kù)目前采用7位ASCII碼存儲(chǔ)文本(包括中文),這是一個(gè)極大的錯(cuò)誤,已經(jīng)導(dǎo)致了很多問(wèn)題。我們后面會(huì)講到。 模板文件Velocity和WebMacro是常用的Java模板系統(tǒng)。模板文件也是簡(jiǎn)單的文本文件。Velocity和WebMacro都可以在各自的配置文件定義讀取模板所用的字符編碼方式。例如Velocity可以這樣設(shè)置:
如果是在 Turbine(一種基于MVC設(shè)計(jì)模式的WEB應(yīng)用框架,http://jakarta./turbine)中調(diào)用Velocity,可以在Turbine的配置文件中設(shè)置:
這樣 Velocity就可以用GBK編碼讀取模板文件。JSP頁(yè)面JSP是一種特殊的WEB頁(yè)面,在第一次使用時(shí),被自動(dòng)編譯成一個(gè)普通的servlet。在JSP的開頭指定JSP的字符編碼:
上面這行告訴 JSP:
2. 使用GBK編碼輸出JSP servlet中的所有字符,相當(dāng)于:
3. 使用GBK解碼用戶的表單輸入,相當(dāng)于:
瀏覽器是根據(jù)頁(yè)面的content type來(lái)決定以何種方式來(lái)編碼用戶輸入的表單的。例如,一個(gè)頁(yè)面的content type是text/html; charset=GBK,那么,當(dāng)用戶按下頁(yè)面中的submit按鈕時(shí),瀏覽器自動(dòng)將用戶的輸入用GBK方式編碼并發(fā)送回服務(wù)器端。服務(wù)器接到用戶的請(qǐng)求后,需要用正確的方式來(lái)解碼,方法是:
然后再調(diào)用 request.getParameter(parameterName)時(shí)就可以得到正確的Unicode字符。注意 必須在第一次調(diào)用request.getParameter(parameterName)之前調(diào)用request.setCharacterEncoding(charset),因?yàn)榻獯a是在第一次調(diào)用request.getParameter(parameterName)時(shí)發(fā)生的。 Servlet規(guī)范規(guī)定,如果沒有設(shè)定request.setCharacterEncoding,則使用ISO-8859-1來(lái)解碼用戶輸入的表單,而不是使用系統(tǒng)默認(rèn)的編碼。 對(duì)于multipart form(例如,上傳圖片的form表單),情況要復(fù)雜一些。因?yàn)?/span>servlet并沒有直接支持multipart form。所以大多數(shù)應(yīng)用程序使用了第三方的工具包來(lái)解析multipart form,例如:Oreilly COS工具包。然而,這些工具包大多使用系統(tǒng)默認(rèn)的編碼來(lái)解析用戶表單,和servlet規(guī)范不一致。如果你的servlet代碼沒有特別指明編碼方式,則兩種form表單將有不同的表現(xiàn),必有一種情況會(huì)出現(xiàn)亂碼現(xiàn)象。 Servlet輸出Servlet可以用兩種方式向?yàn)g覽器輸出內(nèi)容:
在此我們只討論輸出文本的情形:response.getWriter()。在調(diào)用response.getWriter()前,我們必須設(shè)置content type:
response.getWriter() 通過(guò)content type中指定的字符編碼來(lái)決定如何將字符流轉(zhuǎn)換成字節(jié)流。在Turbine中,在配置文件中指定下面的內(nèi)容,Turbine會(huì)為你自動(dòng)設(shè)置content type:
瀏覽器收到從WEB服務(wù)器返回的頁(yè)面時(shí),
2. 如果HTTP響應(yīng)中沒有指定字符集,那么瀏覽器會(huì)檢查HTML頁(yè)面中是否包含:
如果找到,則使用這里指定的字符編碼。
其它輸出環(huán)節(jié)文本還可能被寫入XML文件、文本文件、數(shù)據(jù)庫(kù)中。類似的,輸出文件時(shí)一般都要指定字符編碼。如果不指定,通常Java會(huì)選擇系統(tǒng)默認(rèn)的編碼。這為程序運(yùn)行的結(jié)果產(chǎn)生了不確定因素。 “亂碼”分析明白了各輸入、轉(zhuǎn)換、輸出環(huán)節(jié)是怎樣工作的,我們的分析工作就有頭緒了。在深入分析之前,有不少情況,觀察亂碼的表面現(xiàn)象就可以得到大概的結(jié)論。 一個(gè)中文變成了兩個(gè)問(wèn)號(hào)“?”這個(gè)現(xiàn)象通常表明字符在輸入時(shí)出錯(cuò),也就是解碼錯(cuò)誤。
雖然輸出編碼是對(duì)的,但在此之前,由于錯(cuò)誤的輸入編碼,每個(gè)中文字變成了兩個(gè)不相干的歐洲字符。而這些歐洲字符的編碼和GBK編碼是相沖突的(但也不一定完全沖突,例如上例中的第三個(gè)字節(jié)B0,被轉(zhuǎn)換成GBK的E3A1)。因此大部分中文被輸出成兩個(gè)問(wèn)號(hào)。 如果出現(xiàn)亂碼的中文字是從Velocity模板讀入的,說(shuō)明Velocity配置文件中的input.encoding設(shè)置不正確;如果這個(gè)中文字是從數(shù)據(jù)庫(kù)讀入的,說(shuō)明數(shù)據(jù)庫(kù)的配置出錯(cuò),也有可能文本在保存進(jìn)數(shù)據(jù)庫(kù)之前就已經(jīng)錯(cuò)了;如果這個(gè)中文字是從用戶表單輸入的,很可能是你忘了調(diào)用request.setCharacterEncoding("GBK")。 一個(gè)中文變成了一個(gè)問(wèn)號(hào)“?”這個(gè)現(xiàn)象通常表明字符在輸出時(shí)出錯(cuò),也就是編碼錯(cuò)誤。
這很可能是因?yàn)闆]有設(shè)置response.setContentType("text/html; charset=GBK")。 中文顯示成了看不懂的符號(hào),如“ÎÒ°®Alibaba”這個(gè)現(xiàn)象通常表明字符在輸入輸出時(shí)都出錯(cuò)了。
明眼人一看就發(fā)現(xiàn),實(shí)際上在這種情況下,最后輸出到瀏覽器上的字節(jié)流是正確的!只是因?yàn)?/span>content type被設(shè)成了錯(cuò)誤的ISO-8859-1編碼,所以才導(dǎo)致瀏覽器顯示不正確的。事實(shí)上,用戶可以手工改變?yōu)g覽器的設(shè)置,使瀏覽器使用GBK對(duì)字節(jié)流重新解碼。 看起來(lái)象是數(shù)學(xué)中的“負(fù)負(fù)得正”。為什么會(huì)這樣呢?這是因?yàn)?u>ISO-8859-1編碼的特殊性導(dǎo)致的。ISO-8859-1字符集的編碼范圍是0000-00FF,正好和一個(gè)字節(jié)的編碼范圍相對(duì)應(yīng)。這種特性保證了使用ISO-8859-1進(jìn)行編碼/解碼可以保持編碼數(shù)值“不變”。雖然中文字符在輸入JVM時(shí),被錯(cuò)誤地“拆”成了兩個(gè)歐洲字符,但由于輸出時(shí)也是用ISO-8859-1,結(jié)果被“拆”開的中文字的兩半又被神奇地合并在一起。 這種情形在英文版的Linux上最常發(fā)生,事實(shí)上我們公司的很多程序就是這樣做的。英文版Linux的系統(tǒng)默認(rèn)編碼為ISO-8859-1。假設(shè)我們的servlet從模板中生成動(dòng)態(tài)網(wǎng)頁(yè):
同樣的代碼,如果在中文Windows上運(yùn)行,因?yàn)橄到y(tǒng)默認(rèn)編碼為GBK,因而會(huì)轉(zhuǎn)變成“一個(gè)中文變成一個(gè)問(wèn)號(hào)”的情形。 如果頁(yè)面中有部分字符不是來(lái)源于模板,而是來(lái)源于XML文件或UTF-8編碼的數(shù)據(jù)庫(kù),又會(huì)轉(zhuǎn)變成“WEB頁(yè)面中部分中文顯示正常,部分中文是亂碼”的情形。 此外,把一個(gè)中文字符轉(zhuǎn)換成兩個(gè)歐洲字符,不僅使字符串變長(zhǎng)了一倍,影響效率,而且前面所說(shuō)的和Unicode相關(guān)的功能一概失效:斷句斷詞、排序、查看字符屬性、格式化日期和數(shù)字。 可見,使用這種方法顯示中文,引入了諸多不確定因素,實(shí)在不是一種可取的方法。但是很多程序員滿足于“完成任務(wù)”,卻不求甚解,不理解Unicode的精義。甚至網(wǎng)上很多的文章也主張這么做,真是可悲可嘆。 WEB頁(yè)面中部分中文顯示正常,部分中文是亂碼很明顯,這是由于同一頁(yè)面中的字符是從不同的輸入源取得的。假設(shè)有如下常見情形:
結(jié)果就是:
注意 所謂“碰巧正常”是指雖然在服務(wù)器上,一個(gè)中文被當(dāng)作兩個(gè)歐洲字符處理,但是輸出到瀏覽器以后,又被重新組合成了正確的字節(jié)序列,并且瀏覽器按默認(rèn)的選項(xiàng),會(huì)以中文GBK解碼此序列。對(duì)于“中文變成歐洲字符”的情形,可以在瀏覽器上人工設(shè)置字符編碼為GBK,或是在HTML中設(shè)置<meta http-equiv="Content-Type" content="text/html; charset=GBK">標(biāo)記,來(lái)顯示出中文。 深入分析以上只是分析了最常見的“亂碼”現(xiàn)象。實(shí)際上,還可能會(huì)發(fā)生更復(fù)雜一點(diǎn)的情形。但是無(wú)論什么情形,都可以通過(guò)仔細(xì)分析中文字符經(jīng)過(guò)的每一個(gè)輸入、轉(zhuǎn)換、輸出環(huán)節(jié),來(lái)了解它的原因。 |
|
來(lái)自: -ー意孤行ノ > 《中文化與國(guó)際化》