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

How do I convert stored misencoded data?

提问人:ssd 提问时间:5/10/2009 更新时间:5/15/2009 访问量:3518

问:

我的 Perl 应用程序和 MySQL 数据库现在可以正确处理传入的 UTF-8 数据,但我必须转换预先存在的数据。一些数据似乎被编码为 CP-1252,在被编码为 UTF-8 并存储在 MySQL 中之前没有被解码。我读过 O'Reilly 的文章 将 MySQL 数据从 latin1 转换为 utf8 utf-8,但尽管它经常被引用,但它并不是一个明确的解决方案。

我查看了 Encode::D oubleEncodedUTF8Encoding::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::Guess 和 Encode::D etect,但它们无法区分正确编码和错误编码的记录。因此,如果在转换后找到 \x{FFFD} 字符,我只需撤消转换。

但是,有些记录仅部分转换。下面是一个示例,其中左边的大括号被正确转换,但右边的大括号被篡改了。

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")))'

我是否也在这里处理双重编码数据?我还需要做些什么来转换这些记录?

MySQL Perl 编码 UTF-8

评论


答:

6赞 Daniel Martin 5/15/2009 #1

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

  1. CP1252 数据通过 CP1252 到 UTF8 进程运行两次,或者
  2. 通过 CP1252 到 UTF8 进程运行的 UTF8 数据

(当然,这两种情况看起来是一样的)

现在,这就是你所期望的,那么为什么你的代码不起作用呢?

首先,我想向您介绍此表,该显示了从 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 中将它们中的每一个表示为一个字节,但是当 cp1252 中无法表示 unicode 字符时,让我们将其替换为具有相同数值的字节。(而不是默认值,即用问号替换它)然后,如果我们对数据发生的情况是正确的,我们应该有一个有效的 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),请断开这个“而......有效的 UTF8“ 循环。
    3. 将 $bytestream 设置为“demangle($bytestream)”的结果,其中 demangle 如下所示。此例程撤消了我们认为此数据受到影响的 cp1252 到 utf8 转换器。
  3. 如果$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])});
  }
}

这是基于这样的假设,即除非它真的是 utf-8,否则不是全 ASCII 的字符串实际上是非常罕见的,除非它确实是 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 转换看到了一个它无法识别的字符,并将其替换为“不可翻译”。然后,无法以编程方式恢复原始字符。

结果是,您无法仅通过进行解码然后查找 utf8 然后查找0xFFFD来检测字节流是否有效 utf8(我上面给出的算法需要)。相反,您应该使用如下内容:

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