一次亂碼引發的思考

前言何爲編碼ASCIIISO8859-1GBKUnicodeUTF-8ANSIJava中編碼規則java.util.Properties類來讀取properties文件文件存儲properties類對文件讀取

前言

昨天做一個從properties文件讀取短信內容,然後到程序中動態替換變量值,發送短信這麼一個功能。本地測試完畢,發現沒有任何問題,於是乎,就非常欣喜的提交了代碼,提測。但是提交到Linux服務器上後,發送出去的短信一直亂碼的狀態,日誌打印出的短信內容也是亂碼狀態,怎麼辦?先找解決方法,於是乎,找到了兩種種解決方式:

  • 直接將短信內容改成unicode編碼串放至properties文件
#註冊成功短信 
sms.register.content=\u5C0A\u6599\u5DF2\u63D0\u4EA4{0}\uFF0C\u8BF7\u52FF\u6CC4\u9732\uFF0C\u4E3A\u4FDD
  • 在載入properties文件的地方指定UTF-8編碼
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="order" value="0" />
        <property name="ignoreUnresolvablePlaceholders" value="true" />
        <property name="locations">
            <list>
                <value>classpath:application.properties</value>
            </list>
        </property>
        <!-- 指定UTF-8編碼-->
        <property name="fileEncoding" value="UTF-8"/>
    </bean>

按照上述兩種方法之一的果然解決了短信內容亂碼的問題,但卻引起我對編碼這個一直模模糊糊的領域的思考。

  • unicode編碼爲什麼就可以正常讀取出來中文字符呢?

  • 爲什麼本地編輯器直接寫中文可以正常讀取?

於是乎,趁此機會,好好惡補梳理下這關於編碼這塊一知半解的盲區點。

何爲編碼

計算中只有0和1這兩種狀態,美國人根據8位2進制位組成一個字節來表示不同的字符和控制動作。所以剛開始出現了ASCII編碼,後來世界上其他國家和地區也開始使用計算機,由一個字節來表示一個字符,總共也就256種可能,同時前128位字節都被美國人編碼佔了,一個字節不夠,那就兩個字節唄。於是各個國家針對自己的文化符號推出了自己的編碼標準。

. ASCII

最開始計算機是美國使用的,英文中只有26個字母,再加上一些控制字符和特殊的轉義字符全部都編碼進去,一直到127號。剛好是8位字節用了後7位,所以由美國提出的最高位爲0、包含了128位字符的編碼標準稱之爲ASCII(American Standard Code for Information Interchange,美國信息互換標準代碼)編碼,這是很重要的一個編碼標準,後面的編碼基本都基於該編碼擴展,所以基本上都兼容該編碼標準。

比如大寫字母A,在ASCII編碼中是第65號元素,因此在計算機中存儲爲0100 0001

二進制          十進制     十六進制          符號
0100 0001       65          41             A

. ISO8859-1

ISO859-1是單字節編碼的字符集,同時也是存儲和傳輸的編碼方式。美國人把一個字節的前127號字節位都編碼進去了,這個時候歐洲人開始使用計算機了,他們把第8位也利用起來,但還是單字節編碼同時包含了ASCII中的128位字符,同時擴展利用第8位(僅從160-255之間有值),支持歐洲的大部分國家的編碼字符。

. GBK

當中國人開始使用計算機時,已經沒有可利用的字節狀態來表示漢字,況且一個字節最多能表示256個字符,而常用漢字則達3000多個。於是我國便制定了一個GB2312的編碼標準:

一個小於127的字符的意義與原來相同,但兩個大於127的字符連在一起時,就表示一個漢字,前面的一個字節(他稱之爲高字節)從0xA1用到 0xF7,後面一個字節(低字節)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。在這些編碼裏,我們還把數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在 ASCII 裏本來就有的數字、標點、字母都統統重新編了兩個字節長的編碼,這就是常說的”全角”字符,而原來在127號以下的那些就叫”半角”字符了。

後來發現好多古文、罕見字沒有編碼進去,於是又有了GBK標準。
只要第一個字節是大於127就固定表示這是一個漢字的開始,不管後面跟的是不是擴展字符集裏的內容。結果擴展之後的編碼方案被稱爲 GBK 標準,GBK 包括了 GB2312 的所有內容,同時又增加了近20000個新的漢字(包括繁體字)和符號。

後來少數民族也要用電腦了,於是我們再擴展,又加了幾千個新的少數民族的字,GBK 擴成了 GB18030。從此之後,中華民族的文化就可以在計算機時代中傳承了。

. Unicode

世界上每個國家都有自己的編碼標準,顯然很不利於國家間的信息傳遞。這時ISO(國際標準化組織)解決了這個問題。
廢了所有的地區性編碼方案,重新搞一個包括了地球上所有文化、所有字母和符號的編碼!他們打算叫它"Universal Multiple-Octet Coded Character Set",簡稱 UCS, 俗稱 "UNICODE"。

UNICODE 開始制訂時,計算機的存儲器容量極大地發展了,空間再也不成爲問題了。於是 ISO 就直接規定必須用兩個字節,也就是16位來統一表示所有的字符,對於ascii裏的那些“半角”字符,UNICODE 包持其原編碼不變,只是將其長度由原來的8位擴展爲16位,而其他文化和語言的字符則全部重新統一編碼。由於"半角"英文符號只需要用到低8位,所以其高8位永遠是0,因此這種大氣的方案在保存英文文本時會多浪費一倍的空間。

目前採用的是UCS-2標準,即2個字節來表示一個字符。Unicode只是一個編碼字符集,它只規定了字符的對應的編碼值,而並沒有規定如何存儲和傳輸。

. UTF-8

採用Unicode編碼時,如果存儲的是中文時,一個字符兩個字節來表示,好像沒啥大問題。但如果是英文時,這個時候它的高位永遠都是0,只有低位是ACCII的值,則造成了一個空間的巨大浪費和帶寬支出。於是就提出了一種針對Unicode編碼的存儲和傳輸方式-UTF-8 。

UTF-8是一種變長的Unicode編碼的實現方式。它通過1-4個字節(最長是6個字節)的長度來對Unicode碼錶中的字符通過某種規則來重新編碼,便於存儲和傳輸。


  • 對於單字節的符號,字節的第一位設爲0,後面7位爲這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。

  • 對於n字節的符號(n>1),第一個字節的前n位都設爲1,第n+1位設爲0,後面字節的前兩位一律設爲10。剩下的沒有提及的二進制位,全部爲這個符號的unicode碼。

Unicode符號範圍       | UTF-8編碼方式
(十六進制)            |
 (二進制)
--------------------+---------------------------------------------
0000 0000-0000 007| 0xxxxxxx
0000 0080-0000 07FF |
 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF |
 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

爲什麼UTF-8編碼一個字符最長是6個字節呢?
如果是6個字節,則其二進制爲:
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
其中的X替代的是unicode編碼集中的有效位數,剛好是32位,也就是unicode中的最長4個字節編碼。

. ANSI

ANSI編碼是windows的默認編碼,對於英文文件是ASCII編碼,對於簡體中文文件是GBK編碼(只針對Windows簡體中文版,如果是繁體中文版會採用Big5碼)。

總結完上面的編碼理論知識後,來看下前面的主題中的兩個疑惑點。

Java中編碼規則

. java.util.Properties類來讀取properties文件

spring中的PropertyPlaceholderConfigurer類來讀取文件,其實就是java.util.Properties類來讀取文件。Properties類中默認編碼規則爲ISO8859-1。

 try{
           File file = new File("E:\\aaa.txt");
           Properties propertiesUtil = new Properties();
           propertiesUtil.load(new FileInputStream(file));
           //propertiesUtil.load(new InputStreamReader(new FileInputStream(file),"utf-8"));
           System.out.println(propertiesUtil.get("key1"));
           System.out.println(propertiesUtil.get("key2"));
           System.out.println(((char)21517));
       }catch (Exception e){
          e.printStackTrace();
       }

aaa.txt文件內容如下:

key1=\u540C\u6B65\u5E26\u5A01\u5BCC\u901A
key2=我是一個

上述代碼執行的結果是:

同步帶威富通
我是一个

也就是說key2的取值是亂碼的,而文件中寫入漢字的Unicode編碼串是正常的。但如果是啓用註釋行的代碼則都是可以正常讀取的。下面來分析其中的讀取過程。

文件存儲

先看下aaa.txt在Windows存儲的二進制碼。

aaa.txt文本文件


 

可以看到實際存儲中,對應的Unicode編碼串就是當成普通的英文字符處理,\對應的ACSII編碼就是5C,u對應的ASCII編碼就是75。
而對於中文“我是一個”的處理則不然,“我”的Unicode編碼爲\u6211。
6211對應的二進制編碼爲:0110 0010 0001 0001
根據Unicode編碼與UTF-8編碼對應的規則,“我”這個中文字符應該是以三個字節來進行UTF-8編碼的。

1110xxxx 10xxxxxx 10xxxxxx

將6211對應的二進制編碼填充進去則得到:11100110 10001000 10010001 ,轉成16進制則爲E6 88 91,可以看到與通過編輯器工具查看的編碼是一樣的。所以當中文以UTF-8編碼存儲時,首先是先將中文字符轉成對應的Unicode編碼,然後將該編碼轉成UTF-8表示的三個字節的二進制編碼存儲在文本文件中。

properties類對文件讀取

/**
** 以字節輸入流作爲構造函數的參數
*/

 public synchronized void load(InputStream inStream) throws IOException {
        load0(new LineReader(inStream));
    }
private String loadConvert (char[] inint off, int len, char[] convtBuf{
        if (convtBuf.length < len) {
            int newLen = len * 2;
            if (newLen < 0) {
                newLen = Integer.MAX_VALUE;
            }
            convtBuf = new char[newLen];
        }
        char aChar;
        char[] out = convtBuf;
        int outLen = 0;
        int end = off + len;

        while (off < end) {
            aChar = in[off++];
            if (aChar == '\\') {
                aChar = in[off++];
                if(aChar == 'u') {
                    // Read the xxxx
                    int value=0;
                    for (int i=0; i<4; i++) {
                        aChar = in[off++];
                        switch (aChar) {
                          case '0'case '1'case '2'case '3'case '4':
                          case '5'case '6'case '7'case '8'case '9':
                             value = (value << 4) + aChar - '0';
                             break;
                          case 'a'case 'b'case 'c':
                          case 'd'case 'e'case 'f':
                             value = (value << 4) + 10 + aChar - 'a';
                             break;
                          case 'A'case 'B'case 'C':
                          case 'D'case 'E'case 'F':
                             value = (value << 4) + 10 + aChar - 'A';
                             break;
                          default:
                              throw new IllegalArgumentException(
                                           "Malformed \\uxxxx encoding.");
                        }
                     }
                    out[outLen++] = (char)value;
                } else {
                    if (aChar == 't') aChar = '\t';
                    else if (aChar == 'r') aChar = '\r';
                    else if (aChar == 'n') aChar = '\n';
                    else if (aChar == 'f') aChar = '\f';
                    out[outLen++] = aChar;
                }
            } else {
                out[outLen++] = aChar;
            }
        }
        return new String (out0, outLen);
    }

loadConvert()方法中當讀取到\u開頭的字符時,就當成是unicode編碼來處理,直到讀取到該編碼結束。將Unicode編碼得到的十進制進行強轉成字符則得到該中文字符。

out[outLen++] = (char)value;

這也就解釋了爲什麼properties文件中,寫的是Unicode編碼的16進制串可以正常翻譯成中文字符的原因。


同理,來看下直接寫中文的方式是如何讀取的呢?
上面已分析得出“我”字符的實際上是三個字節編碼存儲的。那麼字節流中就是E6 88 91 ,對應的十進制就是230 136 145,而Properties類默認編碼爲ISO8859-1,所以直接將230翻譯成了æ 字符,而ISO8859-1編碼實際上從160-255之間纔有實際的實體字符與之對應。所以Properties類自動填充\u,上述後倆字節於是就變成了

\u0088 \u0091 

最終翻譯出來就是ˆ‘這倆unicode編碼表中的字符。最後“我”這個中文字符於是讀取出來就是我 ,這也就是我們經常看到的中文亂碼。
而如果是採用指定編碼格式的字符流進行處理的話則不會發生亂碼的現象。

propertiesUtil.load(new InputStreamReader(new FileInputStream(file),"utf-8"));

InputStreamReader是將字節流轉成字符流,所以會將該字節流以指定編碼去讀,因此也能讀出正常的中文字符。

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