細談 URL 編碼

The following examples had been tested on Mozilla's Firefox and Microsoft's IE. The document is provided as is. You are welcomed to use it for non-commercial purpose.
Written by: 國立中興大學資管系呂瑞麟

請勿轉貼


在這十幾年的教學生涯中,不論在教學或是指導學生做專題的過程中,總是 不斷的跟學生解釋為什麼在網頁上輸入中文(Big5 或者 UTF-8),但是處理 的結果(可能網頁的呈現或者資料庫的存取)卻老是會出現亂碼。因為我有 寫網頁教材的習慣,所以總是寫一寫就叫學生自己去看,而曾經探討過這個 議題的有:資料處理入門(這是使用 JDK 1.2 版以前所需要解決的方式)、Java Servlet 入門(這是第一次處理網頁的中文資料的解決方式)、MySQL Server 簡介(這是針對將 Big5 資料存入 MySQL,並將 MySQL 的資料正確轉回 Big5 的 解決方式)、以及 由 XMLHttpRequest 送出中文資料給 Tomcat(這是針對 XMLHttpRequest 傳送中文給 Tomcat 的解決方式)。

最近有一次跟學生聊天,他們提到網頁內容的編碼方式是利用 UTF-8 的方式, 處理的結果卻不盡相同,於是讓我想要重新做一次(儘可能)完整的探討,來細細的說明 為什麼網頁中文資料的傳送有那麼樣的"曲折離奇"!

在參考了幾篇文章之後,我列出最有幫助的兩篇:

首先我們來看幾件事實:第一、雖然網頁的內容允許各式各樣的編碼(我們在之後 的範例中使用兩種常見的中文編碼:Big5 和 UTF-8),但是 URL 的內容(依據 RFC 1738 的規定)只能出現部分的 ASCII 碼;第二、目前並沒有任何標準來規範 網頁資料與後端伺服器之間應該如何的編碼,而且這些資料在各式的程式介面之間 被轉換的方式也沒有一定的規範(也就是說,開發人員只知道有這一項事實,但是 解決的方式卻會依據作業系統、預設語系、瀏覽器的不同而有不同的結果),所以 解決的方法必須依環境而定,而這可能正是考驗程式設計人員能力的時候。

知道了這些事實之後,我們先把我們的測試環境描述一下:在瀏覽器的部分,我們 使用 Firefox 3.x 和 IE 8.x;後端伺服器的部分,我們使用 Tomcat 5.5.x, 後端程式是 Java 的 servlet。如果你使用的環境不同,應該也可以用類似的方法 來測試,並找尋你的解決方法。

網頁的內容,我們採用兩種主要的編碼:Big5 和 UTF-8。由於我習慣上都是 使用 Microsoft Windows 的記事本(也就是 notepad.exe),所以我要提醒一下 讀者:使用 notepad 來儲存網頁內容的時候,預設的編碼方式就是 Big5; 如果你想產生 UTF-8 的內容,請記得在儲存網頁的時候,選擇 UTF-8 的 編碼(如下圖所示)。如果你使用的是其他的文字編輯器,請再測試前先確認 網頁儲存時所使用的編碼是什麼,然後做必要的調整。

測試用的網頁還蠻簡單的,以下我只提供第一個版本的網頁內容,其他的,讀者 可以自己去閱讀網頁的原始碼。原始碼如下:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=Big5">
<title>測試劉覽器 URL 編碼</title>
</head>
<body>
<h2>測試劉覽器 URL 編碼</h2>
<h3>HTML 以 Big5 編碼,呼叫 EchoBig5</h3>
<form method="GET" action="/xml/servlet/EchoBig5">
<input type="text" value="老呂" name="data">
<input type="submit" value="確定">
</form>
</body>
</html>
讀者可以到 test1Big5 來看它的畫面。在這個網頁中,由於包含中文的內容,而又由於 notepad 預設的 文字編碼是 Big5,所以網頁中包含一個 <meta> 標籤,其中指明網頁的 內容是以 Big5 的方式編碼,因此瀏覽器(包含 Firefox 和 IE)都能正確的顯示 網頁資料。另一個要注意的地方在 <form> 標籤;我們在 <form> 中 為 method 定義的屬性值是 GET,這樣子一來,我們也可以從瀏覽器的 URL 欄位 看出究竟瀏覽器送出的 URL 長什麼樣子。

請在測試網頁上點一下"確定"按鈕,然後你應該會看到如下的畫面:

如果你使用的是 IE,也會得到同樣的結果。首先,請注意畫面上方的紅色框框; 我們在網頁中輸入的資料是"老呂",而在 URL 欄位中出現的卻是 %A6%D1%A7f。 從"老呂"變成 %A6%D1%A7f,這就是 URL 編碼的工作。在之前我們曾經說過, 雖然網頁允許各式各樣的編碼,但是依據標準,URL 中只能出現部分的 ASCII 碼; 這些 ASCII 碼包含 a-z、A-Z、0-9、以及一些特殊字元和保留字元。"老呂" 絕對不是 ASCII 碼的任何部分,所以瀏覽器必須對"老呂"進行編碼。一般來說, 瀏覽器會根據網頁所指定的編碼(以本例來說就是 Big5)對"老呂"編碼,而 編碼的結果就是 A6 D1 A7 66 這四個 16 進位(hex number)的數字(其實, 這也是畫面中間紅色框框內的值);然後, 瀏覽器會在每一個數字之前加上 % 符號,所以就形成了 %A6%D1%A7%66; 又因為 16 進位數字的 66 就是 f,因此編碼結果也可以是 %A6%D1%A7f

讀者也可以使用一般的 16 進位編輯器來確認"老呂"就是由 A6 D1 A7 66; 我們使用的是 notepad++ 以及其 16 進位編輯器的外掛程式,顯示"老呂"畫面(highlighted) 如下:

一般畫面16 進位畫面

到目前為止,我們已經清楚了瀏覽器是如何進行 URL 編碼,可是編碼後的資料 是如何被伺服器所處理呢?就之前所說的事實,這部份是沒有編準的,所以我們 就以我們所使用的伺服器 Tomcat 5.5.x 版來說明。在預設的情形下,Tomcat 無法以 Big5 的方式來解讀從瀏覽器傳過來的 URL,而是以 ISO-8859-1 的編碼 方式來解讀。為了確認這個說法,我們設計了以下的程式,也就是剛剛 <form> 標籤中的 EchoBig5:

01  import javax.servlet.*;
02  import javax.servlet.http.*;
03  import java.io.*;
04  
05  public class EchoBig5 extends HttpServlet {
06    public void doGet(HttpServletRequest req, HttpServletResponse res)
07           throws ServletException, IOException {
08      PrintWriter output;
09  
10      res.setContentType("text/html;charset=Big5");
11      output = res.getWriter();
12  
13      StringBuffer buf = new StringBuffer();
14      buf.append("<html><head><title>\n");
15      buf.append("Echo Big5\n");
16      buf.append("</title></head><body>\n");
17  
18      String data = req.getParameter("data");
19      String orig = data.length() + " ";
20      for(int i=0; i<data.length(); i++) {
21        int ch = (int)data.charAt(i);
22        orig = orig + "%" + Integer.toHexString(ch);
23      }
24      String out1 = new String(data.getBytes("ISO-8859-1"), "Big5");
25  
26      buf.append("<h1>String: " + data + "</h1>\n");
27      buf.append("<h1>Hex Numbers: " + orig + "</h1>\n");
28      buf.append("<h1>Big5 Encoded: " + out1 + "</h1>\n");
29      buf.append("</body></html>\n");
30      output.println(buf.toString());
31  
32      output.close();
33    }
34  }
EchoBig5 類別是一個 Java servlet,不清楚的讀者可以參考 Java Servlet 入門。 這個程式有幾個地方稍微說明一下:第 10 行中,servlet 告訴瀏覽器即將回傳的 資料編碼方式是 Big5;第 18 行是把瀏覽器傳送過來的資料直接以字串的方式 放到 data 變數中;第 19-23 行是把 data 內的資料一次以一個字元的方式取出來, 並顯示每一個字元的 16 進位值,也就是 orig;在第 19 行,orig 的第一個資料 代表 data 的長度。

由於 Tomcat 對於傳送進來的 URL 資料是以 ISO-8859-1 的方式解讀,因此 把 data 直接輸出(或者顯示)的結果就是如執行畫面中第一行的輸出( ISO-8859-1 是一個支援西歐語系的編碼,它是無法正確顯示中文)。 畫面中,第二行顯示從瀏覽器總共傳送了四個字元,而每一個字元的 16 進位值 分別是 A6 D1 A7 66;而程式的第 22 行將輸出的數字之前加上了 % 符號, 這也就形成了 %A6%D1%A7%66 的結果。

由於 Tomcat 將 A6 D1 A7 66 視為 ISO-8859-1 碼,因此要能夠正確的 顯示其 Big5 的內容,我們必須藉助 Java 的 String 轉碼功能;在第 24 行的 地方,程式先把 data 字串依據 ISO-8859-1 碼的方式取出並形成一個 字元陣列,然後利用該陣列來建構一個字串物件,而轉碼的方式是把其 視為 Big5,而結果就是 out1 字串,該字串在第 28 行中輸出,也就是 執行畫面中第三行的結果。

練習題: 既然 Tomcat 是使用 ISO-8859-1 的方式編、解碼,那麼 我們的網頁內容直接使用 ISO-8859-1 來寫,可不可以?

繼續閱讀:Part II


Last Updated: Friday, 12-Jun-2009 15:48:38 CST
Written by: 國立中興大學資管系呂瑞麟