2009-05-10 10 views
5

Moja aplikacja Perl i baza danych MySQL obsługują poprawnie dane przychodzące UTF-8, ale muszę przekonwertować istniejące wcześniej dane. Niektóre dane wydają się być zakodowane jako CP-1252 i nie dekodowane jako takie przed zakodowaniem jako UTF-8 i przechowywane w MySQL. Przeczytałem artykuł O'Reilly Turning MySQL data in latin1 to utf8 utf-8, ale chociaż jest on często przywoływany, nie jest to ostateczne rozwiązanie.Jak przekonwertować przechowywane dane w stanie zmodowanym?

Przyjrzałem się Encode::DoubleEncodedUTF8 i Encoding::FixLatin, ale żadne z nich nie działało na moich danych.

To, co zrobiłem do tej pory:

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

To rozwiązuje większości przypadków, ale jeśli uruchomić przeciwko proplerly kodowanych rekordów, to Zmieniany im. Próbowałem już używać Encode::Guess i Encode::Detect, ale nie potrafią odróżnić odpowiednio zakodowanych i zminiaturyzowanych rekordów. Więc po prostu cofam konwersję, jeśli po konwersji nastąpi \x{FFFD} character.

Niektóre rekordy są jednak tylko częściowo przekształcone. Oto przykład, w którym lewe kręcone cytaty są poprawnie konwertowane, ale prawe kręcone cytaty zostają zniekształcone.

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")))' 

A i oto przykład gdzie prawo apostrof nie konwertować:

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

jestem również do czynienia z podwójnymi danych zakodowanych tutaj? Co jeszcze muszę zrobić, aby przekonwertować te rekordy?

Odpowiedz

6

Przykład "czterech wyników" prawie na pewno jest danymi zakodowanymi podwójnie. Wygląda na to, albo:

    danych
  1. CP1252, który był prowadzony przez CP1252 do procesu utf8 dwukrotnie lub
  2. utf8 danych, który był prowadzony przez CP1252 do procesu utf8

(Naturalnie obu przypadkach wyglądają identycznie)

Teraz tego właśnie się spodziewałeś, więc dlaczego twój kod nie zadziałał?

Po pierwsze, chciałbym odnieść się do this table, która pokazuje konwersję z cp1252 do Unicode. Ważną rzeczą, którą chciałbym zauważyć, jest to, że istnieje kilka bajtów (takich jak 0x9D), które nie są prawidłowe w cp1252.

Kiedy wyobrażam sobie napisanie konwertera cp1252 do utf8, muszę zrobić coś z tymi bajtami, których nie ma w cp1252. Jedyną rozsądną rzeczą, jaką mogę wymyślić jest przekształcenie nieznanych bajtów w znaki Unicode o tej samej wartości. W rzeczywistości wydaje się, że to się stało. Weźmy przykład "cztery punkty" z powrotem o jeden krok na raz.

Po pierwsze, ponieważ jest on ważny UTF-8, niech dekodować z:

$ 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 

Daje to ciąg punktów kodowych Unicode:

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

("FMT" to komenda UNIX że po prostu formatujemy tekst tak, abyśmy mieli ładną linię z długimi danymi)

Teraz, przedstawmy każdy z nich jako bajt w cp1252, ale gdy znaku Unicode nie można przedstawić w cp1252, to ju st zamień go na bajt o tej samej wartości liczbowej. (Zamiast domyślnego, którym jest zastąpienie go znakiem zapytania) Powinniśmy wtedy, jeśli mamy rację co do tego, co stało się z danymi, mieć poprawny strumień bajtów utf8.

$ 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 

Trzeci argument do zakodowania - kiedy jest podrzędny - mówi, co zrobić z nieodkrywalnymi znakami.

Daje:

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

Teraz jest to prawidłowy strumień utf8 bajtów. Nie możesz tego stwierdzić przez inspekcję? Cóż, zapytać Perl zdekodować ten strumień bajtów jako utf8:

$ 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 

Przechodząc „1” jako trzeci argument do dekodowania zapewnia, że ​​nasz kod będzie rechot jeśli strumień bajtów jest nieprawidłowy. Daje to:

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

lub drukowanych:

$ 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” 

Więc myślę, że pełny algorytm powinien być tak:

  1. Grab strumień bajtów z mysql. Przypisz to do $ bytestream.
  2. Podczas $ bytestream jest prawidłowy strumień utf8 bajt:
    1. Przypisanie bieżącej wartości $ bytestream do $ dobry
    2. Jeśli $ bytestream jest all-ASCII (czyli każdy bajt jest mniej niż 0x80), przerwa z tej pętli "while ... valid utf8".
    3. Ustaw $ bytestream na wynik "demangle ($ bytestream)", gdzie demangle podano poniżej. Ta procedura rozwiązuje konwerter cp1252-to-utf8, który, jak sądzimy, ucierpiał z powodu tych danych.
  3. Umieść $ good z powrotem w bazie danych, jeśli nie jest to niepotrzebne. Jeśli $ good nigdy nie zostało przypisane, załóżmy, że $ bytestream było strumieniem bajtów cp1252 i przekonwertowało go na utf8. (Oczywiście, zoptymalizuj i nie rób tego, jeśli pętla w kroku 2 niczego nie zmieniła, itp.)

.

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

ta opiera się na założeniu, że jest to w rzeczywistości bardzo rzadko zdarza się, że ciąg nie jest all-ASCII jest poprawnym UTF-8 bajtów strumienia chyba że naprawdę jest UTF-8. To znaczy, nie jest to coś, co dzieje się przypadkowo.

edytowane ADD:

pamiętać, że ta technika nie pomaga zbytnio swoim przykładzie „Boba”, niestety. Myślę, że ten ciąg również przeszedł dwie rundy konwersji cp1252-to-utf8, ale niestety było również pewne uszkodzenie. Stosując tę ​​samą technikę, jak poprzednio, najpierw odczytać sekwencję bajtów jako utf8 i spojrzeć na sekwencji Unicode referencji znakowych otrzymujemy:

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

Daje:

62 6f 62 ef bf bd 73 

Teraz, po prostu tak się dzieje dla trzech bajtów ef bf bd zgadzają się unicode i cp1252. Zatem reprezentowanie tej sekwencji punktów kodu unicode w cp1252 jest po prostu:

62 6f 62 ef bf bd 73 

To jest ta sama sekwencja cyfr. Teraz, to jest w istocie ważny UTF-8 bajtów strumienia, ale co to dekoduje się może cię zaskoczyć:

$ 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 

Oznacza to, że bajt strumień UTF-8, chociaż uzasadnionego UTF-8 bajtów strumienia, zakodowany znak 0xFFFD, który jest ogólnie używany dla "nieprzetłumaczalnego znaku". Podejrzewam, że to, co się tutaj wydarzyło, to to, że pierwsza transformacja * -to-utf8 zobaczyła postać, której nie rozpoznała i zastąpiła ją "nieprzetłumaczalną". Nie ma sposobu, aby programowo odzyskać oryginalną postać.

Konsekwencją jest to, że nie można wykryć, czy strumień bajtów jest poprawny utf8 (potrzebne dla tego algorytmu, który podałem powyżej) po prostu przez dekodowanie, a następnie szukanie 0xFFFD. Zamiast tego powinieneś użyć czegoś takiego:

sub is_valid_utf8 { 
    defined(eval { decode("utf-8", $_[0], 1) }) 
} 
Powiązane problemy