2012-01-07 16 views
10

Ho una routine che deve essere fornita con stringhe normalizzate. Tuttavia, i dati che arrivano non sono necessariamente puliti e String.Normalize() genera ArgumentException se la stringa contiene punti di codice non validi.Come rimuovere punti di codice non validi da una stringa?

Quello che mi piacerebbe fare è semplicemente sostituire quei punti di codice con un carattere "usa e getta" come "?". Ma per farlo ho bisogno di un modo efficace per cercare attraverso la stringa per trovarli in primo luogo. Qual è un buon modo per farlo?

Il seguente codice funziona, ma fondamentalmente si utilizza try/catch come una grezza istruzione if, quindi le prestazioni sono terribili. Sto solo la condivisione per illustrare il comportamento che sto cercando:

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(); 
} 

(Edit :) sto pensando convertire il testo in UTF-32 in modo da poter scorrere rapidamente su di esso e vedere se ogni dword corrisponde a un punto di codice valido. C'è una funzione che lo farà? In caso contrario, c'è una lista di intervalli non validi che galleggiano là fuori?

+0

Si noti che, a causa delle coppie di surrogati, non sarà possibile guardare semplicemente a un 'DWORD' arbitrario e indica se si tratta di un punto di codice valido. –

+1

UTF-32 non usa coppie surrogate. –

+0

Come stai ricevendo questi dati errati? Se lo stai leggendo con la classe 'Encoding', questi caratteri dovrebbero essere rimossi di default. – porges

risposta

8

Sembra che l'unico modo per farlo è 'manualmente' come hai fatto. Ecco una versione che offre gli stessi risultati della tua, ma è un po 'più veloce (circa 4 volte su una stringa di tutto chars fino a char.MaxValue, meno miglioramenti fino a U+10FFFF) e non richiede il codice unsafe. Ho anche semplificato e commentato il mio metodo IsCharacter per spiegare ogni selezione:

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

Ho appena provato questo. L'output è identico all'input, i punti non validi e tutti. –

+0

Ho appena fatto altri test. Sembra che la codifica UTF-16 sostituisca i codepoint non funzionanti, ma non tratta i "non caratteri". Interessante! – porges

+0

Il problema non si tratta di surrogati, ma di codepunti completi definiti come non caratteri. U + FFFF, per esempio. –

0

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx dovrebbe avere le informazioni che stai cercando quando si fa riferimento alla lista di punti di codice validi/non validi in C#. Per quanto riguarda come farlo, mi servirebbe un po 'per formulare una risposta corretta. Questo link dovrebbe aiutarti a iniziare.

+0

Non vedo l'elenco di punti codice valido/non valido in nessuna parte di questi documenti: potresti indicarci? Grazie – Rup

+0

Guardare vicino alla parte superiore della pagina sotto dove dice "Osservazioni" e afferma: _ ".NET Framework utilizza la struttura Char per rappresentare un carattere Unicode. Lo standard Unicode identifica ogni carattere Unicode con un unico 21- numero scalare di bit chiamato punto di codice e definisce il modulo di codifica UTF-16 che specifica in che modo un punto di codice viene codificato in una sequenza di uno o più valori a 16 bit. Ogni valore a 16 bit varia da 0x0000 a 0xFFFF esadecimale e viene archiviato in una struttura Char. Il valore di un oggetto Char è il suo valore numerico (ordinale) a 16 bit. "_ – th3n3wguy

+0

OK, ma il problema qui è che' String.Normalise' sta rifiutando gli intervalli 0xfdd0-ef e 0xfffe-f come non validi punti di codice. Questa è l'informazione che volevamo e non la vedo sulla pagina 'System.Char'. – Rup

3

Sono andato avanti con la soluzione suggerita nella modifica.

Impossibile trovare un elenco di intervalli validi valido nello spazio Unicode; anche il database dei caratteri Unicode ufficiale avrebbe richiesto più analisi di quello che volevo veramente affrontare. Così, invece, ho scritto uno script veloce per eseguire il loop su ogni numero nell'intervallo [0x0, 0x10FFFF], convertirlo in un string utilizzando Encoding.UTF32.GetString(BitConverter.GetBytes(code)) e provare il risultato con .Normalize(). Se viene sollevata un'eccezione, quel valore non è un punto di codice valido.

Da questi risultati, ho creato la seguente funzione:

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); 
} 

Si noti che questa funzione non è necessariamente grande per general-purpose di pulizia, a seconda delle esigenze. Non esclude punti codice non assegnati o riservati, solo quelli che sono specificatamente designati come "non caratteri" (modifica: e alcuni altri che Normalize() sembra soffocare, come 0xfffff). Tuttavia, questi sembrano essere gli unici punti di codice che causeranno IsNormalized() e Normalize() per sollevare un'eccezione, quindi va bene per i miei scopi.

Dopodiché, si tratta solo di convertire la stringa in UTF-32 e di sfogliarla. Dal momento che Encoding.GetBytes() restituisce un array di byte e IsValidCodePoint() aspetta un'UInt32, ho usato un blocco non sicuro e un po 'di fusione per colmare il gap:

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); 
} 

Le prestazioni sono buone, relativamente parlando - di diversi ordini di grandezza più veloce rispetto al campione registrato nel domanda. Lasciare i dati in UTF-16 sarebbe presumibilmente più veloce e più efficiente in termini di memoria, ma al costo di un sacco di codice aggiuntivo per gestire i surrogati. E ovviamente avere replacement un char significa che il carattere di sostituzione deve essere sul BMP.

edit: Ecco una versione molto più conciso di IsValidCodePoint():

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

Esiste un punto di codice designato per caratteri sconosciuti che dovresti sostituire con, almeno come carattere di sostituzione predefinito; U + FFFD. – tripleee

+0

Per quello che vale, non è necessario il codice non sicuro; puoi usare ['BitConverter.ToUInt32'] (http://msdn.microsoft.com/en-us/library/system.bitconverter.touint32.aspx) per convertire i byte in una matrice in' UInt32's. –

+0

Sì, ma crea un'altra copia dei dati. –

0

mi piace Regex avvicinarsi il più

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

Nel corso del tempo da quando ho posto questa domanda per la prima volta, mi sono completamente spostato dall'usare espressioni regolari per questi tipi di lavori di rimozione dei caratteri. L'uso della regex può salvare alcune battute, ma in pratica finisce per essere meno leggibile, più difficile da eseguire il debug e meno performante. –

+0

@SeanU Questo è un punto valido. Ho fornito la soluzione Regex solo per completezza. – mnaoumov

Problemi correlati