Java字符集编码问题(从UTF8到cp866)

12
如何将文本从utf8/cp1251(Windows Cyrillic)转换为DOS Cyrillic(cp866)?
我找到了这个示例:
Charset fromCharset = Charset.forName("utf8");
Charset toCharset = Charset.forName("cp866");

String text1 = "Николай"; // my name in bulgarian
String text2 = "Nikolay"; // my name in english

System.out.println("TEXT1 :[" + toCharset.decode(fromCharset.encode(text1)).toString() + "]");
System.out.println("TEXT2 :[" + toCharset.decode(fromCharset.encode(text2)).toString() + "]");

输入的内容为:

TEXT1 :[╨Э╨╕╨║╨╛╨╗╨░╨╣] // WRONG
TEXT2 :[Nikolay]  // CORRECT

问题出在哪里?


2
你期望什么?你正在使用UTF-8编码“Николай”,然后使用Cp866解码已编码的字节。输出对我来说似乎是合理的,但显然你期望发生其他一些魔法。 - jarnbjo
5个回答

15
首先:如果您有一个String对象,那么它不再具有编码,它是一个纯Unicode字符串(*)!
在Java中,编码仅在从字节(byte[])转换为字符串(String)或反之时使用。 (理论上可以直接从byte[]转换为byte[],但我还没有见过在Java中实现这种方式)。
如果您有一些cp1251编码的数据,则必须是byte[](即字节数组)或某种流(例如作为InputStream提供给您)。
如果您想将一些数据提供为cp866,则必须将其提供为byte[]或某种流(例如OutputStream)。
另外:没有“utf8/cp1251”这样的东西。 UTF-8和CP-1251几乎是无关的字符编码。您的输入可能是UTF-8或CP-1251(或其他内容)。它实际上不能同时是两者(+)。
这是强制性链接:每个软件开发人员绝对必须了解Unicode和字符集的绝对最低限度(没有借口!) (*) 是的,严格来说,它具有编码,并且是UTF-16,但大多数情况下,您可以(并且应该)将其视为“无编码理想Unicode字符串”
(+) 严格来说,如果它只使用在两种编码中编码为相同字节的字符集,则两者都可以,这通常是ASCII子集。

为什么要在那里加上“叹气…”?它对答案没有任何帮助,乍一看似乎是在贬低提问者。 - McStretch
关于字符编码的各种常见误解,这篇文章讲得非常清楚易懂。真希望我可以给它点赞超过一次。 - sleske
@McStretch:你说得对。这只是我读到这样的问题时的初始反应。我只是添加了答案的其余部分,因为仅仅回答“叹气”会显得非常恶劣;-) - Joachim Sauer
我理解你最初的反应,特别是因为你显然是一个非常有知识的软件开发者。我唯一的顾虑是,经验较少的开发人员可能会被这样的反应吓到,我不想因为错误的原因而让无知的开发人员离开。感谢你编辑了你的帖子,我现在会点赞了。 - McStretch

5
问题在于您试图将一个编码的输出解码为另一种编码。假设您有一个只能输出JPEG格式的程序和另一个只能读取PNG格式的程序……您是否期望能够使用第二个程序读取第一个程序的输出?
在这种情况下,这两种编码恰好兼容ASCII字符,但从根本上说,您正在做错误的事情。
如果您已经有了UTF-8格式的文本,您应该使用UTF-8编码将其从二进制数据读入Unicode字符串中,然后再使用其他编码将其写回二进制数据。Unicode基本上是Java本地文本格式的中间步骤。这相当于将JPEG输出加载到另一个可以执行转换为PNG的程序中,然后再使用第二个应用程序读取它。

5

以下是您问题的简短解决方案:

 System.out.write("ВАСЯ\n".getBytes("cp866")); // its right
 System.out.println("ВАСЯ".getBytes("cp866")); // its wrong

cmd.exe的结果:

C:\Documents and Settings\afram\Мои документы\NetBeansProjects\Encoding\dist>java -jar Encoding.jar

ВАСЯ

[B@1bab50a

提示:此处为计算机命令执行的结果。

System.out.write("ВАСЯ\n".getBytes("cp866")) 是错误的。输出是 Р?Р?РЎРЇ - Green
1
@Green 这很可能是因为你的终端编码与cp866不同。 - vadipp

3

简短版:

您将一个utf8字符串解码为cp866编码。由于utf8和cp866只共享ascii符号,因此其他所有内容都会被搞乱。

详细版:

Java内部使用UTF-16表示字符串,所有字符串对象都以UTF-16进行编码。

Charset.encode()创建一个包含所选编码中的字符串的bytebuffer,在您的代码中,这将把Java UTF-16字符串转换为utf-8编码的字节数组。

Charset.decode()接受作为字符集编码的bytebuffer,并将其转换为Java UTF-16字符串。在您的情况下,您使用cp866解码了一个utf-8字符串,导致字符串被搞乱了。

由于Java字符串具有指定的编码方式,因此在读取或写入它们时必须指定它们的编码方式。InputStreamReader和OutputStreamWriter都提供了带有Charset参数的构造函数。

以下是一个示例,展示如何转换文件/流。

//input the source is encoded in fromCharset
BufferedReader in = new BufferedReader(new InputStreamReader(...,fromCharset));
//output the target will be encoded in toCharset
PrintWriter out = new PrintWriter(new OutputStreamWriter(...,toCharset));
//reads a decoded String
String line = in.readLine();
while(line != null)
{
   out.println(line);
   line = in.readLine();
}

0
问题在于,您的控制台输出不是cp866。控制台是一个,转换是另一个。
在Java中,内部字符串始终为Unicode,字符集对于输入/输出操作非常重要。您没有指定要对“converted”字符串执行什么操作,但是您应该确实查看InputStreamReader / OutputStreamWriter类。它们为您的I/O操作提供字符集设置。

3
可能这是一个额外的问题,但基本问题在于他把没有意义的操作链接在一起。将某些文本编码为UTF-8,然后使用另一种编码解码它并不会产生有用的结果。 - Joachim Sauer
我想将数据发送到使用DOS Cyrillic的财务打印机。 - NikolayGS
2
所以你需要使用OutputStreamWriter。即使你需要将byte[]直接发送到端口,你也可以从Writer中受益,它可以将数据写入ByteArrayOutputStream。 - Danubian Sailor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接