问题 修复由UTF-8和Windows-1252组成的文件


我有一个生成UTF-8文件的应用程序,但有些内容编码不正确。一些字符编码为iso-8859-1 aka iso-latin-1或cp1252 aka Windows-1252。有没有办法恢复原始文本?


5029
2018-02-23 19:33


起源

(这是Perl中的常见问题,因为解码后的文本在没有编码的情况下发出。) - ikegami
我不认为这是Perl特有的,Ruby和PHP也有同样的问题。 Python 3具有不同的字节与字符类型。 - chansen


答案:


是!

显然,最好修复创建文件的程序,但这并不总是可行的。以下是两种解决方案。

一行可以包含多种编码

编码:: 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

采用启发式方法,但它们相当可靠。只有以下情况才会失败:

  • 之一
    [ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß]
    使用iso-8859-1或cp1252编码,然后是其中之一
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]
    使用iso-8859-1或cp1252编码。

  • 之一
    [àáâãäåæçèéêëìíîï]
    使用iso-8859-1或cp1252编码,然后是两个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]
    使用iso-8859-1或cp1252编码。

  • 之一
    [ðñòóôõö÷]
    使用iso-8859-1或cp1252编码,然后是两个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]
    使用iso-8859-1或cp1252编码。

使用核心模块可以产生相同的结果 编码虽然我想这比安装Encoding :: FixLatin :: XS的Encoding :: FixLatin慢一点。

$ 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进行编码,

  • 至少有一个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷]
    在线,

  • 所有的实例
    [ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß]
    总是紧跟其中一个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]

  • 所有的实例
    [àáâãäåæçèéêëìíîï]
    总是紧跟两个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]

  • 所有的实例
    [ðñòóôõö÷]
    总是紧跟三个
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]

  • 没有
    [øùúûüýþÿ]
    在线,和

  • 没有
    [€,ƒ“...†‡‰Š<OEZ‘’‘’•--~™S>œžŸ<NBSP>¡¢£¤¥|§¨©ª«¬<SHY>®°±²³'μ¶·¸¹º»¼½¾¿]
    除前面提到的以外,它们都存在于该行中。


笔记:

  • Encoding :: FixLatin安装命令行工具 fix_latin 转换文件,使用第二种方法写一个文件是微不足道的。
  • fix_latin (功能和文件)都可以通过安装来加速 编码:: FixLatin :: XS
  • 相同的方法可用于UTF-8与其他单字节编码的混合。可靠性应该相似,但可以变化。

11
2018-02-23 19:33



不应将有效的UTF-8序列<D0 92>解码为U + 0412吗? - chansen
@chansen,当你知道这行使用cp1252编码时,它不应该。 - ikegami


这是我写的原因之一 统一:: UTF8。使用Unicode :: UTF8,这在使用回退选项时是微不足道的 统一:: 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序列时调用回调/回退。


5
2018-02-23 20:20



这可能比Encode :: decode_utf8快一点(因为Encode :: decode_utf8的回调必须产生UTF-8),但它仍然使用回调。编码:: FixLatin没有,所以它必然会更快。 (如果不是,它可以变得更快。)它也更简单(fix_latin($bytes) VS decode_utf8($bytes, sub { decode('cp1252', $_[0]) })) - ikegami
@ikegami,Unicode :: UTF8完全优于Encode的UTF-8实现, 贝特马克。 - chansen
正如我所说的那样。你好像错过了这一点。 - ikegami
我说的没有特别针对cp1252。同样的事情适用于拉丁语-1。 fix_latin($bytes) 更简单,更快(或者必须更快) decode_utf8($bytes, sub { $_[0] }) 因为缺乏回调。 (缺点是 fix_latin 不能用来解决你的其他问题。) - ikegami


最近我遇到了严重混合的UTF-8,CP1252和UTF-8编码的文件,然后解释为CP1252,然后再次编码为UTF-8,再次解释为CP1252,依此类推。

我写了下面的代码,对我来说效果很好。它查找典型的UTF-8字节序列,即使某些字节不是UTF-8,也是等效CP1252字节的Unicode表示。

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编码的字符串。


-1
2017-12-03 12:50



没关系。我自己管理了回滚。答案适用于问题,并处理更复杂的编码问题,因此这里是适当的。 - fishinear
这些假设与您的假设类似,但最后一个假设除外。独立的CP1252 80-7F将解码为Unicode。唯一的额外假设是没有UTF-8序列可以编码您提到的任何字符序列。如果你想解码双重编码的UTF-8,这是不可避免的。它仍然不是一个非常严格的假设。 - fishinear
也许类似,但因为循环,还有更多。你的代码失败了 encode_utf8("\xF0\xBF\xBF\xBF"), 例如。除了您的解决方案之外的所有解 "\xF0\xBF\xBF\xBF" - ikegami
encode_utf8("\xF0\xBF\xBF\xBF") 解码为Unicode F0 BF BF BF。您期望解码到什么? - fishinear
确实,但你的代码返回 "\x{3FFFF}" 代替 "\xF0\xBF\xBF\xBF"。 - ikegami