2009-05-10 17 views
5

Mi aplicación Perl y la base de datos MySQL ahora manejan los datos entrantes UTF-8 correctamente, pero tengo que convertir los datos preexistentes. Algunos de los datos parecen haber sido codificados como CP-1252 y no decodificados como tales antes de ser codificados como UTF-8 y almacenados en MySQL. He leído el artículo de O'Reilly Turning MySQL data in latin1 to utf8 utf-8, pero aunque se lo menciona con frecuencia, no es una solución definitiva.¿Cómo convierto datos almacenados mal codificados?

Miré Encode::DoubleEncodedUTF8 y Encoding::FixLatin, pero ninguno de mis datos funcionó.

Esto es lo que he hecho hasta ahora:

#Return the $bytes from the DB using BINARY() 
my $characters = decode('utf-8', $bytes); 
my $good = decode('utf-8', encode('cp-1252', $characters)); 

que fija la mayoría de los casos, pero si se ejecuta con los registros codificados proplerly-, les destroza. Intenté usar Encode::Guess y Encode::Detect, pero no pueden distinguir entre los registros codificados correctamente y los mal codificados. Así que solo deshago la conversión si se encuentra el \x{FFFD} character después de la conversión.

Algunos registros, sin embargo, solo se convierten parcialmente. Aquí hay un ejemplo donde las comillas rizadas a la izquierda se convierten correctamente, pero las comillas rectas correctas se arruinan.

perl -CO -MEncode -e 'print decode("utf-8", encode("cp-1252", decode("utf-8", "\xC3\xA2\xE2\x82\xAC\xC5\x93four score\xC3\xA2\xE2\x82\xAC\xC2\x9D")))' 

Y y aquí está un ejemplo donde una comilla simple derecho no se convertía:

perl -CO -MEncode -e 'print decode("utf-8", encode("cp-1252", decode("utf-8", "bob\xC3\xAF\xC2\xBF\xC2\xBDs")))' 

así yo soy Manejo de los datos codificados dobles aquí? ¿Qué más debo hacer para convertir estos registros?

Respuesta

6

Con el ejemplo de "cuatro puntajes", es casi seguro que se trata de datos doblemente codificados. Parece que o bien:

    de datos
  1. CP1252 que se ejecuta a través de una CP1252 a proceso utf8 dos veces, o
  2. de datos UTF8 que se ejecuta a través de una CP1252 a proceso utf8

(Naturalmente, ambos casos parece idéntico)

Ahora, eso es lo que esperaba, ¿por qué no funcionó su código?

En primer lugar, me gustaría referirlo a this table que muestra la conversión de cp1252 a unicode. Lo importante que quiero que note es que hay algunos bytes (como 0x9D) que no son válidos en cp1252.

Cuando me imagino escribiendo un conversor cp1252 a utf8, por lo tanto, necesito hacer algo con esos bytes que no están en cp1252. Lo único sensato que puedo pensar es transformar los bytes desconocidos en caracteres Unicode con el mismo valor. De hecho, esto parece ser lo que sucedió. Hagamos retroceder su ejemplo de "cuatro puntajes" un paso a la vez.

En primer lugar, ya que es válida UTF-8, vamos a decodificar con:

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "\xC3\xA2\xE2\x82\xAC\xC5\x93" . 
    "four score" . 
    "\xC3\xA2\xE2\x82\xAC\xC2\x9D"); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

Esto produce esta secuencia de puntos de código Unicode:

e2 20ac 153 66 6f 75 72 20 73 63 6f 72 65 e2 20ac 9d 

("fmt" es un comando UNIX que simplemente reformatea el texto para que tengamos buenos saltos de línea con datos largos)

Ahora, vamos a representar cada uno de estos como un byte en cp1252, pero cuando el carácter Unicode no se puede representar en cp1252, vamos a ju st reemplazarlo con un byte que tenga el mismo valor numérico. (En lugar del valor predeterminado, que es para reemplazarlo con un signo de interrogación) Deberíamos entonces, si estamos en lo correcto sobre lo que sucedió con los datos, tener un flujo de bytes utf8 válido.

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "\xC3\xA2\xE2\x82\xAC\xC5\x93" . 
    "four score" . 
    "\xC3\xA2\xE2\x82\xAC\xC2\x9D"); 
    $a=encode("cp-1252", $a, sub { chr($_[0]) }); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

Ese tercer argumento para codificar - cuando es un sub - dice qué hacer con los caracteres irrepresentables.

Esto produce:

e2 80 9c 66 6f 75 72 20 73 63 6f 72 65 e2 80 9d 

Ahora, se trata de un flujo de bytes UTF-8 válidos. No se puede decir eso por inspección? Bueno, vamos a pedir Perl para decodificar este flujo de bytes como UTF-8:

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "\xC3\xA2\xE2\x82\xAC\xC5\x93" . 
    "four score" . 
    "\xC3\xA2\xE2\x82\xAC\xC2\x9D"); 
    $a=encode("cp-1252", $a, sub { chr($_[0]) }); 
    $a=decode("utf-8", $a, 1); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

Pasando "1" como el tercer argumento para decodificar asegura que nuestro código se croar si el flujo de bytes no es válido. Esto produce:

201c 66 6f 75 72 20 73 63 6f 72 65 201d 

o impresas:

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "\xC3\xA2\xE2\x82\xAC\xC5\x93" . 
    "four score" . 
    "\xC3\xA2\xE2\x82\xAC\xC2\x9D"); 
    $a=encode("cp-1252", $a, sub { chr($_[0]) }); 
    $a=decode("utf-8", $a, 1); 
    print "$a\n"' 
“four score” 

por lo que creo que el algoritmo completo debe ser la siguiente:

  1. Coge la secuencia de bytes de MySQL. Asignar esto a $ bytestream.
  2. Mientras $ bytestream es un flujo de bytes UTF-8 válidos:
    1. Asignar el valor actual de $ bytestream a $ buena
    2. Si $ bytestream es todo-ASCII (es decir, cada byte es inferior a 0x80), ruptura fuera de este "while ... valid utf8" loop.
    3. Establezca $ bytestream en el resultado de "demangle ($ bytestream)", donde demandgle aparece a continuación. Esta rutina deshace el convertidor cp1252-to-utf8 del que creemos que ha sufrido esta información.
  3. Ponga $ good nuevamente en la base de datos si no es undef. Si $ good nunca se asignó, suponga que $ bytestream era una secuencia de bytes cp1252 y la convierte a utf8. (Por supuesto, optimice y no haga esto si el ciclo en el paso 2 no cambió nada, etc.)

.

sub demangle { 
    my($a) = shift; 
    eval { # the non-string form of eval just traps exceptions 
     # so that we return undef on exception 
    local $SIG{__WARN__} = sub {}; # No warning messages 
    $a = decode("utf-8", $a, 1); 
    encode("cp-1252", $a, sub {$_[0] <= 255 or die $_[0]; chr($_[0])}); 
    } 
} 

Esto se basa en la suposición de que en realidad es muy raro que una cadena que no está totalmente ASCII para ser un flujo de bytes UTF-8 válido a menos que realmente es UTF-8. Es decir, no es el tipo de cosa que sucede accidentalmente.

editar para agregar:

Tenga en cuenta que esta técnica no ayuda demasiado con tu ejemplo "de Bob", por desgracia. Creo que esa cadena también pasó por dos rondas de conversión cp1252-a-utf8, pero desafortunadamente también hubo algo de corrupción. Utilizando la misma técnica que antes, leemos por primera vez la secuencia de bytes como UTF-8 y observamos la secuencia de referencias de caracteres Unicode se obtiene:

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "bob\xC3\xAF\xC2\xBF\xC2\xBDs"); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

Esto produce:

62 6f 62 ef bf bd 73 

Ahora, da la casualidad que para los tres bytes ef bf bd, unicode y cp1252 están de acuerdo. Por lo que representa esta secuencia de puntos de código Unicode en CP1252 es simplemente:

62 6f 62 ef bf bd 73 

Es decir, la misma secuencia de números. Ahora bien, esto es, de hecho, un flujo de bytes UTF-8 válidos, pero lo que decodifica a que puede sorprender:

$ perl -CO -MEncode -e '$a=decode("utf-8", 
    "bob\xC3\xAF\xC2\xBF\xC2\xBDs"); 
    $a=encode("cp-1252", $a, sub { chr(shift) }); 
    $a=decode("utf-8", $a, 1); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

62 6f 62 fffd 73 

Es decir, el flujo de bytes UTF-8, a través de un flujo de bytes legítima UTF-8, codificada el carácter 0xFFFD, que generalmente se usa para "carácter intraducible". Sospecho que lo que sucedió aquí es que la primera transformación * -to-utf8 vio un personaje que no reconoció y lo reemplazó por "intraducible". No hay forma de recuperar el carácter original mediante programación.

Una consecuencia es que no se puede detectar si una secuencia de bytes es válida utf8 (necesaria para ese algoritmo que proporcioné anteriormente) simplemente haciendo una decodificación y luego buscando 0xFFFD. En su lugar, debe usar algo como esto:

sub is_valid_utf8 { 
    defined(eval { decode("utf-8", $_[0], 1) }) 
} 
Cuestiones relacionadas