Фиксирование файла, состоящего из UTF-8 и Windows-1252

У меня есть приложение, которое создает файл UTF-8, но некоторая часть содержимого неправильно закодирована. Некоторые из символов кодируются как iso-8859-1 aka iso-latin-1 или cp1252 aka Windows-1252. Есть ли способ восстановить исходный текст?

Ответ 1

Да!

Очевидно, лучше исправить программу, создающую файл, но это не всегда возможно. Ниже следует два решения.

Строка может содержать сочетание кодировок

Encoding::FixLatin предоставляет функцию с именем fix_latin, которая декодирует текст, который состоит из сочетания UTF-8, iso-8859-1, cp1252 и US-ASCII.

$ perl -e'
   use Encoding::FixLatin qw( fix_latin );
   $bytes = "\xD0 \x92 \xD0\x92\n";
   $text = fix_latin($bytes);
   printf("U+%v04X\n", $text);
'
U+00D0.0020.2019.0020.0412.000A

Используются эвристики, но они достаточно надежны. Только следующие случаи не будут выполнены:

  • One of
    [ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß]
    encoded using iso-8859-1 or cp1252, followed by one of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿]
    encoded using iso-8859-1 or cp1252.

  • One of
    [àáâãäåæçèéêëìíîï]
    encoded using iso-8859-1 or cp1252, followed by two of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿]
    encoded using iso-8859-1 or cp1252.

  • One of
    [ðñòóôõö÷]
    encoded using iso-8859-1 or cp1252, followed by two of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿]
    encoded using iso-8859-1 or cp1252.

Тот же результат может быть получен с использованием основного модуля Encode, хотя я предполагаю, что это справедливый бит медленнее, чем Encoding:: FixLatin с Encoding:: FixLatin:: XS установлен.

$ perl -e'
   use Encode qw( decode_utf8 encode_utf8 decode );
   $bytes = "\xD0 \x92 \xD0\x92\n";
   $text = decode_utf8($bytes, sub { encode_utf8(decode("cp1252", chr($_[0]))) });
   printf("U+%v04X\n", $text);
'
U+00D0.0020.2019.0020.0412.000A

В каждой строке используется только одна кодировка

fix_latin работает на уровне персонажа. Если известно, что каждая строка полностью закодирована с использованием одного из UTF-8, iso-8859-1, cp1252 или US-ASCII, вы можете сделать процесс еще более надежным, если проверить правильность линии UTF-8.

$ perl -e'
   use Encode qw( decode );
   for $bytes ("\xD0 \x92 \xD0\x92\n", "\xD0\x92\n") {
      if (!eval {
         $text = decode("UTF-8", $bytes, Encode::FB_CROAK|Encode::LEAVE_SRC);
         1  # No exception
      }) {
         $text = decode("cp1252", $bytes);
      }

      printf("U+%v04X\n", $text);
   }
'
U+00D0.0020.2019.0020.00D0.2019.000A
U+0412.000A

Используются эвристики, но они очень надежны. Они будут только терпеть неудачу, если для данной строки истинны все:

  • Строка кодируется с использованием iso-8859-1 или cp1252,

  • At least one of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷]
    is present in the line,

  • All instances of
    [ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß]
    are always followed by exactly one of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿],

  • All instances of
    [àáâãäåæçèéêëìíîï]
    are always followed by exactly two of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿],

  • All instances of
    [ðñòóôõö÷]
    are always followed by exactly three of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿],

  • Ничего из присутствующих [& # xF8; & # xF9; & # xFA; & # xFB; & # xFC; & # xFD; & # xFE; & # xFF;]
    в строке и

  • None of
    [€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ<NBSP>¡¢£¤¥¦§¨©ª«¬<SHY>®¯°±²³´µ¶·¸¹º»¼½¾¿]
    are present in the line except where previously mentioned.


Примечания:

  • Encoding:: FixLatin устанавливает инструмент командной строки fix_latin для преобразования файлов, и было бы тривиально писать один, используя второй подход.
  • fix_latin (как функция, так и файл) можно ускорить, установив Encoding::FixLatin::XS.
  • Такой же подход можно использовать для микширования UTF-8 с другими однобайтовыми кодировками. Надежность должна быть одинаковой, но она может меняться.

Ответ 2

Это одна из причин, по которой я написал Unicode::UTF8. С Unicode:: UTF8 это тривиально, используя резервную опцию в Unicode:: UTF8:: decode_utf8().

use Unicode::UTF8 qw[decode_utf8];
use Encode        qw[decode];

print "UTF-8 mixed with Latin-1 (ISO-8859-1):\n";
for my $octets ("\xD0 \x92 \xD0\x92\n", "\xD0\x92\n") {
    no warnings 'utf8';
    printf "U+%v04X\n", decode_utf8($octets, sub { $_[0] });
}

print "\nUTF-8 mixed with CP-1252 (Windows-1252):\n";
for my $octets ("\xD0 \x92 \xD0\x92\n", "\xD0\x92\n") {
    no warnings 'utf8';
    printf "U+%v04X\n", decode_utf8($octets, sub { decode('CP-1252', $_[0]) });
}

Вывод:

UTF-8 mixed with Latin-1 (ISO-8859-1):
U+00D0.0020.0092.0020.0412.000A
U+0412.000A

UTF-8 mixed with CP-1252 (Windows-1252):
U+00D0.0020.2019.0020.0412.000A
U+0412.000A

Unicode:: UTF8 записывается в C/XS и вызывает только обратный вызов/резерв при столкновении с некорректной последовательностью UTF-8.

Ответ 3

Недавно я наткнулся на файлы с жестким сочетанием кодировок UTF-8, CP1252 и UTF-8, затем интерпретировался как CP1252, затем снова закодирован как UTF-8, который снова интерпретируется как CP1252 и т.д.

Я написал код ниже, который хорошо работал у меня. Он ищет типичные последовательности байтов UTF-8, даже если некоторые из байтов не являются UTF-8, но Unicode представляет эквивалентный байт CP1252.

my %cp1252Encoding = (
# replacing the unicode code with the original CP1252 code
# see e.g. http://www.i18nqa.com/debug/table-iso8859-1-vs-windows-1252.html
"\x{20ac}" => "\x80",
"\x{201a}" => "\x82",
"\x{0192}" => "\x83",
"\x{201e}" => "\x84",
"\x{2026}" => "\x85",
"\x{2020}" => "\x86",
"\x{2021}" => "\x87",
"\x{02c6}" => "\x88",
"\x{2030}" => "\x89",
"\x{0160}" => "\x8a",
"\x{2039}" => "\x8b",
"\x{0152}" => "\x8c",
"\x{017d}" => "\x8e",

"\x{2018}" => "\x91",
"\x{2019}" => "\x92",
"\x{201c}" => "\x93",
"\x{201d}" => "\x94",
"\x{2022}" => "\x95",
"\x{2013}" => "\x96",
"\x{2014}" => "\x97",
"\x{02dc}" => "\x98",
"\x{2122}" => "\x99",
"\x{0161}" => "\x9a",
"\x{203a}" => "\x9b",
"\x{0153}" => "\x9c",
"\x{017e}" => "\x9e",
"\x{0178}" => "\x9f",
);
my $re = join "|", keys %cp1252Encoding;
$re = qr/$re/;
my %cp1252Decoding = reverse % cp1252Encoding;
my $cp1252Characters = join "|", keys %cp1252Decoding;

sub decodeUtf8
{
    my ($str) = @_;

    $str =~ s/$re/ $cp1252Encoding{$&} /eg;
    utf8::decode($str);
    return $str;
}

sub fixString
{
    my ($str) = @_;

    my $r = qr/[\x80-\xBF]|$re/;

    my $current;
    do {
        $current = $str;

        # If this matches, the string is likely double-encoded UTF-8. Try to decode
        $str =~ s/[\xF0-\xF7]$r$r$r|[\xE0-\xEF]$r$r|[\xC0-\xDF]$r/ decodeUtf8($&) /eg;

    } while ($str ne $current);

    # decodes any possible left-over cp1252 codes to Unicode
    $str =~ s/$cp1252Characters/ $cp1252Decoding{$&} /eg;
    return $str;
}

Это имеет аналогичные ограничения, такие как ответ ikegami, за исключением того, что те же ограничения применимы и к закодированным строкам UTF-8.