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