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