2015-03-03

[Java] 自定字符集 Charset

網路上找到有關自定 Charset 的資料很少,大部分的人都是不建議自行定義一個 Charset,的確沒有什麼特別的理由,實在不需要去做這樣的事,但實際遇到的是工作上用的 DB 實在太舊,還在用 Big5 編碼,以及一堆的自造字,偏偏大部分都還是人名,讓別人的名子變成奇怪的字,實在很不禮貌,基於這個理由我延伸了 Big5 Charset。

要製作一個 Charset 一定要瞭解 Java 的字串核心,如果之前是寫 C 語言的朋友,首先要認知 char 跟 btye 是不一樣的,在 Java 的 char 是固定 Unicode 編碼,所有輸入都會轉成 Unicode,輸出時在轉成指定編碼,如下圖:



那麼要實做哪些東西呢??
class Charset
定義 Charset 的名稱跟提供 Encoder 跟 Decoder,跟據 IANA 的規範自定的 Charset 名稱必須以 X-x- 開頭。
class CharsetDecoder
字符解碼器負責將 byte[] 轉換成 char[]
class CharsetEncoder
字符編碼器負責將 char[] 轉換成 byte[]
class CharsetProvider
Charset 提供者,以名稱提供 Charset,在 Charset.forName("xxx") 調用時會尋訪所有的 Provider 來取得 Charset。
META-INF/services/java.nio.charset.spi.CharsetProvider
告知 JVM 將 CharsetProvider 註冊到延伸清單中,JVM 在啟動的時候會掃描所有這個路徑的檔案。

最後再將 {Charset}.jar 檔複製到 {jre}/lib/ext 目錄下就部署完畢


Big5_Extend

public class Big5_Extend extends Charset {

    private static final String BASE_CHARSET = "Big5";
    private static final String NAME = "X-Big5-Extend";
    private static final String[] ALIASES = { "X-Big5_Extend" };
    private Charset baseCharset;

    public Big5_Extend() {
        this(NAME, ALIASES);
    }

    public Big5_Extend(String canonical, String[] aliases) {
        super(canonical, aliases);
        baseCharset = Charset.forName(BASE_CHARSET);
    }

    public boolean contains(Charset cs) {
        return this.getClass().isInstance(cs) ||
                baseCharset.getClass().isInstance(cs);
    }

    public CharsetDecoder newDecoder() {
        return new Decoder(this, baseCharset.newDecoder());
    }

    public CharsetEncoder newEncoder() {
        return new Encoder(this, baseCharset.newEncoder());
    }

    // ...
}
繼承自 Charset,我們只要實作名稱定義跟 Encoder / Decoder 兩件事,當然如果有 char mapping array 也是在這裡初始化,讓所有 Encoder 跟 Decoder 可以共用同一個記憶體空間。


解碼處理

protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
    base.reset(); /* 重置狀態 */

    /* 先用原生的 Big5 進行解碼 */
    CoderResult result = base.decode(in, out, true);
    if(!result.isUnmappable() || in.remaining() < 2){ return result; }


    /* 無法轉換,進一步使用自訂的解碼 */
    int pos = in.position();
    char big5Char = (char)(in.get(pos) << 8 | in.get(pos + 1));
    char outChar;

    switch (big5Char) {
        case '\uFA40': outChar = '\u5803'; break; /* 堃 */
        case '\uFA41': outChar = '\u83D3'; break; /* 菓 */
        case '\uFA42': outChar = '\u854B'; break; /* 蕋 */
        case '\uFA43': outChar = '\u4F8A'; break; /* 侊 */
        default: return result; /* 不在清單內直接回傳 */
    }
    out.put(outChar);

    in.position(pos + 2);
    return decodeLoop(in, out); /* 遞迴處理*/
}
解碼的部分就是先呼叫 big5 原本的 decode,當發生無法解碼的四種狀況就會停止解碼,回傳解碼狀態,我們只要針對 isUnmappable 的狀態接著處裡,base.reset() 是為了清除 Decoder 內部的狀態紀錄,不然會被前一次的解碼結果所影響。


編碼處理

protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
    base.reset(); /* 重置狀態 */

    /* 先用原生的 Big5 進行編碼 */
    CoderResult result = base.encode(in, out, true);
    if(!result.isUnmappable() || out.remaining() < 2){ return result; }


    /* 無法轉換,進一步使用自訂的編碼 */
    int pos = in.position();
    char uniChar = in.get(pos);
    char outChar;

    switch (uniChar) {
        case '\u5803': outChar = '\uFA40'; break; /* 堃 */
        case '\u83D3': outChar = '\uFA41'; break; /* 菓 */
        case '\u854B': outChar = '\uFA42'; break; /* 蕋 */
        case '\u4F8A': outChar ='\uFA43'; break; /* 侊 */
        default: return result; /* 不在清單內直接回傳 */
    }
    out.put((byte)(outChar >> 8));
    out.put((byte)outChar);

    in.position(pos + 1);
    return encodeLoop(in, out); /* 遞迴處理*/
}
編碼的部分跟解碼採用相同的方式,一樣是先呼叫 big5 原本的 encode。


CoderResult 四種狀態

  • UNDERFLOW 欠位
  • OVERFLOW 溢位
  • MALFORMED 有缺陷的輸入
  • UNMAPPABLE 無映射字符


完整的 Big5_Extend

package com.custom.nio.charset;

import java.nio.CharBuffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;


public class Big5_Extend extends Charset {

    private static final String BASE_CHARSET = "Big5";
    private static final String NAME = "X-Big5-Extend";
    private static final String[] ALIASES = { "X-Big5_Extend" };
    private Charset baseCharset;

    public Big5_Extend() {
        this(NAME, ALIASES);
    }

    public Big5_Extend(String canonical, String[] aliases) {
        super(canonical, aliases);
        baseCharset = Charset.forName(BASE_CHARSET);
    }

    public boolean contains(Charset cs) {
        return this.getClass().isInstance(cs) ||
                baseCharset.getClass().isInstance(cs);
    }

    public CharsetDecoder newDecoder() {
        return new Decoder(this, baseCharset.newDecoder());
    }

    public CharsetEncoder newEncoder() {
        return new Encoder(this, baseCharset.newEncoder());
    }



    private class Decoder extends CharsetDecoder {
        /* Java 原生的 Big5 解碼器 */
        private final CharsetDecoder base;

        Decoder(Charset cs, CharsetDecoder base) {
            super(cs, base.averageCharsPerByte(), base.maxCharsPerByte());
            this.base = base;
        }

        @Override
        protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
            base.reset(); /* 重置狀態 */

            /* 先用原生的 Big5 進行解碼 */
            CoderResult result = base.decode(in, out, true);
            if(!result.isUnmappable() || in.remaining() < 2){ return result; }


            /* 無法轉換,進一步使用自訂的解碼 */
            int pos = in.position();
            char big5Char = (char)(in.get(pos) << 8 | in.get(pos + 1));
            char outChar;

            switch (big5Char) {
                case '\uFA40': outChar = '\u5803'; break; /* 堃 */
                case '\uFA41': outChar = '\u83D3'; break; /* 菓 */
                case '\uFA42': outChar = '\u854B'; break; /* 蕋 */
                case '\uFA43': outChar = '\u4F8A'; break; /* 侊 */
                default: return result; /* 不在清單內直接回傳 */
            }

            out.put(outChar);

            in.position(pos + 2);
            return decodeLoop(in, out);
        }
    }



    private class Encoder extends CharsetEncoder {
        /* Java 原生的 Big5 編碼器 */
        private final CharsetEncoder base;

        Encoder(Charset cs, CharsetEncoder base) {
            super(cs, base.averageBytesPerChar(), base.maxBytesPerChar());
            this.base = base;
        }

        @Override
        protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
            base.reset(); /* 重置狀態 */

            /* 先用原生的 Big5 進行編碼 */
            CoderResult result = base.encode(in, out, true);
            if(!result.isUnmappable() || out.remaining() < 2){ return result; }


            /* 無法轉換,進一步使用自訂的編碼 */
            int pos = in.position();
            char uniChar = in.get(pos);
            char outChar;

            switch (uniChar) {
                case '\u5803': outChar = '\uFA40'; break; /* 堃 */
                case '\u83D3': outChar = '\uFA41'; break; /* 菓 */
                case '\u854B': outChar = '\uFA42'; break; /* 蕋 */
                case '\u4F8A': outChar ='\uFA43'; break; /* 侊 */
                default: return result; /* 不在清單內直接回傳 */
            }

            out.put((byte)(outChar >> 8));
            out.put((byte)outChar);

            in.position(pos + 1);
            return encodeLoop(in, out);
        }
    }
}


CharsetProvider

package com.custom.nio.charset;

import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

/** 字元編碼器連結器,用來向 JVM 提交自訂的編碼器
 */
public class CharsetProvider extends java.nio.charset.spi.CharsetProvider {

    static Map<String, Charset> name2charset;
    static Collection<Charset> charsets;

    public Charset charsetForName(String charsetName) {
        if (charsets == null){ init(); }
        return name2charset.get(charsetName.toLowerCase());
    }

    public Iterator<Charset> charsets() {
        if (charsets == null){ init(); }
        return charsets.iterator();
    }

    void init() {
        name2charset = new HashMap<String, Charset>();

        charsets = new HashSet<Charset>();
        charsets.add(new Big5_Extend());

        for (Charset charset : charsets) {
            name2charset.put(charset.name().toLowerCase(), charset);
            for (String aliase: charset.aliases()) {
                name2charset.put(aliase.toLowerCase(), charset);
            }
        }
    }
}


java.nio.charset.spi.CharsetProvider

com.custom.nio.charset.CharsetProvider
內容就一行類別定義


測試

public class Test {

    public static void main(String[] args) throws Throwable {
        String charset = "X-Big5-Extend";
        String source = "堃菓蕋侊";

        byte[] bytes = source.getBytes(charset);
        for (byte b : bytes) {
            System.out.printf("%x ", b);
        }
        System.out.println("\n");
        // fa 40 fa 41 fa 42 fa 43

        String result = new String(bytes, charset);
        System.out.println(result);
        // 堃菓蕋侊
    }
}

參考自:Java字符编码解码的实现详解_java_脚本之家

0 回應: