Как преобразовать сохраненные неверно закодированные данные? - PullRequest
5 голосов
/ 10 мая 2009

Мое приложение Perl и база данных MySQL теперь корректно обрабатывают входящие данные UTF-8, но мне нужно преобразовать уже существующие данные. Некоторые из данных, по-видимому, были закодированы как CP-1252 и не декодированы как таковые до того, как были закодированы как UTF-8 и сохранены в MySQL. Я прочитал статью О'Рейли Превращение данных MySQL в латинице 1 в 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));

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

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

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

Имею ли я здесь дело с данными с двойным кодированием? Что еще я должен сделать, чтобы преобразовать эти записи?

1 Ответ

6 голосов
/ 15 мая 2009

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

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

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

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

Во-первых, я хотел бы отослать вас к этой таблице , которая показывает преобразование из 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

Это дает следующую последовательность кодовых точек Unicode:

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

("fmt" - это команда unix, которая просто переформатирует текст, чтобы у нас были хорошие разрывы строк с длинными данными)

Теперь давайте представим каждый из них как байт в cp1252, но когда символ unicode не может быть представлен в cp1252, давайте просто заменим его байтом с таким же числовым значением. (Вместо значения по умолчанию, которое заменяет его знаком вопроса). Затем мы должны, если мы правильно поняли, что произошло с данными, иметь действительный поток байтов 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. Хотя $ bytestream является действительным потоком байтов utf8:
    1. Назначить текущее значение $ bytestream для $ good
    2. Если $ bytestream является полностью ASCII (то есть каждый байт меньше 0x80), вырвитесь из цикла «while ... valid utf8».
    3. Установите $ bytestream равным результату "demangle ($ bytestream)", где demangle приведен ниже. Эта процедура отменяет преобразователь 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. То есть это не та случайность, которая случается случайно.

ИЗМЕНЕНО В ДОБАВИТЬ:

Обратите внимание, что этот метод, к сожалению, не слишком помогает с вашим примером "Боба". Я думаю, что эта строка также прошла два раунда преобразования cp1252-в-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 согласуются. Таким образом, представление этой последовательности кодовых точек Unicode в 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) })
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...