struts的中文亂碼問題

本篇我們來討論一下struts的國際化編程問題,即所謂的i18n編程問題,這一篇我們討論其基礎部分。與這個問題緊密相關的是在各java論壇中被頻繁提及的中文亂碼問題,因爲,英、美編程人員較少涉及到中文亂碼問題,因此,這方面的英文資料也是非常奇缺的,同時也很少找到這方面比較完整的中文資料,本文也嘗試對中文亂碼問題做一些探討。要解決上述問題,需要有一定的字符集方面的知識,下面,我們就先介紹字符集的有關情況:

一、從ASCII到Unicode(UTF-8)

電子計算機技術是從美國開始發展起來的,因爲美國使用的文字爲英文,美國規定的計算機信息交換用的字符編碼集是人們熟知的擴展ASCII碼,它以8bit字節爲單位存儲,ASCII的0-31及127爲控制符,32-126爲可見字符,包括所有的英文字母,阿拉伯數字和其他一些常見符號,128-255的ASCII碼則沒有定義。

ASCII對英語國家是夠用了,但對其他西歐國家卻不夠用,因此,人們將ASCII擴展到0-255的範圍,形成了ISO-8859-1字符集。值得一提的是,因爲考慮到程序中處理的信息大多是西文信息,因此有些WEB容器(如:Tomcat4.x)在處理所接收到的request字符串時,如果您沒指定request的編碼方式則系統就缺省地採用ISO-8859-1,明白這一點對理解後面的問題會有幫助。

相比西方的拼音文字,東方的文字(如中文)的字符數要大得多,根本不可能在一個字節內將它們表示出來,因此,它們以兩個字節爲單位存儲,以中文國標字符集GB2312爲例,它的第一個字節爲128-255。系統可以據此判斷,若第一個字節大於127,則把與該字節後緊接着的一個字節結合起來共兩個字節組成一箇中文字符。這種由多個字節存儲一個字符的字符集叫多字節字符集(MultiByte Charsets),對應的象ASCII這種用一個字節存儲一個字符的字符集叫單字節字符集(SingleByte Charsets)。在GB2312字符集中,ASCII字符仍然用一個字節存儲,換句話說該ASCII是該字符集的子集。

GB2312只包含數千個常用漢字,往往不能滿足實際需要,因此,人們對它進行擴展,這就有了我們現在廣泛使用的GBK字符集,GBK是現階段Windows及其他一些中文操作系統的缺省字符集。它包含2萬多個字符,除了保持和GB2312兼容外,還包含繁體中文字,日文字符和朝鮮字符。值得注意的是GBK只是一個規範而不是國家標準,新的國家標準是GB18030-2000,它是比GBK包含字符更多的字符集。

我國的臺灣地區使用的文字是繁體字,其字符集是BIG5,而日本採用的字符集則是SJIS。它們的編碼方法與GB2312類似,它們的ASCII字符部分是兼容的,但擴展部分的編碼則是不兼容的,比如這幾種字符集中都有"中文"這兩個字符,但他們在各自的字符集中的編碼並不相同,這就是用GB2312寫成的網頁用BIG5瀏覽時,看到的是亂糟糟的信息的原因。

可見,在字符集的世界裏,呈現給我們的是一個羣雄割據的局面,各字符集擁有一塊自己的地盤。這給各國和各地區交換信息帶來了很大的困難,同時,也給國際化(本地化)編程造成了很大的麻煩。

常言道:"分久必合",隨着國際標準ISO10646定義的通用字符集(Universal Character Set即UCS)的出現,使這種局面發生了徹底的改觀。UCS 是所有其他字符集標準的一個超集. 它保證與其他字符集是雙向兼容的. 就是說, 如果你將任何文本字符串翻譯到 UCS格式, 然後再翻譯回原編碼, 你不會丟失任何信息。UCS 包含了用於表達所有已知語言的字符。不僅包括拉丁語、希臘語、 斯拉夫語、希伯來語、阿拉伯語、亞美尼亞語和喬治亞語的描述、還包括中文、 日文和韓文這樣的象形文字、 以及平假名、片假名、 孟加拉語、 旁遮普語果魯穆奇字符(Gurmukhi)、 泰米爾語、印.埃納德語(Kannada)、Malayalam、泰國語、 老撾語、 漢語拼音(Bopomofo)、Hangul、 Devangari、Gujarati、Oriya、Telugu 以及其他數也數不清的語。對於還沒有加入的語言, 由於正在研究怎樣在計算機中最好地編碼它們, 因而最終它們都將被加入。

ISO 10646 定義了一個 31 位的字符集。 然而, 在這巨大的編碼空間中, 迄今爲止只分配了前 65534 個碼位 (0x0000 到 0xFFFD)。 這個 UCS 的 16位子集稱爲 基本多語言面 (Basic Multilingual Plane, BMP)。 將被編碼在 16 位 BMP 以外的字符都屬於非常特殊的字符(比如象形文字), 且只有專家在歷史和科學領域裏纔會用到它們。

UCS 不僅給每個字符分配一個代碼, 而且賦予了一個正式的名字。 表示一個 UCS 值的十六進制數, 通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大寫字母A"。 UCS 字符 U+0000 到 U+007F 與 US-ASCII(ISO 646) 是一致的, U+0000 到 U+00FF 與 ISO 8859-1(Latin-1) 也是一致的。這裏要注意的是它是以16bit爲單位存儲,即便對字母"A"也是用16bit,這是與前面介紹的所有字符集不同的地方。

歷史上,在國際標準化組織研究ISO10646標準的同時,另一個由多語言軟件製造商組成的協會也在從事創立單一字符集的工作,這就是現在人們熟知的Unicode。幸運的是,1991年前後ISO10646和Unicode的參與者都認識到,世界上不需要兩個不同的單一字符集。他們合併雙方的工作成果,併爲創立單一編碼表而協同工作。兩個項目仍都存在並獨立地公佈各自的標準,都同意保持ISO10646和Unicode的碼錶兼容,並緊密地共同調整任何未來的擴展。這與當年在PC機上的操作系統MS-dos與PC-dos的情形有些相象。後面,我們將視ISO10646和Unicode爲同一個東西。

有了Unicode,字符集問題接近了完美的解決,但不要高興得過早。由於歷史的原因:一些操作系統如:Unix、Linux等都是基於ASCII設計的。此外,還有一些數據庫管理系統軟件如:Oracle等也是圍繞ASCII來設計的(從其8i的白皮書上介紹的設置系統字符集和字段的字符集中可以間接地看到這一點)。在這些系統中直接用Unicode會導致嚴重的問題。用這些編碼的字符串會包含一些特殊的字符, 比如 '/0' 或 '/', 它們在 文件名和其他 C 庫函數參數裏都有特別的含義。 另外, 大多數使用 ASCII 文件的 UNIX 下的工具, 如果不進行重大修改是無法讀取 16 位的字符的。 基於這些原因, 在文件名, 文本文件, 環境變量等地方,直接使用Unicode是不合適的。

在 ISO 10646-1 Annex R 和 RFC 2279 裏定義的 UTF-8 (Unicode Transformation Form 8-bit form)編碼沒有這些問題。

UTF-8 有以下一些特性:

UCS 字符 U+0000 到 U+007F (ASCII) 被編碼爲字節 0x00 到 0x7F (ASCII 兼容)。 這意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 兩種編碼方式下是一樣的。

所有 >U+007F 的 UCS 字符被編碼爲一個多個字節的串, 每個字節都有標記位集。 因此,ASCII 字節 (0x00-0x7F) 不可能作爲任何其他字符的一部分。

表示非 ASCII 字符的多字節串的第一個字節總是在 0xC0 到 0xFD 的範圍裏, 並指出這個字符包含多少個字節。 多字節串的其餘字節都在 0x80 到 0xBF 範圍裏。 這使得重新同步非常容易, 並使編碼無國界,且很少受丟失字節的影響。

 

UTF-8 編碼字符理論上可以最多到 6 個字節長, 然而 16 位 BMP 字符最多只用到 3 字節長。

字節 0xFE 和 0xFF 在 UTF-8 編碼中從未用到。

通過,UTF-8這種形式,Unicode終於可以廣泛的在各種情況下使用了。在討論struts的國際化編程之前,我們先來看看我們以前在jsp編程中是怎樣處理中文問題以及我們經常遇到的:

二、中文字符亂碼的原因及解決辦法

java的內核是Unicode的,也就是說,在程序處理字符時是用Unicode來表示字符的,但是文件和流的保存方式是使用字節流的。在java的基本數據類型中,char是Unicode的,而byte是字節,因此,在不同的環節java要對字節流和char進行轉換。這種轉換髮生時如果字符集的編碼選擇不當,就會出現亂碼問題。

我們常見的亂碼大致有如下幾種情形:
1、漢字變成了問號"?"
2、有的漢字顯示正確,有的則顯示錯誤
3、顯示亂碼(有些是漢字但並不是你預期的)
4、讀寫數據庫出現亂碼

下面我們逐一對它們出現的原因做一些解釋:

首先,我們討論漢字變成問號的問題。

Java中byte與char相互轉換的方法在sun.io包中。其中,byte到char的常用轉換方法是:
public static ByteToCharConverter getConverter(String encoding);

爲了便於大家理解,我們先來做一個小實驗:比如,漢字"你"的GBK編碼爲0xc4e3,其Unicode編碼是/u4f60。我們的實驗是這樣的,先有一個頁面比如名爲a_gbk.jsp輸入漢字"你",提交給頁面b_gbk.jsp。在b_gbk.jsp文件中以某種編碼方式得到"你"的字節數組,再將該數組以某種編碼方式轉換成char,如果得到的char值是0x4f60則轉換是正確的。

a_gbk.jsp的代碼如下:

<%@ page contentType="text/html; charset=GBK" language="java" import="java.sql.*" errorPage="" %><table width="611" border="0" align="center" cellpadding="0" cellspacing="0">  <tr>    <td>&nbsp;</td>    <td class="bigword">&nbsp;</td>    <td>&nbsp;</td>  </tr>  <tr>    <td width="100">&nbsp;</td>    <td class="bigword">Input</td>    <td width="100">&nbsp;</td>  </tr>  <tr>    <td>&nbsp;</td>    <td class="bigword">&nbsp;</td>    <td>&nbsp;</td>  </tr></table><table width="611" border="0" align="center" cellpadding="0" cellspacing="0">  <tr>    <td><form method="post" action="b_gbk.jsp">        <table width="611" border="0" cellpadding="0" cellspacing="0">          <tr>            <td width="100" align="right"></td>            <td><input name="ClsID" type="text" class="word" id="ClsID" maxlength="2" >              *</td>          </tr>          <tr>            <td width="100" align="right">&nbsp;</td>            <td><input name="btn" type="submit" value="OK">             </td>          </tr>        </table>      </form></td>  </tr></table>



b_gbk.jsp的代碼如下:

<%@ page contentType="text/html; charset=GBK" import="sun.io.*,java.util.*" %><%String a=(String)request.getParameter("ClsID");byte b[]=a.getBytes("ISO8859-1");for(int j=0;j<b.length;j++){  out.println(Integer.toHexString(b[j])+"<br>");}ByteToCharConverter convertor=ByteToCharConverter.getConverter("GBK");char[] c=convertor.convertAll(b);out.println("b length:"+b.length+"<br>");out.println("c length:"+c.length+"<br>");for(int i=0;i<c.length;i++){       out.println(Integer.toHexString(c[i])+"<br>");}String a1=new String(a.getBytes("ISO8859-1"),"GBK");%><%="a是:"+a%><br><%="a1是:"+a1%>



在瀏覽器中打開a_gbk.jsp並輸入一個"你"字,點擊OK按鈕提交表單,則會出現如圖1所示的結果:



圖1

從圖1可以看出,在b_gbk.jsp中這樣將byte轉換爲char是正確的,即得到的char是/u4f60。這裏要注意的是:byte b[]=a.getBytes("ISO8859-1");中的編碼是ISO8859-1,這就是我們前面提到的有些web容器在您沒有指定request的字符集時它就採用缺省的ISO8859-1。

從圖1中我們還看到表達式中的a並沒有正確地顯示"你"而是變成"??"這是什麼原因呢?這裏的a是作爲一個String被顯示的,我們來看看我們常用的String構造函數:

String(byte[] bytes,String encoding);

在國標平臺上,該函數會認爲bytes是按GBK編碼的,如果後一個參數省略,它也會認爲是encoding是GBK。

對前一個參數就相當於將b_gbk.jsp文件的這句byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1改爲GBK,這樣顯然在GBK字符集中找不到相應的目的編碼,它給出的結果是0x3f、0x3f。因此,就會顯示爲"??",這也就是造成亂碼的第一種現象的原因。我們的例子是演示的從byte到char的轉換過程,相反的過程也會造成同樣的問題,限於篇幅,就不在此討論了,大家自己可以做類似的實驗來驗證。

解決該問題的方法就是象例子中a1那樣,在獲取byte數組時,指定編碼爲ISO8859-1。

接下來,我們討論有些漢字能正常顯示,有些不能正常顯示的問題。

如果我們將String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的GBK改爲GB2312則象朱鎔基的"鎔"字就不能正常顯示,這是因爲該字是GBK中的字符而在GB2312中不存在。

解決上述兩種問題的方法就是象a1那樣構造String,也就是人們常說的同時也是常用的轉碼的方法。採用這種方法會在程序中到處出現這種語句,特別是在Struts中,Struts有一個回寫表單的功能,在回寫時也要做這種轉換,這樣的語句差不多要多一倍。因此,這是個比較笨拙的方法,有沒有簡捷一些的方法呢?其實是有的,只要在取得request的字符串前加上request.setCharacterEncoding("GBK");這句,指定request的字符集。則中的a就能正常顯示,a1反而不能正常顯示。此時要將byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1變成GBK,從byte到char的轉換纔是正確的,這就是此時a能正常顯示而a1反而不能正常顯示的原因。如果此時要a1正常顯示則必須將String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的ISO8859-1改爲GBK。

很顯然,使用request.setCharacterEncoding("GBK");只能解決GBK字符問題,要解決i18n問題則要使用UTF-8來取代GBK。我們接着做上述實驗,將a_gbk.jsp和b_gbk.jsp分別另存爲a.jsp和b.jsp將文件中的GBK改爲UTF-8,更改後的代碼分別如下:

a.jsp代碼:

<%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %><table width="611" border="0" align="center" cellpadding="0" cellspacing="0">  <tr>    <td>&nbsp;</td>    <td class="bigword">&nbsp;</td>    <td>&nbsp;</td>  </tr>  <tr>    <td width="100">&nbsp;</td>    <td class="bigword">Input</td>    <td width="100">&nbsp;</td>  </tr>  <tr>    <td>&nbsp;</td>    <td class="bigword">&nbsp;</td>    <td>&nbsp;</td>  </tr></table><table width="611" border="0" align="center" cellpadding="0" cellspacing="0">  <tr>    <td><form method="post" action="b.jsp">        <table width="611" border="0" cellpadding="0" cellspacing="0">          <tr>            <td width="100" align="right"></td>            <td><input name="ClsID" type="text" class="word" id="ClsID" maxlength="2" >              *</td>          </tr>          <tr>            <td width="100" align="right">&nbsp;</td>            <td><input name="btn" type="submit" value="OK">             </td>          </tr>        </table>      </form></td>  </tr></table>b.jsp代碼:<ccid_nobr><table width="400" border="1" cellspacing="0" cellpadding="2"  bordercolorlight = "black" bordercolordark = "#FFFFFF" align="center"><tr>    <td bgcolor="e6e6e6" class="code" style="font-size:9pt">    <pre><ccid_code>  <%@ page contentType="text/html; charset=UTF-8" import="sun.io.*,java.util.*" %><%request.setCharacterEncoding("UTF-8");String a=(String)request.getParameter("ClsID");byte b[]=a.getBytes("UTF-8");for(int j=0;j<b.length;j++){  out.println(Integer.toHexString(b[j])+"<br>");}ByteToCharConverter convertor=ByteToCharConverter.getConverter("UTF-8");char[] c=convertor.convertAll(b);out.println("b length:"+b.length+"<br>");out.println("c length:"+c.length+"<br>");for(int i=0;i<c.length;i++){  out.println(Integer.toHexString(c[i])+"<br>");}String a1=new String(a.getBytes("UTF-8"),"UTF-8");%><%="a是:"+a%><br><%="a1是:"+a1%>



再在a.jsp中輸入"你"字,你會發現顯示結果中,一個漢字是用三個byte表示的,它們的值分別是0xe4、0xbd、0xa0,也就是說用UTF-8來表示漢字,每個漢字要比GBK多佔用一個byte,這也是使用UTF-8要多付出的一點代價吧。

現在,我們討論一下第三個問題,即顯示亂碼,有些莫名其妙的漢字並不是你預期的結果。

在上例中將String a1=new String(a.getBytes("UTF-8"),"UTF-8");改爲String a1=new String(a.getBytes("UTF-8"),"GBK");再輸入"你"字,則a1會顯示成"浣?",您只要看一看"浣"的UTF-8碼和GBK碼就會知道其中的奧祕了。

下面,我們討論一下最後一個問題,就是讀寫數據庫時出現亂碼。

現在一些常用的數據庫都支持數據庫encoding,也就是說在創建數據庫時可以指定它自己的字符集設置,數據庫數據以指定的編碼形式存儲。當應用程序訪問數據庫時,在入口和出口處都會有encoding轉換。如果,在應用程序中字符本來已變成了亂碼,當然也就無法正確地轉換爲數據庫的字符集了。數據庫的encoding可根據需要來設置,比如要支持簡、繁體中文、日、韓、英語選GBK,如果還要支持其他語言最好選UTF-8。

本篇文章對字符集及中文亂碼問題做了一下探討,爲實現國際化編程的實踐打下一個基礎。下一篇文章,我們將介紹struts中實現國際化編程的具體步驟,並將我們前面介紹的登錄例子進行國際化  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章