2012-01-07 61 views
10

我有一個例程,需要提供規範化的字符串。但是,進入的數據不一定是乾淨的,如果字符串包含無效的代碼點,則String.Normalize()會引發ArgumentException。如何從字符串中刪除無效的代碼點?

我想要做的只是用一次性字符替換那些代碼點,比如'?'。但要做到這一點,我需要一種有效的方式來搜索字符串,以便首先找到它們。什麼是這樣做的好方法?

下面的代碼有效,但它基本上使用try/catch作爲粗略的if語句,因此性能很差。我只是分享它來說明我在尋找的行爲:

private static string ReplaceInvalidCodePoints(string aString, string replacement) 
{ 
    var builder = new StringBuilder(aString.Length); 
    var enumerator = StringInfo.GetTextElementEnumerator(aString); 

    while (enumerator.MoveNext()) 
    { 
     string nextElement; 
     try { nextElement = enumerator.GetTextElement().Normalize(); } 
     catch (ArgumentException) { nextElement = replacement; } 
     builder.Append(nextElement); 
    } 

    return builder.ToString(); 
} 

(編輯:)我想轉換文本UTF-32這樣我就可以快速迭代過目一下,看看如果每個雙字對應於有效的代碼點。有沒有一個函數可以做到這一點?如果沒有,那裏是否有無效範圍列表?

+0

需要注意的是,因爲代理對,就不可能簡單地看在一個任意的'DWORD'處,並判斷它是否是一個有效的代碼點。 – 2012-01-09 17:24:12

+1

UTF-32不使用代理對。 – 2012-01-09 17:54:32

+0

你如何收到這些不良數據?如果您正在使用'Encoding'類讀取它們,則默認情況下應刪除這些字符。 – porges 2012-01-11 11:38:52

回答

8

好像只有這樣,才能做到這一點是「手動」像你這樣做。下面是給出了相同的結果你的一個版本,但是是快一點(在所有chars一串約4倍達char.MaxValue,提高相對較少高達U+10FFFF),並且不需要unsafe代碼。我還簡化和評論我IsCharacter方法來解釋每一個選擇:

static string ReplaceNonCharacters(string aString, char replacement) 
{ 
    var sb = new StringBuilder(aString.Length); 
    for (var i = 0; i < aString.Length; i++) 
    { 
     if (char.IsSurrogatePair(aString, i)) 
     { 
      int c = char.ConvertToUtf32(aString, i); 
      i++; 
      if (IsCharacter(c)) 
       sb.Append(char.ConvertFromUtf32(c)); 
      else 
       sb.Append(replacement); 
     } 
     else 
     { 
      char c = aString[i]; 
      if (IsCharacter(c)) 
       sb.Append(c); 
      else 
       sb.Append(replacement); 
     } 
    } 
    return sb.ToString(); 
} 

static bool IsCharacter(int point) 
{ 
    return point < 0xFDD0 || // everything below here is fine 
     point > 0xFDEF && // exclude the 0xFFD0...0xFDEF non-characters 
     (point & 0xfffE) != 0xFFFE; // exclude all other non-characters 
} 
+0

我剛試過這個。輸出與輸入,無效點和全部相同。 – 2012-01-11 20:50:40

+0

只做了一些更多的測試。 UTF-16編碼看起來代替了破損的代碼點,但不涉及「非字符」。有趣! – porges 2012-01-11 21:49:09

+0

問題不是代理人破裂,它是完整的代碼點被定義爲非字符。例如,U + FFFF。 – 2012-01-11 21:56:02

0

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx在引用C#中的有效/無效代碼點列表時,應該有您正在查找的信息。至於如何做到這一點,我會花一點時間來制定正確的迴應。該鏈接應該可以幫助你開始。

+0

我沒有在這些文檔中的任何地方看到有效/無效的代碼點列表 - 您能指出我們嗎?由於 – Rup 2012-01-09 18:01:35

+0

看看附近的頁面下,它說:「備註」上,它指出: _「.NET框架使用的字符結構來表示一個Unicode字符的Unicode標準標識具有獨特21-每個Unicode字符。位標量號稱爲代碼點,並定義了UTF-16編碼形式,該形式指定如何將代碼點編碼爲一個或多個16位值的序列。每個16位值的範圍從十六進制0x0000到0xFFFF,並被存儲在char結構中。Char對象的值是它的16位數字(有序)值。「_ – th3n3wguy 2012-01-22 03:52:06

+0

好的,但問題在於'String.Normalise'拒絕範圍0xfdd0-ef和0xfffe-f作爲無效代碼點。這是我們想要的信息,我沒有在'System.Char'頁面上看到。 – Rup 2012-01-22 11:14:59

3

我繼續在編輯中暗示的解決方案。

我在Unicode空間中找不到有效範圍的易用列表;即使是官方的Unicode字符數據庫也會比我想要處理的更多的解析。因此,我寫了一個快速腳本來遍歷範圍[0x0,0x10FFFF]中的每個數字,然後使用Encoding.UTF32.GetString(BitConverter.GetBytes(code))將其轉換爲string,然後嘗試.Normalize()結果。如果引發異常,那麼該值不是有效的代碼點。

從這些結果,我創建了以下功能:

bool IsValidCodePoint(UInt32 point) 
{ 
    return (point >= 0x0 && point <= 0xfdcf) 
     || (point >= 0xfdf0 && point <= 0xfffd) 
     || (point >= 0x10000 && point <= 0x1fffd) 
     || (point >= 0x20000 && point <= 0x2fffd) 
     || (point >= 0x30000 && point <= 0x3fffd) 
     || (point >= 0x40000 && point <= 0x4fffd) 
     || (point >= 0x50000 && point <= 0x5fffd) 
     || (point >= 0x60000 && point <= 0x6fffd) 
     || (point >= 0x70000 && point <= 0x7fffd) 
     || (point >= 0x80000 && point <= 0x8fffd) 
     || (point >= 0x90000 && point <= 0x9fffd) 
     || (point >= 0xa0000 && point <= 0xafffd) 
     || (point >= 0xb0000 && point <= 0xbfffd) 
     || (point >= 0xc0000 && point <= 0xcfffd) 
     || (point >= 0xd0000 && point <= 0xdfffd) 
     || (point >= 0xe0000 && point <= 0xefffd) 
     || (point >= 0xf0000 && point <= 0xffffd) 
     || (point >= 0x100000 && point <= 0x10fffd); 
} 

注意,這個功能不是通用的清理一定很大,這取決於你的需求。它不排除未指定或保留的代碼點,只是那些被明確指定爲「非字符」的編碼點(編輯:和Normalize()似乎阻塞的其他一些代碼點,例如0xfffff)。但是,這些似乎是導致IsNormalized()Normalize()引發異常的唯一代碼點,所以對我的目的來說很好。

之後,它只是將字符串轉換爲UTF-32並對其進行梳理。由於Encoding.GetBytes()返回一個字節數組和IsValidCodePoint()需要一個UInt32的,我用了一個不安全的塊和一些鑄造縮小差距:

unsafe string ReplaceInvalidCodePoints(string aString, char replacement) 
{ 
    if (char.IsHighSurrogate(replacement) || char.IsLowSurrogate(replacement)) 
     throw new ArgumentException("Replacement cannot be a surrogate", "replacement"); 

    byte[] utf32String = Encoding.UTF32.GetBytes(aString); 

    fixed (byte* d = utf32String) 
    fixed (byte* s = Encoding.UTF32.GetBytes(new[] { replacement })) 
    { 
     var data = (UInt32*)d; 
     var substitute = *(UInt32*)s; 

     for(var p = data; p < data + ((utf32String.Length)/sizeof(UInt32)); p++) 
     { 
      if (!(IsValidCodePoint(*p))) *p = substitute; 
     } 
    } 

    return Encoding.UTF32.GetString(utf32String); 
} 

表現還是不錯的,相比較而言 - 幾個數量級比張貼在樣品更快題。將數據留在UTF-16中可能會更快,更有效率,但是代價很高,代價很大。當然有replacementchar意味着替換字符必須在BMP上。

編輯:這裏是一個更簡潔的版本IsValidCodePoint(的):

private static bool IsValidCodePoint(UInt32 point) 
{ 
    return point < 0xfdd0 
     || (point >= 0xfdf0 
      && ((point & 0xffff) != 0xffff) 
      && ((point & 0xfffe) != 0xfffe) 
      && point <= 0x10ffff 
     ); 
} 
+1

有一個未知字符的指定代碼點,您應該替換爲至少作爲默認替換字符; U + FFFD。 – tripleee 2012-01-10 07:12:12

+0

對於它的價值,你不需要不安全的代碼;您可以使用['BitConverter.ToUInt32'](http://msdn.microsoft.com/en-us/library/system.bitconverter.touint32.aspx)將數組中的字節轉換爲'UInt32's。 – 2012-01-11 23:09:55

+0

是的,但創建了另一個數據副本。 – 2012-01-11 23:20:13

0

我喜歡正則表達式接近最

public static string StripInvalidUnicodeCharacters(string str) 
{ 
    var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])"); 
    return invalidCharactersRegex.Replace(str, ""); 
} 
+0

自從我第一次問這個問題以來,我已經完全擺脫了使用正則表達式來處理這些類型的角色剝離作業。使用正則表達式可以節省一些擊鍵次數,但實際上最終會導致可讀性差,調試困難和性能下降。 – 2014-06-16 19:41:12

+0

@SeanU這是一個有效的觀點。爲了完整性,我提供了Regex解決方案。 – mnaoumov 2014-06-17 00:29:29