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

ssd*_*ssd 5 mysql perl encoding utf-8

我的Perl应用程序和MySQL数据库现在正确处理传入的UTF-8数据,但我必须转换预先存在的数据.有些数据似乎已编码为CP-1252,在编码为UTF-8并存储在MySQL中之前未进行解码.我已经阅读了O'Reilly文章将latin1中的MySQL数据转换为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));
Run Code Online (Sandbox Code Playgroud)

这解决了大多数情况,但如果针对proplerly编码的记录运行,它会破坏它们.我尝试过使用Encode :: GuessEncode :: Detect,但他们无法区分正确编码和错误编码的记录.所以我只是在转换后找到\ 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")))'
Run Code Online (Sandbox Code Playgroud)

并且这是一个右单引号未转换的示例:

perl -CO -MEncode -e 'print decode("utf-8", encode("cp-1252", decode("utf-8", "bob\xC3\xAF\xC2\xBF\xC2\xBDs")))'
Run Code Online (Sandbox Code Playgroud)

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

Dan*_*tin 6

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

  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
Run Code Online (Sandbox Code Playgroud)

这产生了这个unicode代码点序列:

e2 20ac 153 66 6f 75 72 20 73 63 6f 72 65 e2 20ac 9d
Run Code Online (Sandbox Code Playgroud)

("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
Run Code Online (Sandbox Code Playgroud)

编码的第三个参数 - 当它是一个子 - 告诉如何处理不可代表的字符.

这会产生:

e2 80 9c 66 6f 75 72 20 73 63 6f 72 65 e2 80 9d
Run Code Online (Sandbox Code Playgroud)

现在,这是一个有效的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
Run Code Online (Sandbox Code Playgroud)

将"1"作为解码的第三个参数传递可确保在字节流无效时我们的代码会发出嘎嘎声.这会产生:

201c 66 6f 75 72 20 73 63 6f 72 65 201d
Run Code Online (Sandbox Code Playgroud)

或打印:

$ 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”
Run Code Online (Sandbox Code Playgroud)

所以我认为完整的算法应该是这样的:

  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中的循环没有改变任何东西,则优化并且不执行此操作,等等)

.

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])});
  }
}
Run Code Online (Sandbox Code Playgroud)

这是基于这样的假设,即非全非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
Run Code Online (Sandbox Code Playgroud)

这会产生:

62 6f 62 ef bf bd 73
Run Code Online (Sandbox Code Playgroud)

现在,恰好是对于三个字节ef bf bd,unicode和cp1252同意.因此,在cp1252中表示这个unicode代码点序列就是:

62 6f 62 ef bf bd 73
Run Code Online (Sandbox Code Playgroud)

也就是说,数字序列相同.现在,这实际上是一个有效的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
Run Code Online (Sandbox Code Playgroud)

也就是说,utf-8字节流虽然是合法的utf-8字节流,但却编码了字符0xFFFD,它通常用于"不可翻译的字符".我怀疑这里发生的事情是,第一次*-to-utf8变换看到了一个它无法识别的角色,并将其替换为"不可翻译".然后无法以编程方式恢复原始角色.

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

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