2012-01-07 10 views
10

Tengo una rutina que debe suministrarse con cadenas normalizadas. Sin embargo, los datos que entran no son necesariamente limpios, y String.Normalize() genera ArgumentException si la cadena contiene puntos de código no válidos.¿Cómo eliminar puntos de código inválidos de una cadena?

Lo que me gustaría hacer es simplemente reemplazar esos puntos de código con un carácter desechable como '?'. Pero para hacer eso necesito una manera eficiente de buscar a través de la cadena para encontrarlos en primer lugar. ¿Cuál es una buena manera de hacer eso?

El siguiente código funciona, pero básicamente está usando try/catch como un enunciado crudo para que el rendimiento sea terrible. Sólo estoy compartiendo para ilustrar el comportamiento Busco:

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

(edición :) Estoy pensando en convertir el texto a UTF-32 para que pudiera recorrer rápidamente sobre él y ver si cada uno dword corresponde a un punto de código válido. ¿Hay alguna función que haga eso? Si no, ¿hay una lista de rangos no válidos flotando por ahí?

+0

Tenga en cuenta que, a causa de los pares suplentes, no será posible mirar simplemente en un 'DWORD' arbitrario y diga si es un punto de código válido. –

+1

UTF-32 no usa pares suplentes. –

+0

¿Cómo está recibiendo esta información incorrecta? Si lo está leyendo con la clase 'Encoding', estos caracteres deberían eliminarse por defecto. – porges

Respuesta

8

Parece que la única manera de hacerlo es 'manualmente' como usted ha hecho. Esta es una versión que ofrece los mismos resultados que la suya, pero es un poco más rápida (aproximadamente 4 veces en una cadena de chars hasta char.MaxValue, menos mejora hasta U+10FFFF) y no requiere el código unsafe. También he simplificado y comenté mi método IsCharacter para explicar cada selección:

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

Acabo de probar esto. La salida es idéntica a la entrada, puntos inválidos y todo. –

+0

Acabo de hacer algunas pruebas más. Parece que la codificación UTF-16 reemplaza los puntos de código quebrados, pero no se ocupa de los "no caracteres". ¡Interesante! – porges

+0

El problema no se trata de sustitutos, sino de puntos de código completos que se definen como sin carácter. U + FFFF, por ejemplo. –

0

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx debe tener la información que está buscando al hacer referencia a la lista de puntos de código válidos/no válidos en C#. En cuanto a cómo hacerlo, me llevaría un poco formular una respuesta correcta. Sin embargo, ese enlace debería ayudarte a comenzar.

+0

No veo la lista de puntos de códigos válidos/no válidos en ninguna parte de esos documentos, ¿podría indicarnos qué hacer? Gracias – Rup

+0

Mire cerca de la parte superior de la página debajo de donde dice "Observaciones" y dice: _ ".NET Framework utiliza la estructura Char para representar un carácter Unicode. El estándar Unicode identifica cada carácter Unicode con un carácter único. un número escalar llamado punto de código y define el formulario de codificación UTF-16 que especifica cómo se codifica un punto de código en una secuencia de uno o más valores de 16 bits. Cada valor de 16 bits varía de hexadecimal 0x0000 a 0xFFFF y se almacena en una estructura Char. El valor de un objeto Char es su valor numérico (ordinal) de 16 bits. "_ – th3n3wguy

+0

Bien, pero el problema aquí es que' String.Normalise' está rechazando los rangos 0xfdd0-ef y 0xfffe-f como inválidos puntos de código Esa es la información que queríamos y no la veo en la página 'System.Char'. – Rup

3

Continué con la solución insinuada en la edición.

No pude encontrar una lista fácil de usar de rangos válidos en el espacio Unicode; incluso la base de datos oficial de caracteres Unicode iba a tomar más análisis de los que realmente quería tratar. Así que en su lugar escribí un guión rápido para recorrer cada número en el rango [0x0, 0x10FFFF], convertirlo a string usando Encoding.UTF32.GetString(BitConverter.GetBytes(code)), y probar .Normalize() buscando el resultado. Si se genera una excepción, ese valor no es un punto de código válido.

A partir de esos resultados, he creado la siguiente función:

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

Tenga en cuenta que esta función no es necesariamente grande para la limpieza de uso general, dependiendo de sus necesidades. No excluye los puntos de código no asignados o reservados, solo los que están específicamente designados como 'noncharacter' (edit: y algunos otros que Normalize() parece ahogarse, como 0xfffff). Sin embargo, estos parecen ser los únicos puntos de código que provocarán que IsNormalized() y Normalize() presenten una excepción, por lo que está bien para mis propósitos.

Después de eso, solo se trata de convertir la cuerda en UTF-32 y peinarla. Desde Encoding.GetBytes() devuelve una matriz de bytes y IsValidCodePoint() espera un UInt32, he utilizado un bloque insegura y algo de colada para reducir la brecha:

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

El rendimiento es bueno, comparativamente hablando - varios órdenes de magnitud más rápido que la muestra publicado en el pregunta. Dejando los datos en UTF-16 presumiblemente habría sido más rápido y más eficiente en la memoria, pero a costa de un montón de código adicional para tratar con los sustitutos. Y, por supuesto, tener replacement ser un char significa que el personaje de reemplazo debe estar en el BMP.

edición: Aquí es una versión mucho más concisa de IsValidCodePoint():

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

Hay un punto de código designado para caracteres desconocidos con el que debe reemplazar, al menos como el carácter de reemplazo predeterminado; U + FFFD. – tripleee

+0

Por lo que vale, no necesita código inseguro; puede usar ['BitConverter.ToUInt32'] (http://msdn.microsoft.com/en-us/library/system.bitconverter.touint32.aspx) para convertir bytes en una matriz a' UInt32's. –

+0

Sí, pero eso crea una copia más de los datos. –

0

me gusta Regex acercarse a los más

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

A lo largo del tiempo transcurrido desde que hice esta pregunta por primera vez, me he alejado completamente del uso de expresiones regulares para este tipo de trabajos de eliminación de caracteres. El uso de expresiones regulares puede ahorrar algunas pulsaciones de teclas, pero en la práctica resulta menos legible, más difícil de depurar y menos rendimiento. –

+0

@SeanU Ese es un punto válido. Proporcioné la solución Regex solo por completo. – mnaoumov

Cuestiones relacionadas