路邊逮一美國IT佬,問其亂碼問題,愕然相對,你這是鬧哪樣,全然不知。相反,如遇國內IT民工,如臨大敵。誠然,在國內做軟件,像我這種初級程序員,遇到字符亂碼,往往會不知所措,直冒汗。第一台計算機誕生不久,就有了ASCII編碼,后來因ASCII不能滿足現下的字符,就由ISO組織擴展成為ISO-8859-1。計算機的普及,各個國家都有了自己的編碼,目的可以在計算機上可以顯示它們的語言。比如GBK編碼來表示中文。但這也產生了編碼不一致的問題,后來unicode統一了全世界的語言的編碼規則,它可以表示全世界的語言。那為何美國人就不會遇到字符亂碼的問題?美國人使用的是英文,而中國人使用的是中文。原因是全世界的字符編碼對英文的編碼規則是一致的,都是以一個字節來保存英文的。而中文不同,有些編碼根本不支持中文,比如ISO-8859-1,有些編碼對中文的編碼規則不一致,比如GBK以2個字節,而UTF-8是以3個字節保存中文。
一、為什么需要字符編碼
了解此問題前,首先得理解幾個過程
- 編碼過程:將非二進制字符轉換成二進制("string".getBytes(String encoding))
- 解碼過程:將二進制轉換為字符(new String(byte[] c,String encoding))
- 存儲過程:計算機是一個字節一個字節存儲的,比如"中文"通過GBK編碼后為d6d0 cec4,然后計算機拆分為d6 d0 ce c4以一個字節一個字節將信息存儲
計算機只識別二進制即1010,由1和0組成的序列集,當需計算機識別其他字符時,就必須將其轉換成二進制存儲到計算機中。當需從計算機中讀取信息以某種字符形式表示時,就需從中讀取二進制信息,然后以特定的字符編碼將二進制轉換為字符。字符編碼指將字符轉換為二進制的規則。比較常見的字符編碼ISO-8859-1(常用於網絡傳輸),GBK,UTF-8(unicode的一種實現)。
二、為什么會出現亂碼
在我們溝通過程中,經常也會出現"亂碼問題"。小明想表達的意思是A,說出來的意思是B,小芳接收到的意思是C,小芳理解的意思是D。A=D時,證明此溝通成功。但往往溝通過程中,沒那么順暢,出現A!=B,B!=C,C!=D,其中任何一個環節出錯,都會造成A!=D的情況。A是二進制信息,B是編碼后的字符,C是通過某種途徑傳輸B后的字符,D是
解碼后的字符。傳輸過程中如果沒有信息丟失,B=C。所以問題往往會出現A!=B和C!=D的情況,這兩種情況就是編碼和解碼不一致導致的,這也是產生亂碼的根本原因。
三、亂碼問題的情景
細心的童靴會留意到前面溝通B->C過程中,是要將表達意思傳遞給小芳。當系統需要從外部資源讀寫數據時,外部資源可以是文件(數據庫)、網絡及內存。這里有兩個過程,數據傳輸過程和接收端發送端編碼和解碼的過程。因為發送端需要將數據編碼成二進制,由計算機通過某種載體傳輸到接收端,接收端接收到二進制數據,就需要將數據解碼。當出現亂碼問題時,我們首先確定2個端的字符編碼方式,然后統一2個端的編碼方式即可。亂碼問題的情景有二種,A!=B和B!=D(假設B=C),A!=B是編譯階段,B!=D是系統從外部資源讀寫數據階段。總結一下亂碼問題的情景:
- Java 編譯階段(A!=B)
- Java文件
- JSP文件
- 從外部資源讀寫數據階段(B!=D)
- WEB交互
- 表單提交get/post
- 超鏈接
- XMLHttpRequest異步提交get/post
- 直接在瀏覽器輸入URL
- 數據庫
- 文件
- 顯式的操作文件,I/O流
- 編寫代碼階段
- WEB交互
四、解決亂碼問題
- 文件
編寫代碼階段,eclipse平台上編寫完代碼后,需要保存到文件,普通的Java文件,通常會根據當前操作系統的默認字符編碼來保存Java文件,比如在中文環境下通常是GBK。而在jsp中,由<%@page pageEncoding="gbk"%>pageEncoding來指定頁面的字符編碼。
在使用I/O流操作文件時,有字節流和字符流2種方式。當使用字節流時固然是沒有問題的,但當使用字符流時,請看下面源碼。

1 public class FromOutsideData { 2 private final static String FILE_SEPARATOR= File.separator; 3 4 public static void main(String[] args) throws IOException { 5 String path = "D:" + FILE_SEPARATOR + "1.txt"; 6 File file = new File(path); 7 write(file, "utf-8"); 8 read(file); // 如果去讀以utf-8編碼后的文件,就會出現亂碼。 9 read(file, "utf-8"); // 指定utf-8去讀取文件,正常。 10 } 11 12 public static void read(File file) throws IOException { 13 // 使用字符流的原理是先使用字節流每次讀2個字節,然后根據當前操作系統的默認字符編碼來解碼成字符 14 BufferedReader br = new BufferedReader(new FileReader(file)); 15 String line = null; 16 StringBuilder sb = new StringBuilder(); 17 while ((line = br.readLine()) != null) { 18 sb.append(line); 19 } 20 System.out.println(sb.toString()); 21 } 22 23 public static void read(File file, String charset) throws IOException { 24 // 使用這種方式可以顯式的指定字符編碼來解碼 25 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset)); 26 String line = null; 27 StringBuilder sb = new StringBuilder(); 28 while ((line = br.readLine()) != null) { 29 sb.append(line); 30 } 31 System.out.println(sb.toString()); 32 } 33 34 /** 35 * 根據charset的字符編碼寫文件 36 */ 37 public static void write(File file, String charset) throws IOException { 38 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), charset)); 39 bw.write("is 最牛x轟轟滴"); 40 bw.flush(); 41 bw.close(); 42 } 43 }
- WEB交互
WEB交互,這里特指瀏覽器與服務器基於http協議進行數據傳輸的過程。http報頭的首部contentType指定了數據傳輸過程中的字符編碼和內容編碼。字符編碼不等同於內容編碼,像MIME指的是內容編碼,它規定了內容的格式。像text/html、application/x-www-form-urlencoded、application/vnd ms-excel等等,例如contentType:"text/html;charset=utf-8",指瀏覽器告訴服務器,傳遞的數據的內容格式是html,字符編碼是utf-8。同樣服務器返回的數據,也是通過contentType來告訴瀏覽器數據的內容編碼和字符編碼。比如,servlet可以通過response.setContentType("text/html;charset=utf-8");所以我們只要正確的指定contentType的字符編碼就可以避免WEB交互的亂碼問題。在jsp頁面有2個部分可以指定contentType的,第一是<%@page contentType="text/html;charset=utf-8"%>,第二是<meta http-equiv="Content-Type" content="text/html; charset=utf-8">,但前者的優先級高於后者,瀏覽器確定頁面字符編碼有4個步驟,首先看<%@page%>有沒有指定,然后是自動檢測,而后會meta,最后會按ISO-8859-1默認編碼來編碼。服務器設置字符解碼的地方有兩個部分:第一是WEB服務器的配置文件指定,第二是request.setCharacterEncoding("utf-8");知道了這些后,我們再來看看WEB交互的亂碼問題。
瀏覽器發送數據到服務器的字符編碼
- 超鏈接
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/ \________/\_________/ \__/
| | | | |
scheme authority path query fragment 前面是URL的組成成分,path部分的編碼會比較麻煩,這部分會由瀏覽器語言版本決定,如果是中文則以GBK編碼,如果英文環境則以ISO-5589-1編碼。所以我們需要用js的方法encodeURIComponent(s,enc)來統一編碼path部分,而query部分由contentType決定的。
<Connector port="8080" protocol="HTTP/1.1"
maxThreads="150" connectionTimeout="20000"
redirectPort="8443" URIEncoding="GBK"/>
URIEncoding告訴服務器servlet解碼URL時采用的編碼。而resin服務器可以直接通過request.setCharacterEncoding("gbk")指定;
- Java 編譯階段
當我們編寫完源碼后,通常會運行javac xxx, 將其編譯為*.class文件,編譯的時候會讀取源碼,那么是以什么編碼來讀取呢,默認是按操作系統的語言環境的,中文環境默認是gbk,如我們需要指定,可以 javac xxx -encoding utf-8。
- 數據庫