2009-05-10 3 views
5

Приложение My Perl и база данных MySQL теперь обрабатывают входящие данные UTF-8 должным образом, но мне нужно преобразовать уже существующие данные. Некоторые из данных, по-видимому, были закодированы как CP-1252 и не декодированы как таковые до того, как будут закодированы как UTF-8 и сохранены в MySQL. Я прочитал статью O'Reilly Turning MySQL data in latin1 to utf8 utf-8, но, хотя это часто упоминается, это не окончательное решение.Как преобразовать сохраненные неправильно записанные данные?

Я просмотрел Encode::DoubleEncodedUTF8 и Encoding::FixLatin, но не работал над своими данными.

Это то, что я сделал до сих пор:

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

Это устраняет большинство случаев, но если работать против proplerly закодированных записей, он искалечил их. Я пробовал использовать Encode::Guess и Encode::Detect, но они не могут отличить правильно закодированные и неправильно записанные записи. Поэтому я просто отменю преобразование, если после преобразования найдено \x{FFFD} character.

Некоторые записи, однако, только частично преобразованы. Вот пример, когда левые фигурные кавычки правильно конвертируются, но правильные фигурные кавычки становятся искалеченными.

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

А и вот пример, когда право апостроф не конвертировать:

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

Могу ли я также имеем дело с двойными закодированных данных здесь? Что еще нужно сделать для преобразования этих записей?

ответ

6

С примером «четыре балла» это почти наверняка представляет собой данные с двойным кодированием. Это выглядит как либо:

    случаи
  1. cp1252 данных, которые пропускали через cp1252 к процессу utf8 дважды или
  2. utf8 данных, которые пропускали через cp1252 в процессе utf8

(Естественно, как выглядят одинаково)

Теперь, вот что вы ожидали, так почему же ваш код не работал?

Во-первых, я хотел бы направить вас к this table, который показывает преобразование cp1252 в unicode. Важно отметить, что есть некоторые байты (например, 0x9D), которые недопустимы в cp1252.

Когда я представляю себе конвертер cp1252 в utf8, поэтому мне нужно что-то сделать с теми байтами, которые не находятся в cp1252. Единственная разумная вещь, о которой я могу думать, это преобразовать неизвестные байты в символы Юникода с одинаковым значением. На самом деле, похоже, это произошло. Возьмем ваш пример с четырьмя баллами за один шаг за раз.

Во-первых, так как он действует в 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"); 
    for $c (split(//,$a)) {printf "%x ",ord($c);}' | fmt 

Это дает эту последовательность точек Юникода кода:

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

("FMT" является команда Unix, которая просто переформатируйте текст так, чтобы у нас были хорошие разрывы строк с длинными данными)

Теперь давайте представим каждый из них как байт в cp1252, но когда символ unicode не может быть представлен в cp1252, давайте сделаем ju st замените его байтом, который имеет то же числовое значение. (Вместо значения по умолчанию, которое должно заменить его вопросительным знаком). Тогда мы должны, если мы правильно относимся к тому, что произошло с данными, имеют действительный поток байтов 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 

Этот третий аргумент для кодирования - когда он является вспомогательным - сообщает, что делать с непредставимыми символами.

Это дает:

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

Теперь это действительный поток utf8 байт. Не можете это проверить? Ну, давайте спросим Perl декодировать этот поток байт в 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 

Передача «1», как третий аргумент декодирование гарантирует, что наш код будет каркать, если поток байт является недействительным. Это дает:

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

Или отпечатанные:

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

Так что я думаю, что полный алгоритм должен быть таким:

  1. захватить поток байтов из MySQL. Назначьте это $ bytestream.
  2. Хотя $ потоковый является допустимым потоком utf8 байт:
    1. Назначает текущее значение $ потокового до $ хорошего
    2. Если $ потокового все-ASCII (т.е. каждый байт меньше, чем 0x80), перерыв из этого "while ... valid utf8" loop.
    3. Установите $ bytestream на результат «demangle ($ bytestream)», где ниже показан демардж. Эта процедура отменяет конвертер cp1252-to-utf8, на который, по нашему мнению, пострадали данные.
  3. Положите $ good обратно в базу данных, если это не undef. Если $ good никогда не был назначен, предположим, что $ bytestream был потоком байта cp1252 и преобразовал его в utf8. (Конечно, оптимизируйте и не делайте этого, если цикл на шаге 2 ничего не изменил и т. Д.)

.

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

Это основано на предположении, что это на самом деле очень редко для строки, не все-ASCII, чтобы быть допустимым UTF-8 байт потока, если он на самом деле не является UTF-8. То есть, это не то, что происходит случайно.

отредактированы ADD:

Обратите внимание, что этот метод не поможет слишком много с вашим примером «Боб», к сожалению. Я думаю, что эта строка также прошла через два раунда конверсии cp1252-to-utf8, но, к сожалению, также была некоторая коррупция. Используя ту же технику, как и раньше, мы сначала прочитать последовательность байт, как utf8 и посмотреть на последовательность Юникода символьных ссылок мы получаем:

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

Это дает:

62 6f 62 ef bf bd 73 

Теперь, просто так бывает что для трех байтов ef bf bd согласны unicode и cp1252. Таким образом, представляя эту последовательность точек Юникода кода в cp1252 просто:

62 6f 62 ef bf bd 73 

То есть, та же последовательность чисел. Теперь, это на самом деле является допустимым UTF-8 байт потока, но то, что он декодирует для Вас может удивить:

$ 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 

То есть, поток байтов UTF-8, хотя легитимной UTF-8 потока байт, кодированный символ 0xFFFD, который обычно используется для «непереводимого символа». Я подозреваю, что здесь произошло то, что первое преобразование * -to-utf8 показало характер, который он не распознал, и заменил его на «нетранслируемый». Невозможно затем программно восстановить оригинальный символ.

В результате вы не можете определить, действительно ли поток байтов utf8 (необходимый для этого алгоритма, который я дал выше) просто выполнил декодирование, а затем искал 0xFFFD. Вместо этого вы должны использовать примерно следующее:

sub is_valid_utf8 { 
    defined(eval { decode("utf-8", $_[0], 1) }) 
} 
Смежные вопросы