2014-07-15

偵測檔案使用的編碼是Utf-8或Big5-新版

前一篇文章討論到如何偵測檔案的編碼方式,但由於還是有很小的機會會誤判,心裡總是有疙瘩在,不確定廠商到底會丟怎樣的資料來(廠商的代名詞就是腦X),而且訂單XML內中文字的數量很少,若只有一筆,很可能只有三個中文字而已,於是從utf-8是如何編碼開始研究起。

沒想到規則意外的簡單,因為我不須知道太詳細的細節,只需知道幾個重點:

  1. 只要是0開頭的byte就表示是ascii編碼,也就是0xxxxxxx後面的七碼x相容於傳統ascii編碼
  2. 非ascii編碼,一律以1開頭再接0,並且最少兩個1
  3. 1的數量指出這個字是由幾個byte所組成,如1110xxxxx表示這個字要3個byte
  4. 其後每個子byte都為10開頭
  5. 最多每個字4個byte
知道規則之後,要判斷就很簡單了,底下是判斷的流程和邏輯:
  1. 依照規格,只要有非0、非110、非1110、非11110開頭的byte,就是非utf-8
  2. 110開頭的byte,需檢查是否有下一個byte,或底下一個byte是否為10開頭
  3. 1110開頭的byte,需檢查是否有下兩個byte,或底下兩個byte是否皆為10開頭
  4. 11110開頭的byte,需檢查是否有下三個byte,或底下三個byte是否皆為10開頭
  5. 全部byte跑完之後的指標是否等於檔案長度
底下就是程式碼了:
/// <summary>
/// 檢查是否符合utf-8編碼的核心程式
/// </summary>
/// <param name="bytes">文字原始byte陣列</param>
/// <returns></returns>
private static bool _IsUtf8(byte[] bytes) {
    int i = 0;
    int size = bytes.Length;

    //跑每一個byte,需要每個byte都符合utf-8編碼規則才算是utf-8編碼
    while (i < size) {
        int step = 0;
        if ((bytes[i] & 0x80) == 0x00) {
            //utf-8的ascii字元,為0開頭,OK跑下一個位元
            step = 1;
        }
        else if ((bytes[i] & 0xE0) == 0xC0) {
            /*
              110xxxxx
            & 11100000 (0xE0)
            ----------
              11000000 (0xC0)
            */
            //utf-8使用2bytes編碼格式為 110xxxxx 10xxxxxx
            //如果是110xxxxx開頭,要判斷下個位元是否是10開頭

            //如果下一個已經沒有資料了,表示這不是utf-8編碼
            if (i + 1 >= size) { return false; }

            //如果下個位元不是10開頭,也表示這不是utf-8編碼
            if ((bytes[i + 1] & 0xC0) != 0x80) { return false; }

            //繼續跑下2個
            step = 2;
        }
        else if ((bytes[i] & 0xF0) == 0xE0) {
            /*
              1110xxxx
            & 11110000 (0xF0)
            ----------
              11100000 (0xE0)
            */
            //utf-8使用3bytes編碼格式為 1110xxxx 10xxxxxx 10xxxxxx
            //所以如果是1110xxxx開頭,要判斷下兩個byte是否都是10開頭

            //如果下兩個已經沒有資料了,表示不是utf-8編碼
            if (i + 2 >= size) { return false; }

            //如果下兩個位元不是10開頭,也不是utf-8編碼
            if ((bytes[i + 1] & 0xC0) != 0x80) { return false; }
            if ((bytes[i + 2] & 0xC0) != 0x80) { return false; }

            //繼續跑下3個
            step = 3;
        }
        else if ((bytes[i] & 0xF8) == 0xF0) {
            /*
              11110xxx
            & 11111000 (0xF8)
            ----------
              11110000 (0xF0)
            */
            //utf-8使用4bytes編碼格式為 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            //所以如果是11110xxx開頭,要判斷下3個byte是否都是10開頭

            //如果下3個已經沒有資料了,表示不是utf-8編碼
            if (i + 3 >= size) { return false; }

            //如果下3個位元不是10開頭,也不是utf-8編碼
            if ((bytes[i + 1] & 0xC0) != 0x80) { return false; }
            if ((bytes[i + 2] & 0xC0) != 0x80) { return false; }
            if ((bytes[i + 3] & 0xC0) != 0x80) { return false; }

            //繼續跑下4個
            step = 4;
        }
        else { 
            //如果都不是當然就不是utf-8了
            return false;
        }
        i += step;
    }
    if (i == size) { return true; }
    return false;
}

參考資料: