如何转换存储的错误编码数据?

时间:2009-05-10 00:36:14

标签: mysql perl encoding utf-8

我的Perl应用程序和MySQL数据库现在正确处理传入的UTF-8数据,但我必须转换预先存在的数据。有些数据似乎已编码为CP-1252,在编码为UTF-8并存储在MySQL中之前未进行解码。我已经阅读了O'Reilly的文章Turning MySQL data in latin1 to utf8 utf-8,但是虽然它经常被引用,但它并不是一个明确的解决方案。

我查看了Encode::DoubleEncodedUTF8Encoding::FixLatin,但都没有处理我的数据。

这是我到目前为止所做的:

#Return the $bytes from the DB using BINARY()
my $characters = decode('utf-8', $bytes);
my $good = decode('utf-8', encode('cp-1252', $characters));

这解决了大多数情况,但如果针对proplerly编码的记录运行,它会破坏它们。我尝试过使用Encode::GuessEncode::Detect,但他们无法区分正确编码和错误编码的记录。因此,如果在转换后找到\x{FFFD} character,我只需撤消转化。

但有些记录只是部分转换。这是一个左卷曲引号被正确转换的例子,但正确的卷曲引号被破坏了。

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 个答案:

答案 0 :(得分:6)

以“四分”为例,几乎可以肯定是双重编码数据。看起来像是:

  1. cp1252通过cp1252到utf8进程两次运行的数据,或
  2. 通过cp1252到utf8进程运行的utf8数据
  3. (当然,两种情况看起来都相同)

    现在,这就是你所期望的,为什么你的代码没有用呢?

    首先,我想推荐给this table,其中显示了从cp1252到unicode的转换。我要你注意的重要一点是有一些字节(如0x9D)在cp1252中无效。

    因此,当我想到将一个cp1252写入utf8转换器时,我需要对那些不在cp1252中的字节做些什么。我能想到的唯一明智的事情是将未知字节转换为相同值的unicode字符。事实上,这似乎是发生了什么。让我们一步一步地回到你的“四分”榜样。

    首先,因为它是有效的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. 如果不是undef,请将$ good放回数据库。如果从未分配$ good,则假设$ bytestream是一个cp1252字节流并将其转换为utf8。 (当然,如果步骤2中的循环没有改变任何东西等,则优化并且不执行此操作。)
    4. 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。也就是说,这不是偶然发生的事情。

      编辑添加:

      请注意,遗憾的是,这种技术对您的“bob”示例没有多大帮助。我认为那个字符串也经历了两轮cp1252-to-utf8转换,但不幸的是还有一些腐败。使用与以前相同的技术,我们首先将字节序列读作utf8,然后查看我们得到的unicode字符引用序列:

      $ 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中表示这个unicode代码点序列只是:

      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变换看到了一个它无法识别的角色,并将其替换为“不可翻译”。然后无法以编程方式恢复原始角色。

      结果是你无法通过执行解码然后查找0xFFFD来检测字节流是否有效utf8(我上面给出的算法所需)。相反,你应该使用这样的东西:

      sub is_valid_utf8 {
        defined(eval { decode("utf-8", $_[0], 1) })
      }