The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

魔鬼般的細節:使用 C# 的 String.Trim() 方法刪除空白字元

昨天同事提到一個網後台內容上稿的問題,客戶反映一個 Bug 說有網站的連結發生問題,點進去之後會發生 A potentially dangerous Request.Path value was detected from the client (:) 的錯誤訊息,查了好一段時間後終於發現在從後台上稿的內容中,該網址的最前面原來有一個 Unicode 的不可見字元 (U+200B),導致網址錯誤,所以瀏覽器當然無法解析該網址。當在思考著如何防呆的時候,卻一時找不到一個漂亮的解決方法,這才發現到一個 String.Trim() 魔鬼般的細節。

我們先來看看滑鼠點下這個錯誤連結後所出現的畫面,如下圖示(點圖可放大):

打開網頁原始碼,也看不出這個超連結的網址到底有什麼異狀(該網站用 AngularJS 製作才有 ng-href

進一步調查後發現,該網址最前方原來出現了一個 Unicode 字元(ZERO WIDTH SPACE)( U+200B )

這是一個所謂的「零寬度空白字元」 ( 英文簡稱 ZWSP ),一個很特殊的空白字元,通常用於排版用途,因為他的零寬度特性,所以不但可以用來間隔文字與文字之間,還能讓排版軟體透過 左右對齊 (justification) 時可以自動調整文字之間的間距。

我們就很好奇,到底客戶是怎樣上稿的,可以輸入到這個特別的文字?我研究了一下發現,在 Thai (泰文)、Khmer (高棉文) 與 Myanmar (緬甸文) 語系中,預設輸入空白時,就是用 U+200B 這個字元做文字間格的(在他們的鍵盤配置裡預設就是這樣)。當然,我們的客戶不太可能輸入這三種語系或用這種鍵盤配置。我想到另一種可能是他們上稿的時候,是從排版軟體複製網址下來的,以至於複製到這個 ZWSP 字元而不自知。
(註:看網路說好像在 Mac 作業系統可透過 Shift+Space 輸入 ZWSP 字元,但我無法確定!)

總之,這字元的由來,已經不可考,現在能想的是怎樣落實防呆措施,下次客戶輸入的時候可以幫他們過濾掉這個不可見字元。

我首先嘗試利用 String.Trim() 清除這個「零寬度空白字元」,結果竟然刪不掉。透過 Regex 也想不出更完美的解決方法,因為會擔心是否有可能出現任何其他的空白字元。

查詢 MSDN 之後,發現在 Remarks (備註) 的段落中,出現一些關於該字元的相關說明,大概的意思是這樣的:

在 .NET Framework 3.5 SP1 與先前的版本,String.Trim() 方法在不傳入參數或傳入空陣列的時候,在內部會維護一份「空白字元」的清單,在移除空白字元時會包含 ZERO WIDTH SPACE (U+200B)ZERO WIDTH NO-BREAK SPACE (U+FEFF) 字元。但從 .NET Framework 4.0 以後版本,變更為判斷 Char.IsWhiteSpace() 方法的回傳結果,如果回傳為 true 才會清除該空白字元,可惜這兩個字元在 Char.IsWhiteSpace() 方法的回傳結果卻為 false,也代表使用 String.Trim() 方法時,不會刪除這兩個字元。

所以,如果我們要刪除這兩個字元,可能會用以下解法:

  • 先刪除 Char.IsWhiteSpace() 方法認得的空白字元,再刪除這兩個字元
    var result = str.Trim().Trim(new char[] { '\uFEFF', '\u200B' });
  • 先刪除這兩個字元,再刪除 Char.IsWhiteSpace() 方法認得的空白字元
    var result = str.Trim(new char[] { '\uFEFF', '\u200B' }).Trim();
  • 僅刪除空白字元與這兩個字元 ( 不含 Unicode 空白字元與 ASCII 控制字元 )
    var result = str.Trim(new char[] { '\uFEFF', '\u200B', ' ' });
  • 最後則是我依據 MSDN 文件所列出的空白字元清單加上本文提到的這兩個空白字元的完整版本
    char[] all_whitespaces = new char[] {
    // SpaceSeparator category
    '\u0020', '\u1680', '\u180E', '\u2000', '\u2001', '\u2002', '\u2003',
    '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A',
    '\u202F', '\u205F', '\u3000',
    // LineSeparator category
    '\u2028',
    // ParagraphSeparator category
    '\u2029',
    // Latin1 characters
    '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u0085', '\u00A0',
    // ZERO WIDTH SPACE (U+200B) & ZERO WIDTH NO-BREAK SPACE (U+FEFF)
    '\u200B', '\uFEFF'
    };
    var result = str.Trim(all_whitespaces);

最後補充一下,我們常在 PowerPoint 中按 Shift+Enter 來輸入強制斷行,這個強制斷行所產生的是一個 VT (vertical tabulation) 字元,這個字元基本上也是個空白字元,透過 String.Trim() 是可以清除掉字串開頭或結尾的 VT 字元。不過,如果你希望客戶從後台上稿時,可能會從 PowerPoint 文件中複製包含斷行符號的內容時,記得要將 VT 字元替換成 CRLF ( \r\n ) 字元,否則在顯示上可能會出問題。

另外,現在除了 IE6 之外,瀏覽器都支援 ZWSP 字元,我們以 Wikipedia 上的 Zero-width space 頁面為例,在該網頁上就用了不可見的 ZWSP 字元,這個字元你從檢視網頁原始碼也看不到,要用 HEX 十六進制的編輯器才可以看的見。

除了直接在文件中輸入 ZWSP 零寬度字元外(你可以透過 Alt+8203 輸入該字元)[參考文章],你也可以透過 HTML Entity 的 &#8203; 設定在網頁中,這只是 ZWSP 零寬度字元的另一種表示法,這兩個都是跨瀏覽器支援的,相容性沒什麼問題。雖然你也可以透過 <wbr> 標籤來設定 ZWSP 零寬度字元,不過即便到了 IE11 也都還不支援 <wbr> 標籤,所以建議各位不要使用這個 <wbr> 標籤當成 ZWSP 零寬度字元。

相關連結