Big5 問號之謎:追查「?」亂碼的源頭

前言

最近同事問了一個問題:「為什麼畫面上看起來都是正常的,但是產出來的文字檔,空格的地方就是變成了問號,而且連續兩個空格,只有一個變成問號,另外一個是正常的」,客戶習慣使用UltraEdit開啟這些測試檔案,檢視內容是否正確的時候,向我們提出了這一個bug單,我們實際拿了notepad++看是正常的,狀況陷入了一團迷霧。

做了什麼確認?

使用UltraEdit開啟檔案

確實如使用者描述,連續空格處,只有一個空格變成了問號

使用NotePad++開啟檔案

直接開啟是正常的,但是嘗試將空格轉為其它顯示方式(Hex?手上只剩下Mac,請容我偷懶不去回憶這部分~XD)的時候,確實發現到兩格空格會轉為不同的內容。

從資料來源複製貼上到UltraEdit

使用資料庫的查詢軟體查詢後,複製貼上到UltraEdit,再轉為Hex(16進位)顯示
發現到分別是 20C2 A0

撰寫測試

實際上查到這就已經破案了,因為使用者要求的串接系統只能接受big5編碼的檔案,因此在系統查詢完資料之後,將透過串接另一個系統的API呼叫取得的資料,轉為big5後產出。但是取得的文字內容是utf-8的編碼,由於big5並沒有不換行空格的編碼,所以無法識別,轉換的過程就變成一個問號了。

那再來就是寫一小段程式來確認是不是這樣 (可使用 jupyter 來執行看看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#i "nuget:https://api.nuget.org/v3/index.json"
#r "nuget:System.Text.Encoding.CodePages"

using System.Text;
using System.Text.RegularExpressions;

// dotnet core版本需另外安裝編碼,不過發生問題的是古蹟dotnet framework
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding big5 = Encoding.GetEncoding("Big5");
Encoding utf8 = Encoding.UTF8;
// 聽說是不換行的空白 C2 A0
byte[] nonBreakingChar = new byte[]{ 0xc2, 0xa0};
var utf8SpaceString = utf8.GetString(nonBreakingChar);
var big5SpaceString = big5.GetString(nonBreakingChar);
//expect: 這是UTF8的空白: " " 。
Console.WriteLine($"這是UTF8的空白: \"{utf8SpaceString}\" 。");
//expect: 這是BIG5的空白: "?" 。
Console.WriteLine($"這是BIG5的空白: \"{big5SpaceString}\" 。");

var testString = $"Hello{utf8SpaceString}, my name is Mat.";
var replaceString = "#ReplaceString#";
var replacedString = testString.Replace(" ",replaceString);
//expect: Replace測試(not working): "Hello ,#ReplaceString#my#ReplaceString#name#ReplaceString#is#ReplaceString#mat."
Console.WriteLine($"Replace測試(not working): \"{replacedString}\"");
//expect: Regex的Replace測試: Hello#ReplaceString#,#ReplaceString#my#ReplaceString#name#ReplaceString#is#ReplaceString#mat.
Console.WriteLine($"Regex的Replace測試: {Regex.Replace(testString,@"\s",replaceString)}");

上面程式假設字串來自於某個第三方服務的API,大多數的API回應都是使用UTF-8編碼,所以寫了一個包含utf-8不換行空格的字串 Hello ,my name is Mat.,實際上也是這次碰到的情況,連續兩個空格,但是一個是不換行空格,一個不是。

做一般想像中的replace的時候,可以發現到直接輸入空格替換是沒用的。後面又加上可能可以做的做法是使用正則表達式( \s )來尋找空白並取代為正常的空白(程式碼中使用變數 replaceString 取代,表示可取代成功),如果不想用正則,也可以使用 Char.IsWhiteSpace({inputChar}) 來一個一個Char檢查。

結論

其實不難想像到,這個問號的來源是來自於資料源系統在存入資料庫的時候,使用了textarea元素來送出資料到後端,後端程式接收到之後就直接存入資料庫中了,雖然外表上看起來都是空格,但還真的沒想過在前端這樣連續輸入兩個空格的時候,會變成一個不換行空格、一個正常空格。在更後端處理資料產出的系統在使用的時候,自然就會無條件相信來源系統資料,直接產出在大部分情況下都是沒什麼問題的,偏偏就是有幾個問號,請來源系統查,就是很正常的空白沒問題,但是來源系統使用的是網頁介面,編碼也都是直接使用UTF8,所以讓人充滿了困惑。

好在這個問題肯定是一堆人都碰過(尤其是中文圈,一堆死不更新,不用萬國編碼的系統),找出問題並沒有耗費太多的時間就找到資料。反而是想在C#中,輸出那一個不換行空白想了一下該怎麼做才能寫出來。

參考

  1. 不換行空格