将UTF8字符串转换为UCS-2并替换java中的无效字符

msa*_*gel 0 java unicode encoding utf-8 utf-16

我对 UTF8 感到困惑:

\n
\n

“RedR\xc3\xb6ses”

\n
\n

我需要将其转换为有效的 UCS-2(或没有 BOM 的固定大小 UTF-16BE,它们是相同的东西)编码,因此输出将是:\n“Red R\xc3\xb6ses”作为“”输出UCS-2 的射程。

\n

我尝试过的:

\n
 @Test\npublic void testEncodeProblem() throws CharacterCodingException {\n    String in = "Red\\uD83C\\uDF39\\uD83C\\uDF39R\xc3\xb6ses";\n    ByteBuffer input = ByteBuffer.wrap(in.getBytes());\n\n    CharsetDecoder utf8Decoder = StandardCharsets.UTF_16BE.newDecoder();\n    utf8Decoder.onMalformedInput(CodingErrorAction.REPLACE);\n    utf8Decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);\n    utf8Decoder.replaceWith(" ");\n\n    CharBuffer decoded = utf8Decoder.decode(input);\n\n    System.out.println(decoded.toString()); //  \xe5\x89\xa5\xe6\x93\xb0\xe9\xbe\x8c\xeb\xa7\xb0\xe9\xbe\x8c\xeb\xa5\x92\xec\x8e\xb6\xe7\x8d\xa5 \n}\n
Run Code Online (Sandbox Code Playgroud)\n

没有。

\n
    @Test\npublic void testEncodeProblem() {\n    String in = "Red\\uD83C\\uDF39\\uD83C\\uDF39R\xc3\xb6ses";\n    byte[] bytes = in.getBytes(StandardCharsets.UTF_16BE);\n    String res = new String(bytes);\n    System.out.println(res); //  Red\xef\xbf\xbd<\xef\xbf\xbd9\xef\xbf\xbd<\xef\xbf\xbd9R\xc3\xb6ses\n}\n
Run Code Online (Sandbox Code Playgroud)\n

没有。

\n

请注意,“\xc3\xb6”是有效的 UCS-2 符号。

\n

有什么想法/图书馆吗?

\n

rzw*_*oot 5

不幸的是,这两个片段实际上都不起作用,那是因为您误解了 UTF-16 编码。UTF-16可以编码这些表情符号,它不是固定宽度。不存在“用 UTF-16 编码修复”这样的东西。有..UCS2。这不是 UTF-16。BE 部分并不使其成为“固定宽度”,它只是锁定字节顺序。这就是为什么这两个都打印玫瑰。不幸的是,Java 没有附带 UCS2 编码系统,这使得这项工作变得更加困难和丑陋。

\n

此外,两个片段都会失败,因为您正在调用禁止的方法。

\n

每当您将字节转换为字符或反之亦然时,都会发生字符转换。你不能选择不这样做。尽管如此,仍然存在一些方法,它们不带任何参数来指示您想要使用哪种字符集编码。这些是被禁止的方法:这些默认为“系统默认”,看起来就像有人挥舞着魔杖,这样我们就可以将字符转换为字节,反之亦然,而不必担心字符编码。

\n

解决方案是永远不要使用禁止的方法。更好的是,告诉您的 IDE 应该将它们标记为错误。唯一的例外是您知道 API 默认值不是“平台默认值”,而是某种理智的东西 - 我所知道的唯一一个是 API,Files.*它默认为 UTF-8 而不是平台默认值。因此,使用无字符集变体是可以接受的。

\n

如果您确实必须具有平台默认值(仅适用于命令行工具),请通过传递 使其明确Charset.defaultCharset()

\n

禁止方法的列表很长,但是new String(bytes)string.getBytes()都在上面。不要使用这些方法/构造函数。曾经

\n

此外,您的第一个片段很混乱。您想要一个字符串(字符串已经是字符并且没​​有编码。它就是它的本质。那么,当没有什么可解码时,为什么要制作解码器?)到 UTF-16,而不是解码它:

\n
String in = "Red\\uD83C\\uDF39\\uD83C\\uDF39R\xc3\xb6ses";\nCharBuffer input = CharBuffer.wrap(in);\nCharsetEncoder utf16Encoder = StandardCharsets.UTF_16BE.newEncoder();\nutf16Encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);\nutf16Encoder.replaceWith(" ");\nByteBuffer encoded = utf16Encoder.encode(input);\n\nSystem.out.println(new String(encoded.array(), StandardCharsets.UTF16_BE));\n
Run Code Online (Sandbox Code Playgroud)\n

或第二个片段:

\n
@Test\npublic void testEncodeProblem() {\n    String in = "Red\\uD83C\\uDF39\\uD83C\\uDF39R\xc3\xb6ses";\n    byte[] bytes = in.getBytes(StandardCharsets.UTF_16BE);\n    String res = new String(bytes, StandardCharsets.UTF_16BE);\n    System.out.println(res);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但是,正如我所说,两者都只打印玫瑰,因为它们可以用 UTF_16 表示。

\n

那么,如何完成工作呢?如果 java 内置了 UCS2 编码,那么将很简单,只需替换StandardCharsets.UTF_16BEStandardCharsets.UCS2,但没有这样的运气。所以,我想......可能是“手工”:

\n
String in = "Red\\uD83C\\uDF39\\uD83C\\uDF39R\xc3\xb6ses";\nByteArrayOutputStream out = new ByteArrayOutputStream();\nin.codePoints()\n    .filter(a -> a < 65536)\n    .forEach(a -> {\n       out.write(a >> 8);\n       out.write(a);\n    });\n\n// stream is ugly, but, because codePoints() was added in a time\n// when oracle had just invented the shiny hammer, they are using it\n// here for smearing butter on their sandwich. Silly geese. Oh well.\n\nbyte[] result = out.toByteArray();\n// given that java has no way of reading UCS2, and UTF16BE doesn't fit,\n// as there are chars representable in 2 bytes in UCS2 that take 3+ in\n// UTF16BE, it's not possible to print this without another loop similar to above. \n// Let's just print the bytes and check em, by hand:\n\nfor (byte r : result) System.out.print(" " + (r & 0xFF));\nSystem.out.println();\n// For the roses string, printing with UTF-16BE does actually work,\n// but it won't be true for all input strings...\nSystem.out.println(new String(result, StandardCharsets.UTF_16BE));\n
Run Code Online (Sandbox Code Playgroud)\n

耶!成功!

\n

注意:codePointAt这里可以工作并避免丑陋的流,但是 cPA 的输入不在“代码点索引”中,而是在“字符索引”中,这使得事情变得相当复杂;对于任何代理对,您都必须增加 2。

\n
\n

对 unicode、UCS2 和 UTF-16 的一些反思:

\n

Unicode 是一个巨大的表,它将 0 到 1,112,064(大约 20 位半)之间的任何数字映射到字符、控制概念、货币、标点符号、表情符号、方框图或其他字符概念。

\n

像 UTF-8 或 US_ASCII 这样的编码定义了将这些数字中的部分或全部转换为一系列字节,以便它也可以解码回代码点序列,这些代码点通常存储在 32 位中,因为它们不适合 16 位,并且没有任何架构能够有意义地处理 24 位或其他类似的问题。

\n

为了适应 UCS2/UTF-16,unicode 规范中从 0xD800 到 0xDFFF 之间没有任何字符,这是故意的,而且永远不会有。

\n

这意味着 UCS2 和 UTF-16 或多或少是相同的,只是有一个“技巧”:

\n

对于任何低于 65536 的 unicode 数字(理论上可以容纳 2 个字节),对于 UTF-16 编码(可以编码表情符号等),UTF-16 编码只是......数字。直起来。作为2字节。D800-DFFF 不可能发生,因为这些代码点是故意不存在的。

\n

对于任何高于65536 的数据,D800 到 DFFF 的空闲块将用于生成所谓的代理对。第二个“字符”(第二个 2 字节块)与我们可以用 D800-DFFF 系列存储的 11 位数据相结合,总共 16+11 = 27 位,足以覆盖其余部分。

\n

因此,UTF-16 会将任何 unicode 代码点编码为 2 字节或 4 字节。

\n

UCS-2 作为一个术语已经基本失去了它的意义。最初,它的意思是每个“字符”正好 2 个字节,不多也不少,现在仍然是这个意思,但是“一个字符”的含义已经被扭曲得面目全非:那朵玫瑰?它算作 2 个字符。在 java 中尝试一下 -x.length()返回 2,而不是 1。 UCS-2 的一个比较合理的定义是:1 个字符实际上意味着 1 个字符,每个字符由 2 个字节表示,如果您尝试存储一个不适合的字符(将是一个代理对),好吧,这些只是无法编码,因此崩溃或应用 on-unreprestable-character-instead 占位符。不幸的是,这并不是 UCS-2 的意思,这让我们不得不编写任何应用此操作的代码(丢弃/用占位符替换任何代理项对,以便字节长度恰好为 2*number的代码点)我们自己。

\n

请注意,这个代理对的东西为您提供了不同的策略,基于 javachar非常接近 UCS2 的理想(因为它是一个 16 位数字,在 java 规范中硬编码)的事实:您可以循环遍历所有字符(如 java 中的char)并丢弃任何c >= 0xD800 && c < 0xE000以及紧随其后的字符,这将消除玫瑰。

\n

  • 基于[this](https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html)`ISO-10646-UCS-2`只是`UTF16的别名-BE`(而不是“真正的”UCS-2)。 (2认同)