1. 程式人生 > >常見亂碼問題分析和總結

常見亂碼問題分析和總結

夏 懷英 和 David Chen 2018 年 1 月 17 日釋出

在我們的日常工作生活中一定碰到過下面的情況:

景 1: 安裝完某個軟體後,看到的安裝程式變成類似這樣的一組字元" µç×ÓË°Îñ¾ÖÖ¤ÊéÇý¶¯¼°·þÎñƽ̨"圖 1 所示的樣子;

圖 1. 安裝程式中的亂碼

景 2:開啟一個文件發現裡面的內容全面是問號??????如圖 2 所示;

圖 2. 帶有問號的亂碼

景 3開啟某個網頁,卻顯示成:如"бЇЯАзЪСЯ"、"�????????"?等等;

景 4開啟某個文件後,出現§ § § §

上面例子中看到的就是困擾著我們的亂碼。這篇文章基於專案實踐,分別從普通使用者和程式設計角度總結了常見的亂碼種類,產生原因以及如何解決這些亂碼問題。在分析亂碼原因和解決辦法之前,首先闡述一下和亂碼相關的術語。

編碼解碼概述

我們都知道計算機不能直接儲存字母,數字,圖片,符號等,計算機能處理和工作的唯一單位是"位元位(bytes)",一個位元位通常只有 0 和 1,是(yes)和否(no),真(true)或者假(false)等等我們喜歡的稱呼。利用位元位序列來代表字母,數字,圖片,符號等,我們就需要一個儲存規則,不同的位元序列代表不同的字元,這就是所謂的"編碼"。反之,將儲存在計算機中的位元位序列(或者叫二進位制序列)解析顯示出來成對應的字母,數字,圖片和符號,稱為"解碼",如同密碼學中的加密和解密,下面將詳細解釋編碼解碼過程中涉及到的一些術語:

字元集合(Character set):是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等,簡單理解就是一個字型檔,與計算機以及編碼無關。

字元編碼集(Coded character set):是一組字元對應的編碼(即數字),為字元集合中的每一個字元給予一個數字,如 Unicode 為每一個字元分配一個唯一的碼點與之一一對應。

字元編碼(Character Encoding:簡單理解就是一個對映關係,將字符集對應的碼點對映為一個個二進位制序列,從而使得計算機可以儲存和處理。常見的編碼方式有 ASCII 編碼、ISO-8859-1(不支援中文)、GBK、GB2312(中國編碼,支援中文)、UTF-8 等等,詳情見表 1。

字符集(Charset):包括編碼字符集和字元編碼,如 ASCII 字符集、ISO-8859-X、GB2312 字符集(簡中)、BIG5 字符集(繁中)、GB18030 字符集、Shift-JIS 等,即下文中提到的字符集。

表 1. 常見字符集和對應編碼方式

字符集 編碼 詳解
ASCII ASCII 編碼 ASCII字符集:主要包括控制字元(回車鍵、退格、換行鍵等);可顯示字元(英文大小寫字元、阿拉伯數字和西文符號。ASCII 編碼:用一個位元組的低 7 位表示,0~31 是控制字元如換行回車刪除等;32~126 是列印字元;ASCII的最大缺點是隻能解決了部份西歐語言的顯示問題,但對更多其他語言依然無能為力。
I 字 ISO-8859-X (常用的 ISO-8859-1) ISO-8859-1 編碼  

ISO-8859-2 編碼

 

ISO-8859-15 編碼
ISO-8859-X 字符集:擴充套件的 ASCII 字符集,包括 ISO-8859-1 ~ ISO-8859-15,涵蓋了大多數西歐語言字元和希臘語。ISO-8859-1 編碼:用 8 位表示一個字元,總共能表示 256 個字元,但還是單位元組編碼,不能對雙位元組如中日韓等進行編碼。
GBXXXX GB2312 編碼 GB2312 編碼 :它的全稱是《資訊交換用漢字編碼字符集 基本集》,它是雙位元組編碼,每個漢字及符號以兩個位元組來表示。第一個位元組稱為"高位位元組"(也稱"區位元組)",第二個位元組稱為"低位位元組"(也稱"位位元組")。 總的編碼範圍是 A1-F7,其中從 A1-A9 是符號區,總共包含 682 個符號,從 B0-F7 是漢字區,它所收錄的漢字已經覆蓋中國大陸 99.75%的使用頻率。對於人名古漢語等方面出現的罕用字,GB2312 不能處理,這導致了後來GBKGB 18030漢字字符集的出現。
GBK 編碼 GBK 編碼: 全稱叫《漢字內碼擴充套件規範》,是在 GB2312-80 標準基礎上的內碼擴充套件規範,使用了雙位元組編碼方案,其編碼範圍從 8140 至 FEFE(剔除 xx7F),共 23940 個碼位,共收錄了 21003 個漢字,完全相容 GB2312-80 標準,支援國際標準 ISO/IEC10646-1 和國家標準 GB13000-1 中的全部中日韓漢字,幷包含了 BIG5 編碼中的所有漢字。
GB18030 編碼 GB18030-2005《資訊科技中文編碼字符集》是我國自主研製的以漢字為主幷包含多種我國少數民族文字(如藏、蒙古、傣、彝、朝鮮、維吾爾文等)的超大型中文編碼字符集強制性標準,其中收入漢字 70000 餘個,解決了中文、日文、朝鮮語等的編碼,相容 GBK。 採用變長位元組表示即單位元組、雙位元組和四位元組三種方式對字元編碼。可表示27484個文字
Big5 Big5 編碼 Big5 編碼: 適用於臺灣、香港地區的一個繁體字編碼方案。 使用了雙八碼儲存方法,以兩個位元組來安放一個字,第一個位元組稱為"高位位元組",第二個位元組稱為"低位位元組。
Unicode UTF-8 UTF-8:採用變長位元組 (1 ASCII, 2 希臘字母, 3 漢字, 4 平面符號) 表示,在網路傳輸中即使錯了一個位元組,不影響其他位元組;儲存效率比較高,適用於拉丁字元較多的場合以節省空間,UTF-8 沒有位元組順序問題,所以 UTF-8 適合傳輸和通訊。
UTF-16 UTF-16:從先前的固定寬度的 16 位編碼(UCS-2)發展而來的,能夠對 Unicode 的所有 1,112,064 個有效 code point 進行編碼。 其編碼方式是可變長度的,因為 code point 是用一個或兩個 16 位程式碼單元編碼的。在 UTF-16 檔案的開頭,會放置一個 U+FEFF 字元作為 Byte Order Mark(BOM):UTF-16LE(小端序)以 FF FE 代表,UTF-16BE(大端序)以 FE FF 代表,以顯示這個文字檔案是以 UTF-16 編碼。 UTF-16 比起 UTF-8,好處在於大部分字元都以固定長度的位元組 (2 位元組) 儲存,但 UTF-16 卻無法兼容於 ASCII 編碼,實際使用也比較少。
UTF-32 UTF-32 (或 UCS-4):對每一個 Unicode 碼點使用 4 位元組進行編碼,其它的 Unicode 編碼方式則使用不定長度編碼。就空間而言,UTF-32 是非常沒有效率的。尤其非基本多文種平面的字元在大部分檔案中通常很罕見,以致於它們通常被認為不存在佔用空間大小的討論,使得 UTF-32 通常會是其它編碼的二到四倍。雖然每一個碼位使用固定長定的位元組看似方便,它並不如其它 Unicode 編碼使用得廣泛。

1 的幾點說明:

  • GB2312、GBK、UTF-8、UTF-16 這幾種格式都可以用來對雙位元組漢字進行編碼,在實際應用中具體選擇哪種編碼方式,需要根據實際應用場景,當前的應用場景是編碼效率重要還是減少儲存空間重要。
  • UTF-16 與 UCS-2 的關係:UTF-16 可看成是 UCS-2 的父集。在沒有輔助平面字元(surrogate code points)前,UTF-16 與 UCS-2 所指的是同一的意思。但當引入輔助平面字元後,就稱為 UTF-16 了。現在若有軟體聲稱自己支援 UCS-2 編碼,那其實是暗指它不能支援在 UTF-16 中超過 2 位元組的字集。對於小於 0x10000 的 UCS 碼,UTF-16 編碼就等於 UCS 碼。
  • 為什麼中文預設使用 GB1832 而不使用 UTF-8?因為 GB1832 對絕大多數中文采用雙位元組編碼,而 UTF-8 要用三位元組,GB11832 大大節省了儲存空間。

ANSI 編:各個國家和地區獨立制定的既相容 ASCII 編碼又彼此之間不相容的字元編碼,微軟統稱為 ANSI 編碼。在 Windows 系統中,ANSI 編碼一般代表系統預設的編碼方式,並且不是確定的某一種特定編碼方式,比如在英文 Windows 作業系統中,ANSI 指的是 ISO-8859-1;簡體中文作業系統中 ANSI 編碼預設指的是 GB 系列編碼(GB2312、GBK、GB18030)等;在繁體中文作業系統中 ANSI 編碼預設指的是 BIG5;在日文作業系統中 ANSI 編碼預設指的是 Shift JIS 等等,並且預設的 ANSI 編碼可以通過設定系統 Locale 更改。

字元解碼(Character Decoding): 根據一定規則,將二進位制序列對映成對應的正確字串,即二進位制序列-->字串,個人將其理解為"翻譯"。

UCSUniversal Character Set):稱作通用字符集,是由 ISO 制定的 ISO 10646(或稱 ISO/IEC 10646)標準所定義的標準字符集。包括了其他所有字符集。它保證了與其他字符集的雙向相容,即,如果你將任何文字字串翻譯到 UCS 格式,然後再翻譯回原編碼,你不會丟失任何資訊。

UTFUCS Transformation Format/Unicode transformation format: UCS 轉換格式/Unicode 轉換格式。

BOMByte Order Mark):位元組順序標記,出現在文字檔案頭部,Unicode 編碼標準中用於標識檔案是採用哪種格式的編碼,其 Unicode 碼點為 U+FEFF。

  • UTF-8 不需要 BOM 來表明位元組順序,但可以用 BOM 來表明編碼方式。字元 "零寬無間斷間隔" 的 UTF-8 編碼是 EF BB BF,如果接收者收到以 EF BB BF 開頭的位元組流,就知道這是 UTF-8 編碼了。
  • Big-Endian(BE)即大端序,UTF-16(BE)以 FEFF 作為開頭位元組,UTF-32(BE)以 00 00 FE FF 作為開頭位元組;
  • Little-Endian (LE)即小端序,UTF-16(LE)以 FFFE 作為開頭位元組,UTF-32(LE)以 FF FE 00 00 作為開頭位元組。

BMPBasic Multilingual Plane):稱作 Unicode 的基本平面或基本多文種平面。也就是每個字元佔用 2 個位元組,這樣理論上一共最多可以表示 216(即 65536)個字元。

Code Point稱作碼點或碼位,是組成編碼空間(或內碼表)的數值。例如,ASCII 碼包含 128 個碼點,範圍是 0 到 7F(16 進位制);ISO-8859-1 包含 256 個碼點,範圍是 0 到 FF;而 Unicode 包含 1,114,112 個碼點,範圍是 0 到 10FFFF。Unicode 碼空間劃分為 17 個 Unicode 字元平面(基本多文種平面,16 個輔助平面),每個平面有 65,536(= 216)個碼點。因此 Unicode 碼空間總計是 17 x 65,536 = 1,114,112。

Code Page內碼表或者內碼錶,是 IBM 早期稱呼計算機的 BIOS 所支援的字符集編碼(也稱作 OEM 內碼表)。Windows 系統在沒有使用 UTF-16 之前,為了解決由於不同國家和地區採用的字符集不一致,很可能出現無法正常顯示所有字元的問題,使用了內碼表(Codepage)轉換表的技術來過渡性的部分解決這一問題,即定義了一系列支援不同國家和地區所制定的字符集,被稱為 Windows 內碼表或 ANSI 內碼表,然而內碼表一般與其所對應的字符集之間並非完全相同,有時候會對字符集有所擴充套件,可以理解為一張字元-位元組序列對映表,通過查表實現編碼解碼功能。作業系統中不同 Locale 設定預設使用不同的內碼表。

Locale是指特定於某個國家或地區的一組設定,包括內碼表,以及數字、貨幣、時間和日期的格式等。在 Windows 內部,有兩個 Locale 設定:系統 Locale 和使用者 Locale。系統 Locale 決定內碼表,使用者 Locale 決定數字、貨幣、時間和日期的格式。

亂碼產生原因概述

亂碼產生的根源一般情況下可以歸結為三方面即:編碼引起的亂碼、解碼引起的亂碼以及缺少某種字型庫引起的亂碼(這種情況需要使用者安裝對應的字型庫),其中大部分亂碼問題是由不合適的解碼方式造成的,如圖 3 所示的魚骨圖。

圖 3. 亂碼產生原因

下面通過幾個常見例子,從普通使用者角度分別闡述這幾種原因導致的亂碼錶象和解決辦法。

編碼引起的亂碼錶象分析

在英文版 windows 系統(實驗使用的是 win7 64 位專業版),新建一個 txt 檔案,寫上"你好"儲存。然後再雙擊開啟,將會看到儲存的內容變成了"??",如圖 4 所示

圖 4. 不合適的編碼方式引起的亂碼

原因分析:Windows 預設選用 ANSI 編碼,英文版 Windows7 預設的系統 Locale 是 English(United States),對應的 codepage 為 437 即編碼方式為 ISO-8859-1。我們用十六進位制檢視器可以看到"你好"對應的的十六進位制數為"3F3F",這是因為中文和中文符號經過不支援中文的 ISO-8859-1 編碼時,將不在字符集範圍內的字元統一用 3F 表示,3F 對應的字元為問號"?",如圖 5 所示。

解決辦法:這種情況下形成的亂碼是不可逆的,也就是說無論用什麼解碼方式都不能正確顯示字元。我們在儲存雙位元組字元的文件時,選擇正確的編碼方式,比如簡中可以選擇 GB2312 或者 UTF-8;繁中字元可以選擇 BIG5 或者 UTF-8 等;如果安裝的是英文作業系統,對於中文使用者,可以將系統 Locale 更改為 Chinese(Simplified, PRC)。

圖 5. 編碼方式引起的亂碼剖析

解碼引起的亂碼錶象分析

在中文版 Windows 系統建立一個 txt 檔案,寫上"你好,中國"然後儲存,再將這個 txt 檔案複製到英文版 Windows 系統,雙擊開啟,將會看到儲存的內容變成"ÄãºÃ£¬Öйú"。

原因分析:中文版 Windows 系統建立的 txt 檔案以預設的 ANSI 編碼即 GB2312,當複製到英文版 Windows 系統時,Notepad 預設的解碼方式為 ISO-8859-1,如圖 6 所示的表象分析。這種情況下產生的亂碼是可逆的,只要使用正確的解碼方式,就可以正確顯示檔案中的字元。

解決辦法:遇到類似解碼問題引起的亂碼,可以換一個編輯器開啟,同時選擇正確的解碼方式。

圖 6. 不正確的解碼方式引起的亂碼

下面的例子是在英文版 Windows 系統上開啟中文版 uedit32.exe 後選單項全為亂碼的現象,如圖 7 所示。

原因分析:對於支援 Unicode 的應用程式,Windows 會預設使用 Unicode 編碼。對於不支援 Unicode 的應用程式 Windows 會採用 ANSI 編碼 (也就是各個國家自己制定的標準編碼方式,如對於西歐文字有 ISO/IEC 8859 編碼,對於簡體中文有 GB2312,對於繁體中文有 BIG-5 等。Uedit32 是不支援 Unicode 的,然後當前實驗使用的英文版 Windows 7 預設的 locale 為英語(美國),其預設字符集是 ISO-8859-1,而中文版 uedit32 程式使用的是中文編碼方式,使用 ISO-8859-1 解碼時肯定出現亂碼情況。這個例子的亂碼根本原因也是不正確的解碼方式造成的。

解決辦法:進入系統的控制面板,找到 Regional and Language Options 語言設定項,開啟進入對應的頁面,將標準與格式中的語言設定為簡體中文;同時在 Advanced 標籤頁中將系統支援的非 Unicode 語言也設定為簡體中文,從而在解碼的時候就會使用中文自己的 ANSI 編碼(實驗環境為 GB1832)。

圖 7. 解碼導致的亂碼

缺少字型引起的亂碼錶象分析

在英文 Windows 系統開啟一個檔案發現裡面的內容有些顯示為方框,如圖 8 所示。

圖 8. 帶有中括號的亂碼

原因分析:這個例子中顯示為方框的都是中文字元。我們看到螢幕上的字元實際上經歷了三種不同形態,從二進位制位元組序列轉換成對應字符集中的碼點,然後碼點通過查詢字型庫找到對應的字元,最後通過點陣的方式顯示在螢幕上。這裡的方框是因為所查詢的字型庫缺少該碼點對應的字元,或者根本沒有安裝該字型庫,從而字元庫中找不到的字元都以方框代替。

解決辦法:安裝對應的字型庫,比如 Windows 系統在 C:\Windows\Fonts 目錄下會有安裝好的字型庫列表。安裝字型庫比較簡單,下載後解壓,然後複製到對應系統的 Fonts 目錄下。這裡有個問題就是如何知道缺少何種字型?有些閱讀器比如 Adobe 在開啟文件時會提示缺少什麼字型,但是很多編輯器或者閱讀器是不提示的,這個時候可能需要根據經驗來判斷。

從程式設計角度分析出現亂碼的場景和解決辦法

從程式設計角度來看,出現亂碼的場景主要是有文字處理的時候,比如檔案的新建和讀取、複製和貼上,匯入和匯出,開啟和儲存,資料儲存和檢索,顯示,列印,分詞處理,字元轉換,規範化,搜尋,整理和傳送資料等,文字資料的示例包括平面檔案,流檔案,資料區域,目錄名稱,資源名稱,使用者標識等。圖 9 是出現亂碼的一個常見場景分類。

圖 9. 出現亂碼的場景

I/O 操作中出現亂碼情況

I/O 操作包括讀(輸入)寫(輸出)兩方面,而所謂的輸入和輸出是以程式為中心的,資料流向程式即輸入流,資料從程式中流出即輸出流。讀資料比如將檔案中的內容顯示出來,即位元組-->字元的轉換,也就是解碼;寫資料比如建立一個新檔案,即字元-->位元組轉換,也就是編碼;在分析 I/O 操作中出現亂碼原因之前,先簡要概述一下 Java I/O 操作介面。如圖 10 所示:

圖 10. Java I/O 介面

當我們想建立一個檔案並且將對應的字元寫入檔案時將用到位元組流 FileOutputStream和字元流 Writer,其流程為圖 11 所示:

圖 11. 寫入檔案的 I/O 流

Java 中與 I/O 操作相關的 API 一般都有是否指定字符集的過載形式,選擇不指定字符集形式的函式時將使用預設字符集。如 String.getBytes()就有兩種形式:String.getBytes() 和 String.getBytes(String charsetName)。下面是 String.getBytes()方法的詳解。

String.getBytes(): Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array.

這個是 Java 幫助文件提供的解釋,這裡需要強調一下"The platform's default charset"即 Charset.defaultCharset(),defaultCharset 由系統屬性 file.encoding 決定,如果使用者沒有設定 JVM 的這個屬性,其值依賴於啟動該 JVM 的環境編碼:比如是由作業系統命令列啟動 JVM,則有作業系統的執行時的區域語言設定決定的編碼;比如是在 Eclipse 裡面啟動 JVM,可以設定 JVM 的這個屬性,預設情況下 file.encoding 屬性由通用設定頁面的編碼決定。

在實際專案中,我們可能直接使用 String.getBytes()進行字元和位元組的轉換。在筆者的專案中就碰到一個這樣的亂碼問題,如清單 1 所示,在寫入錯誤日誌資訊的時候使用了 String.getBytes(),這裡沒有指定字符集,將使用預設字符集,其值依賴於啟動 JVM 的平臺環境,結果顯示出來的都是問號,結合前文顯示問號通常是使用不正確的編碼方式造成的。

清單 1. 使用 String.getBytes()出現亂碼

清單 1. 使用 String.getBytes()出現亂碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public static void main(String[] args) {

private static final String fileName = "c:\\log.txt" ;

String str ="你好,中國";

writeError(str);

}

private static void writeError(String a_error) {

try {

File logFile = new File(fileName);

//建立位元組流物件

FileOutputStream outPutStream = new FileOutputStream(logFile, true);

//使用平臺的預設字符集將此字串編碼為一系列位元組

outPutStream.write(a_error.getBytes(), 0, a_error.length() );

outPutStream.flush();

} catch (IOException e) {

e.printStackTrace();

}

}

對於清單 1 中出現的亂碼問題即錯誤的日誌全是問號,只要我們指定正確的字符集就可以解決,如清單 2 所示。

清單 2. 使用 outputStreamWrite 指定字符集

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private static void writeErrorWithCharSet(String a_error) {

try {

File logFile = new File(FileName);

String charsetName = "utf-8";

//指定字元位元組轉換時使用的字符集為 Unicode,編碼方式為 utf-8

Writer m_write = new BufferedWriter(

new OutputStreamWriter(new java.io.FileOutputStream(logFile, true),

<strong>charsetName</strong>) );

m_write.write(a_error);

m_write.close();

} catch (IOException e) {

e.printStackTrace();

}

}

強調:為了避免亂碼問題出現,在呼叫 I/O 操作相關的 API 時,最好使用帶有指定字符集引數的過載形式。

Web 程序中出現的亂碼情況

在 web 應用程式中,存在使用者輸入以及輸出顯示的地方都有可能存在編碼解碼,圖 12 簡要概括了 HTTP web 請求響應環節。

圖 12. Web 請求響應環節中的編碼解碼

下面是對圖 12 的幾點說明:

  • Web 應用程式中出現亂碼的可能原因有:瀏覽器本身沒有遵循 URI 編碼規範;伺服器端沒有正確配置編碼解碼;開發人員對 Web 程式中涉及到的編碼解碼理解不太深入。
  • HTTP Get 請求方式中的編碼解碼規則:Get 請求方式中請求引數會被附加到位址列的 URL 之後,URL 組成:"域名:埠/contextPath/servletPath/pathInfo?queryString",URL 中 pathInfo 和 queryString 如果含有中文等非 ASCII 字元,則瀏覽器會對它們進行 URLEncode,編碼成為 16 進位制,然後從右到左,取 4 位(不足 4 位直接處理),每 2 位做一位,前面加上%,編碼成%XY 格式。然而 URL 中的 PathInfo 和 QueryString 字串的編碼和解碼是由瀏覽器和應用伺服器的配置決定,在我們的程式中是不能設定的。即使同一瀏覽器對 pathInfo 和 queryString 的編碼方式有可能不一樣,因為瀏覽器對 URL 的編碼格式是可設定的,這就對伺服器的解碼造成很大的困難。應用伺服器端對 Get 請求方式解碼中 pathInfo 和 queryString 的設定是不同的。比如 Tomcat 伺服器一般在 server.xml 中設定的,pathInfo 部分進行解碼的字符集是在 connector 的 <Connector URIEncoding="UTF-8"/> 中定義的;QueryString 的解碼字符集一般通過 useBodyEncodingForURI 設定,如果沒有設定,Tomcat8 之前的版本預設使用的是 ISO-8859-1,但是 Tomcat 8 預設使用的是 UTF-8。為了避免瀏覽器採用了我們不希望的編碼,在我們的程式中最好不要在 URL 中直接使用非 ASCII 字元,而是對雙位元組字元進行 URI 編碼後在放到 URL 中,JavaScript§提供了 encodeURI()函式,它提供的是 UTF-8 的 URI 編碼,也可以通過 java.net.URLEncoder.encode(str,"字符集")進行編碼。
  • HTTP Post 請求方式中的編碼解碼:請求表單中的引數值是通過 request 包傳送給伺服器,此時瀏覽器會根據網頁的 ContentType("text/html; charset=utf-8")中指定的編碼進行對錶單中的資料進行編碼,然後發給伺服器;JSP 中 contentType 設定<%@ page language="java" contentType="text/html; charset="GB18030" pageEncoding="UTF-8"%>,JSP 頁面命令中的 charset 的作用包括通知瀏覽器應該用什麼編碼方式解碼顯示網頁;提交表單時瀏覽器會按 charset 指定的字符集編碼資料(post body)傳送給伺服器;pageEncoding 屬性裡指定的編碼方式是儲存該 jsp 檔案時所用的編碼,比如 eclipse 的文字編輯器可以根據該屬性決定儲存該檔案時採用的編碼方式;伺服器端通過 Request.setCharacterEncoding() 設定編碼,然後通過 request.getParameter 獲得正確的資料。圖 13 是 POST 請求中沒有設定 ContentType 出現的亂碼的例子及解決辦法如清單 3 所示。

圖 13. POST 請求中出現亂碼

清單 3. POST 請求設定 setContentType

1

2

3

4

5

6

7

8

9

10

11

12

13

14

protected void doPost(HttpServletRequest request, HttpServletResponse

response) throws ServletException, IOException {

if(!ServletFileUpload.isMultipartContent(request)){

throw new ServletException("Content type is not multipart/form-data");

}

response.setCharacterEncoding("UTF-8");//設定響應編碼

response.setContentType("text/html;charset=UTF-8");

PrintWriter out = response.getWriter();

out.write("<html><head></head><body>");

try {

List<FileItem> items = (List<FileItem>)

uploader.parseRequest(request);

}

//JSP 程式碼片段,使用 POST 請求方式

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>index</title>

<meta http-equiv="pragma" content="no-cache">

<meta http-equiv="cache-control" content="no-cache">

<meta http-equiv="expires" content="0">

</head>

<body>

<form action="FileUploadServlet" method="post" enctype="multipart/form-data">

選擇上傳檔案:<input type="file" name="fileName">

<br>

<input type="submit" value="上傳">

</form>

</body>

</html>

  • 瀏覽器顯示:通常有 JSP 和 HTML 來展示,通過實驗發現,對於網頁中的靜態內容,不同瀏覽器顯示網頁所使用的字符集原則是不一樣的,Chrome 63 和 IE11 使用 JSP 頁面命令中 contentType 和 charset 設定,html 頁面中的 charset 設定,然而 firefox 52 卻根據自己的 text encoding 方式來顯示頁面。
  • 於 JSP通過 JSP 頁面命令<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>中的 contentType 屬性和 pageEncoding 屬性設定。在 JSP 標準的語法中,如果 pageEncoding 屬性存在,儲存該 jsp 檔案時所用的編碼由該屬性決定,如果沒有指定 pageEncoding 屬性,那麼儲存該 jsp 檔案的編碼就由 contentType 屬性中的 charset 決定,如果 charset 也不存在,JSP 頁面的字元編碼方式就採用預設的 ISO-8859-1;charset 的作用包括通知瀏覽器應該用什麼編碼方式解碼顯示網頁,如果沒有指定 charset 預設的字符集為"ISO-8859-1";提交表單時瀏覽器會按 charset 指定的字符集編碼資料(post body)傳送給伺服器;Post 請求時,瀏覽器會根據 contentType 中 charset 指定的字符集對錶單中的資料進行編碼,然後提交給伺服器。
  • 於 HTML: <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">,其中的 charset 左右和 JSP 中的 charset 作用一樣。
  • 對於動態頁面內容:覽器根據 http 頭中的 ContentType("text/html; charset=utf-8")指定的字符集來解碼伺服器傳送過來的位元組流。在應用伺服器端可以呼叫 HttpServletResponse.setContentType()設定 http 頭的 ContentType,即伺服器端編碼方式。

另外一個亂碼的例子就是我們在下載檔名為雙位元組的檔案時,下載後文件名為亂碼,如圖 15 所示。這是因為 Header 只支援 ASCII 字符集,將不在 ASCII 字符集內中的其他字元全部編碼為 3F 即問號?,解決辦法就是對中文檔名使用 url 編碼後 URLEncoder.encode(filename,charset)再放到 Header 中,如清單 4 所示。

圖 14. GET 請求中出現亂碼

清單 4. 雙位元組檔名亂碼

1

2

3

4

5

6

7

8

protected void doGet(HttpServletRequest request, HttpServletResponse

response) throws ServletException, IOException {

String fileName = getDecodeParameter(request,"fileName");

String userName = getDecodeParameter(request, "username");

response.setHeader("Content-Disposition", "attachment; filename=\"" +

URLEncoder.encode(fileName,"utf-8") + "\";userName=\"" +

URLEncoder.encode(userName,"utf-8") + "\"");

}

資料庫操作過程中的亂碼

在實際應用中,和資料庫操作相關的亂碼可能出現在資料的匯入和匯出操作中,在整個過程中涉及到的字符集有伺服器端資料庫字符集、客戶端作業系統字符集、客戶端環境變數 nls_lang(lang_territory.charset),這三個引數的工作流程如圖 15 所示。如果這三個引數設定一樣,整個資料庫操作中就不會出現亂碼問題,但是實際應用中客戶端的情況複雜多樣,很難保持三者一致,涉及到雙位元組字元就需要伺服器端進行轉碼操作,而轉碼的橋樑就是 Unicode 字符集,這就要求資料庫本身支援 UTF-8 編碼方式。為了編碼資料庫操作過程中的亂碼問題,在建立資料庫的時候使用 UTF-8 編碼方式,如果僅在某些列中使用多語言資料,則可以使用 SQL NCHAR 資料型別(NCHAR,NVARCHAR2 和 NCLOB)以 UTF-16 或 UTF-8 編碼形式儲存 Unicode 資料,避免儲存空間的浪費。

圖 15. Oracle 資料庫字符集

總結

本文基於日常碰到的亂碼現象和專案實踐,詳細綜述了常用字符集,編碼以及使用場景;作為普通使用者碰到的亂碼錶象分析和解決辦法,以及從程式設計角度總結和分析了常見亂碼情況。希望能為讀者深入理解和解決亂碼問題提供幫助。

參考資料 (resources)

轉自: