2015年4月27日

Unicode Surrogate Pairs

特殊的中文字

這個字是個特別的中文字,它沒辦法在一般最通用的 UTF-8 網頁中被看見,也沒辦法儲存到一個定義為 UTF-8 encoding 的 MySQL Database 裡面。這個字在網頁上看不見,所以這篇文章的這個字都是用圖片貼上去的

講到 Unicode 的問題,又需要去查看一串字元編碼歷史,淺釋 Unicode 這篇文章把字元編碼的來龍去脈講得很清楚了,就不在這邊贅述。

如果要完全解決文字編碼的問題,其實就是使用 UTF-32 或是 UTF-16 就好了,但是現在討論一個重點,為什麼 UTF-8 在網頁世界中,會是最流行的編碼方式,原因就是大部分的作業系統與系統程式都是英美語系的工程師做的,為了跟 ASCII code 相容,如果也能夠使用一樣的程式就處理掉多國語言編碼,這樣就天下太平了,程式不需要改太多。

像我們這樣撰寫網頁程式的工程師,其實早已經習慣撰寫網頁,就使用 UTF-8 編碼,一般也不會想到會遇到超過 UTF-8 規範定義的字元,如果使用者硬是在網頁上輸入了 這個字,對於 Java 來說,他是可以接受的,因為 Java 語言內部的編碼格式是 UTF-16,但對於資料庫來說,當我們要把資料放進 table 時,得到的卻是一個 SQLException。

SQL Error: 1366, SQLState: 22001

使用 UTF-8,調整 DB 設定也沒用

遇到 SQLExcpetion 的問題,很直覺會先想到是不是資料庫的編碼寫錯了,還是 MySQL J-Connector 出了什麼問題,連線時忘了設定 encoding。

但因為根本的原因在於 Java 可以容許使用 UTF-16 的字,但是 MySQL Database 以及 Browser 的網頁都是使用 UTF-8,所以不管我們再怎麼改設定,或是想方法,其實都是徒勞無功的。

Surrogate Pair

再看一下這一段 java 程式,雖然鍵入了四個中文字的字串,但實際上,卻因為第三個字有著不同的特性,最後輸出時,就把第三個字排除掉了。

Java 的 Character.isHighSurrogate 與 Character.isLowSurrogate 可以用來判斷是不是符合了上面描述的規則的特殊字。

        String text = "測試字";

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            System.out.println("ch="+ch+":"+String.format("%02X", (int)ch)+"\n");
            if (!Character.isHighSurrogate(ch) && !Character.isLowSurrogate(ch)) {
                sb.append(ch);
            }
        }

        System.out.println("sb=" + sb.toString());

執行後輸出結果為

D:\temp>java Test4
ch=測:6E2C

ch=試:8A66

ch=?:D85D

ch=?:DE57

ch=字:5B57

sb=測試字

除了 這個字之外,其他的字都是 2 bytes,Java 內部的 UTF-16 會使用一個單位(2 byte)儲存 UCS-2 字碼,超過這個範圍的字碼則會拆解成「代理對(surrogate pair)」,用4 bytes儲存。

解決方案

  1. 忽略這個問題,畢竟遇到這些特殊字的機會並不多
  2. 儲存到 DB 前,使用上面的 isHighSurrogate 與 isLowSurrogate,將不合法的字元排除掉。
  3. 將 DB 與 網頁輸出都改為 UTF-16,但是 IE 沒有支援 UTF-16,只能用在 Chrome,不建議這樣做,因為 UTF-8 才是網頁的主流。

Reference

UTF-8 wiki

How to handle SQL state [HY000]; error code [1366]; Incorrect string value?

What is a surrogate pair in Java?