找出字串中隱藏的Big5造字字元,並用Unicode將其取代

大概是去年簡單處理在Excel檔案尋找造字字元,被程式設計之神詛咒,連我自己都很驚訝,到西元2018年這種鳥問題又爆發了,而且這次需要很正式的面對它!或許這也是老程式設計師永遠逃避不了的問題吧。

總而言之,相關編碼知識可能要請大家要先建立起來。

Big5      Unicode
8140–8DFE U+EEB8–U+F6B0
8E40–A0FE U+E311–U+EEB7
C6A1–C8FE U+F6B1–U+F848
FA40–FEFE U+E000–U+E310

如何找出字串中的Big5使用者造字字元?

網路上流傳的利用.NET Framework內建的Big5轉碼機制,透過尋找「?」字元(HEX: 3F、Dec: 63),就可以進行Big5的難字與使用者造字字元偵測,其實這個解決方案只能解決難字問題。這類的轉碼方式大概會長得像下列程式碼:

var oBig5 = System.Text.Encoding.GetEncoding("Big5");
string cQuestionMark = oBig5.GetString(oBig5.GetBytes(new char[] { '堃' }));
if (cQuestionMark == '?') { ... }
Console.WriteLine(String.Concat(cQuestionMark.Select(x => ((int)x).ToString("X"))));

在真正的深入套用後會發現,.NET內建的Utf16轉Big5機制並沒有實作「依據EUDC定義的造字區間」對應「?」,他只有實作「在Unicode有這個字但Big5沒這個字時」吐出「?」,例如「堃」。

舉例來說:FDCD(Big5)這個字確定被定義在EUDC的「FA40–FEFE」區間內,那麼oBig5.GetString我們期待吐回「?」,但顯然的回傳的Char卻是「E242」。以下是這個字的相關解碼,這證明找問號是行不通的:

中文字元:(可以使用微軟新注音輸入「‵BFDCD」)
UTF16(HEX):E242(將字元ToInt32後的返回值)
UTF16(DEX):57922
UTF16(NCR):
BIG5 (HEX):FDCD

答案:若是造字區間,不要oBig5.GetString找問號,要找PUA對應區間

答案就是.NET Framework沒有這種機制,自己K知識後硬幹吧!

//字典檔
private static readonly System.Collections.Generic.Dictionary<int, char> oEudcUtfDictionary = new System.Collections.Generic.Dictionary<int, char>
{
  {57922, '堃'},
};

//被我包裝成擴充方法的使用者造字的Unicode字元取代器
public static string ToEudcUnicode(this System.String cTemp)
{
  System.Text.StringBuilder oSB = new System.Text.StringBuilder();
  /* Big5 EUDC to Unicode PUA */
  //FA40-FEFE
  int iPUA_1S = System.Convert.ToInt32(0xE000);
  int iPUA_1E = System.Convert.ToInt32(0xE310);
  //8E40-A0FE
  int iPUA_2S = System.Convert.ToInt32(0xE311);
  int iPUA_2E = System.Convert.ToInt32(0xEEB7);
  //8140–8DFE
  int iPUA_3S = System.Convert.ToInt32(0xEEB8);
  int iPUA_3E = System.Convert.ToInt32(0xF6B0);
  //C6A1–C8FE
  int iPUA_4S = System.Convert.ToInt32(0xF6B1);
  int iPUA_4E = System.Convert.ToInt32(0xF848);
  //F9D6-F9FE
  //在CP950中,微軟真實的實作了字元的對照到Unicode,也就是說所對照出來的數值已經不是循序的0xF849-0xF8FF了。
  //因此下列這段程式碼就算寫了,實務上也是沒鳥用。(使用者造字與繕打這個編碼,存入資料庫的時候就被翻譯對應到其他字元編碼了)
  //int iPUA_5S = System.Convert.ToInt32(0xF849);
  //int iPUA_5E = System.Convert.ToInt32(0xF8FF);

  //尋訪中文難字字元
  foreach (char cUtf16 in cTemp)
  {
    char cResult = cUtf16;
    int iValue = System.Convert.ToInt32(cUtf16);
    //找看看這個字元的NCR是否落在PUA
    if
    (
      (iPUA_1S <= iValue && iValue <= iPUA_1E) ||
      (iPUA_2S <= iValue && iValue <= iPUA_2E) ||
      (iPUA_3S <= iValue && iValue <= iPUA_3E) ||
      (iPUA_4S <= iValue && iValue <= iPUA_4E)
    )
    { //如果有找到就回傳替換字,沒有找到就維持原字碼丟回去
      cResult = oEudcUtfDictionary.Where(x => x.Key == iValue).FirstOrDefault().Value;
      if (cResult == 0) { cResult = cUtf16; }
    }
    //將字元加回去
    oSB.Append(cResult);
  }
  return oSB.ToString();
}

相關參考

Big5 EndUserDefinedCharacters Unicode PrivateUseArea Convert Converter Finder Seracher CodePage950