2009-08-03 17 views
13

Un proyecto reciente solicitó la importación de datos en una base de datos Oracle. El programa que hará esto es una aplicación C# .Net 3.5 y estoy usando la biblioteca de conexión Oracle.DataAccess para manejar la inserción real.La mejor forma de acortar cadena UTF8 en función de la longitud del byte

me encontré con un problema en el que me recibe este mensaje de error al insertar un campo en particular:

ORA-12899 Valor demasiado grande para la columna X

que utilizan Field.Substring(0, MaxLength); pero todavía tiene el error (aunque no para cada registro).

Finalmente vi lo que debería haber sido obvio, mi cadena estaba en ANSI y el campo era UTF8. Su longitud se define en bytes, no en caracteres.

Esto me lleva a mi pregunta. ¿Cuál es la mejor manera de recortar mi cadena para arreglar MaxLength?

Mi código de subcadena funciona por longitud de carácter. ¿Hay una función simple de C# que pueda recortar inteligentemente una secuencia UT8 por longitud de bytes (es decir, no cortar medio carácter)?

+1

P.S. Incluí la introducción en caso de que alguien busque en Google mi mensaje de error de oráculo en el futuro. Con suerte, esto les ahorrará algo de tiempo. –

Respuesta

13

Aquí hay dos soluciones posibles: un LINQ one-liner procesando la entrada de izquierda a derecha y un tradicional for -loop procesando la entrada de derecha a izquierda. La dirección de procesamiento más rápida depende de la longitud de la cadena, la longitud permitida de los bytes y el número y la distribución de caracteres multibyte, y es difícil dar una sugerencia general. La decisión entre LINQ y el código tradicional es probablemente una cuestión de gusto (o tal vez de velocidad).

Si la velocidad es importante, se podría pensar en simplemente acumular la longitud de bytes de cada carácter hasta alcanzar la longitud máxima en lugar de calcular la longitud de bytes de toda la cadena en cada iteración. Pero no estoy seguro de si esto funcionará porque no conozco bien la codificación UTF-8. Podría imaginar teóricamente que la longitud del byte de una cadena no es igual a la suma de las longitudes de bytes de todos los caracteres.

public static String LimitByteLength(String input, Int32 maxLength) 
{ 
    return new String(input 
     .TakeWhile((c, i) => 
      Encoding.UTF8.GetByteCount(input.Substring(0, i + 1)) <= maxLength) 
     .ToArray()); 
} 

public static String LimitByteLength2(String input, Int32 maxLength) 
{ 
    for (Int32 i = input.Length - 1; i >= 0; i--) 
    { 
     if (Encoding.UTF8.GetByteCount(input.Substring(0, i + 1)) <= maxLength) 
     { 
      return input.Substring(0, i + 1); 
     } 
    } 

    return String.Empty; 
} 
+0

Me encanta el ejemplo de LINQ. ¡Esa es una solución elegante! –

+0

+1 como ambos solución – Feryt

4

Si un byte UTF-8 tiene un bit de orden alto de valor cero, es el comienzo de un carácter. Si su bit de orden superior es 1, está en el "medio" de un personaje. La capacidad de detectar el comienzo de un personaje fue un objetivo de diseño explícito de UTF-8.

Consulte la sección Descripción de wikipedia article para obtener más información.

+0

Gracias por el consejo. ¿Puedes mostrarme un ejemplo de C#? ¿Esto significa que no hay ninguna funcionalidad incorporada para manejar esta necesidad? Parece un problema común. –

+0

Si tiene una cadena C#, puede usar Encoding.UTF8.GetByteCount (string) para obtener el recuento preciso de bytes. Puede recortar caracteres desde el final de la cadena, si es necesario, hasta que el recuento de bytes se ajuste al límite. –

+0

No * del todo * a la derecha. Si es un byte único, comienza con '0', pero si su bit alto es' 1', podría ser el byte principal o el "medio" (digamos "siguiente") de un carácter de múltiples bytes.El byte inicial comienza con '11' y los siguientes bytes en el carácter de múltiples bytes comienzan con' 10'. Entonces, si su bit de entrada es '1', está en un carácter de múltiples bytes, pero ** no necesariamente es el" centro "**. De la 'pedia: * El byte inicial tiene dos o más 1s de orden superior seguidos de un 0, mientras que los bytes de continuación tienen todos' 10 'en la posición de orden superior. * – ruffin

2

¿Hay algún motivo por el que necesite declarar la columna de la base de datos en términos de bytes? Ese es el valor predeterminado, pero no es un valor predeterminado particularmente útil si el conjunto de caracteres de la base de datos es de ancho variable. Preferiría declarar la columna en términos de caracteres.

CREATE TABLE length_example (
    col1 VARCHAR2(10 BYTE), 
    col2 VARCHAR2(10 CHAR) 
); 

Esto creará una tabla donde COL1 almacenará 10 bytes de datos y col2 almacenará 10 caracteres de datos. La semántica de longitud de caracteres tiene mucho más sentido en una base de datos UTF8.

Suponiendo que desea que todas las tablas que crea utilicen la semántica de longitud de caracteres de forma predeterminada, puede establecer el parámetro de inicialización NLS_LENGTH_SEMANTICS en CHAR. En ese punto, las tablas que cree utilizarán de forma predeterminada la semántica de longitud de caracteres en lugar de la semántica de longitud de bytes si no especifica CHAR o BYTE en la longitud del campo.

+0

Gran idea. No controlo Oracle DB, pero lo sugeriré. Tal vez fue solo un descuido. Esta sería la mejor solución a mi problema si están dispuestos a hacer el cambio. –

12

Creo que podemos hacerlo mejor que contando ingenuamente la longitud total de una cadena con cada adición.LINQ es genial, pero puede alentar accidentalmente el código ineficiente. ¿Qué pasa si quiero los primeros 80,000 bytes de una cadena UTF gigante? Eso es un lote de conteo innecesario. "Tengo 1 byte. Ahora tengo 2. Ahora tengo 13 ... Ahora tengo 52,384 ..."

Eso es una tontería. La mayoría de las veces, al menos en l'anglais, podemos cortar exactamente en ese nth byte. Incluso en otro idioma, estamos a menos de 6 bytes de un buen punto de corte.

Así que voy a comenzar con la sugerencia de @ Oren, que es quitar la punta de un bit de UTF8. Comencemos cortando a la derecha en el byte n+1th, y usemos el truco de Oren para averiguar si necesitamos cortar algunos bytes antes.

tres posibilidades

Si el primer byte después del corte tiene una 0 en el bit inicial, sé que estoy cortando con precisión antes de un solo byte (ASCII convencional) carácter, y puede cortar limpiamente.

Si tengo un 11 tras el corte, el siguiente byte después del corte es el inicio de un carácter multi-byte, por lo que es un buen lugar para cortar también!

Si tengo un 10, sin embargo, sé que estoy en medio de un carácter de varios bytes, y necesito volver a comprobar para ver dónde comienza realmente.

Es decir, aunque quiero cortar la cadena después del n-ésimo byte, si ese byte n + 1 viene en medio de un carácter de múltiples bytes, cortar crearía un valor UTF8 no válido. Necesito hacer una copia de seguridad hasta que llegue a uno que comience con 11 y corte justo antes.

Código

Notas: estoy usando cosas como Convert.ToByte("11000000", 2) para que sea más fácil decir lo que estoy bits de enmascaramiento (un poco más sobre el enmascaramiento poco here). En pocas palabras, estoy & para devolver lo que está en los primeros dos bits del byte y devolver 0 s para el resto. Luego verifico el XX desde XX000000 para ver si es 10 o 11, según corresponda.

me enteré hoy que C# 6.0 might actually support binary representations, que es fresca, pero vamos a seguir utilizando este kludge por ahora para ilustrar lo que está pasando.

El PadLeft es solo porque soy demasiado OCD sobre la salida a la consola.

Así que aquí hay una función que lo reducirá a una cadena que es n bytes de longitud o la mayor cantidad menor que n que termina con un carácter "completo" UTF8.

public static string CutToUTF8Length(string str, int byteLength) 
{ 
    byte[] byteArray = Encoding.UTF8.GetBytes(str); 
    string returnValue = string.Empty; 

    if (byteArray.Length > byteLength) 
    { 
     int bytePointer = byteLength; 

     // Check high bit to see if we're [potentially] in the middle of a multi-byte char 
     if (bytePointer >= 0 
      && (byteArray[bytePointer] & Convert.ToByte("10000000", 2)) > 0) 
     { 
      // If so, keep walking back until we have a byte starting with `11`, 
      // which means the first byte of a multi-byte UTF8 character. 
      while (bytePointer >= 0 
       && Convert.ToByte("11000000", 2) != (byteArray[bytePointer] & Convert.ToByte("11000000", 2))) 
      { 
       bytePointer--; 
      } 
     } 

     // See if we had 1s in the high bit all the way back. If so, we're toast. Return empty string. 
     if (0 != bytePointer) 
     { 
      returnValue = Encoding.UTF8.GetString(byteArray, 0, bytePointer); // hat tip to @NealEhardt! Well played. ;^) 
     } 
    } 
    else 
    { 
     returnValue = str; 
    } 

    return returnValue; 
} 

Inicialmente escribí esto como una extensión de cadena. Simplemente vuelva a agregar this antes de string str para volver a ponerlo en el formato de extensión, por supuesto. Eliminé el this para que pudiéramos cambiar el método en Program.cs en una aplicación de consola simple para demostrarlo.

Prueba y salida esperada

He aquí un buen caso de prueba, con la salida se crean a continuación, escrito esperando a ser el método de Main en Program.cs una aplicación de consola sencilla.

static void Main(string[] args) 
{ 
    string testValue = "12345“”67890”"; 

    for (int i = 0; i < 15; i++) 
    { 
     string cutValue = Program.CutToUTF8Length(testValue, i); 
     Console.WriteLine(i.ToString().PadLeft(2) + 
      ": " + Encoding.UTF8.GetByteCount(cutValue).ToString().PadLeft(2) + 
      ":: " + cutValue); 
    } 

    Console.WriteLine(); 
    Console.WriteLine(); 

    foreach (byte b in Encoding.UTF8.GetBytes(testValue)) 
    { 
     Console.WriteLine(b.ToString().PadLeft(3) + " " + (char)b); 
    } 

    Console.WriteLine("Return to end."); 
    Console.ReadLine(); 
} 

Salida a continuación. Tenga en cuenta que las "comillas inteligentes" en testValue tienen tres bytes de longitud en UTF8 (aunque cuando escribimos los caracteres en la consola en ASCII, se emiten comillas). También tenga en cuenta la salida de ? para el segundo y tercer byte de cada cita inteligente en la salida.

Los primeros cinco caracteres de nuestro testValue son bytes individuales en UTF8, por lo que los valores de 0-5 bytes deben tener entre 0 y 5 caracteres. Luego tenemos una cita inteligente de tres bytes, que no se puede incluir en su totalidad hasta 5 + 3 bytes. Efectivamente, vemos que el pop a cabo en la convocatoria de 8 .Our siguiente cita inteligente se sale a las 8 + 3 = 11, y luego estamos de vuelta a través de caracteres de un solo byte 14.

0: 0:: 
1: 1:: 1 
2: 2:: 12 
3: 3:: 123 
4: 4:: 1234 
5: 5:: 12345 
6: 5:: 12345 
7: 5:: 12345 
8: 8:: 12345" 
9: 8:: 12345" 
10: 8:: 12345" 
11: 11:: 12345"" 
12: 12:: 12345""6 
13: 13:: 12345""67 
14: 14:: 12345""678 


49 1 
50 2 
51 3 
52 4 
53 5 
226 â 
128 ? 
156 ? 
226 â 
128 ? 
157 ? 
54 6 
55 7 
56 8 
57 9 
48 0 
226 â 
128 ? 
157 ? 
Return to end. 

Así que es una especie de diversión, y estoy justo antes del quinto aniversario de la pregunta. Aunque la descripción de Oren de los bits tuvo un pequeño error, eso es exactamente el truco que desea usar. Gracias por la pregunta; ordenado.

+2

Increíble, ¡lo hiciste en O (N)! Gracias, esto es genial para cadenas largas. –

+0

Puede evitar la copia del byte final [] con 'Encoding.UTF8.GetString (byteArray, 0, bytePointer)'. –

+0

@NealEhardt Ah, bien hecho. Buena sugerencia; ¡Gracias! – ruffin

-1
public static string LimitByteLength3(string input, Int32 maxLenth) 
    { 
     string result = input; 

     int byteCount = Encoding.UTF8.GetByteCount(input); 
     if (byteCount > maxLenth) 
     { 
      var byteArray = Encoding.UTF8.GetBytes(input); 
      result = Encoding.UTF8.GetString(byteArray, 0, maxLenth); 
     } 

     return result; 
    } 
+0

esto puede corromper el último carácter y, de hecho, no funciona, ya que la matriz de byte recodificada superará nuevamente el límite – firda

1

Siguiendo Oren Trutner's comment aquí hay dos soluciones más al problema:
aquí contamos el número de bytes de eliminar de la final de la cadena de acuerdo a cada personaje al final de la cadena, por lo que don' t evaluar toda la cadena en cada iteración.

string str = "朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣" 
int maxBytesLength = 30; 
var bytesArr = Encoding.UTF8.GetBytes(str); 
int bytesToRemove = 0; 
int lastIndexInString = str.Length -1; 
while(bytesArr.Length - bytesToRemove > maxBytesLength) 
{ 
    bytesToRemove += Encoding.UTF8.GetByteCount(new char[] {str[lastIndexInString]}); 
    --lastIndexInString; 
} 
string trimmedString = Encoding.UTF8.GetString(bytesArr,0,bytesArr.Length - bytesToRemove); 
//Encoding.UTF8.GetByteCount(trimmedString);//get the actual length, will be <= 朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣潬昣昸昸慢正 

Y una solución aún más eficiente (y mantenible): obtener la cadena de la matriz de bytes de acuerdo a la longitud deseada y cortar el último carácter, ya que podría estar dañado

string str = "朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣" 
int maxBytesLength = 30;  
string trimmedWithDirtyLastChar = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str),0,maxBytesLength); 
string trimmedString = trimmedWithDirtyLastChar.Substring(0,trimmedWithDirtyLastChar.Length - 1); 

El único inconveniente con la segunda solución es que podríamos cortar un último personaje perfectamente fino, pero ya estamos cortando la cuerda, por lo que podría encajar con los requisitos.
Gracias a Shhade que pensaban en la segunda solución

1

Esta es otra solución basada en la búsqueda binaria:

public string LimitToUTF8ByteLength(string text, int size) 
{ 
    if (size <= 0) 
    { 
     return string.Empty; 
    } 

    int maxLength = text.Length; 
    int minLength = 0; 
    int length = maxLength; 

    while (maxLength >= minLength) 
    { 
     length = (maxLength + minLength)/2; 
     int byteLength = Encoding.UTF8.GetByteCount(text.Substring(0, length)); 

     if (byteLength > size) 
     { 
      maxLength = length - 1; 
     } 
     else if (byteLength < size) 
     { 
      minLength = length + 1; 
     } 
     else 
     { 
      return text.Substring(0, length); 
     } 
    } 

    // Round down the result 
    string result = text.Substring(0, length); 
    if (size >= Encoding.UTF8.GetByteCount(result)) 
    { 
     return result; 
    } 
    else 
    { 
     return text.Substring(0, length - 1); 
    } 
} 
1

Shorter versión de ruffin's answer. Aprovecha the design of UTF8:

public static string LimitUtf8ByteCount(this string s, int n) 
    { 
     // quick test (we probably won't be trimming most of the time) 
     if (Encoding.UTF8.GetByteCount(s) <= n) 
      return s; 
     // get the bytes 
     var a = Encoding.UTF8.GetBytes(s); 
     // if we are in the middle of a character (highest two bits are 10) 
     if (n > 0 && (a[n]&0xC0) == 0x80) 
     { 
      // remove all bytes whose two highest bits are 10 
      // and one more (start of multi-byte sequence - highest bits should be 11) 
      while (--n > 0 && (a[n]&0xC0) == 0x80) 
       ; 
     } 
     // convert back to string (with the limit adjusted) 
     return Encoding.UTF8.GetString(a, 0, n); 
    } 
Cuestiones relacionadas