将utf8字符串转换为ucs-2,并替换java中的无效字符

e1xvtsh3  于 2021-07-08  发布在  Java
关注(0)|答案(1)|浏览(882)

我在utf8中被刺痛了:
“红色??r”ö“塞斯”
我需要将其转换为有效的ucs-2(或固定大小的utf-16be没有bom,他们是一样的东西)编码,所以输出将是:“红色röses“作为”?“超出ucs-2的范围。
我尝试过:

@Test
public void testEncodeProblem() throws CharacterCodingException {
    String in = "Red\uD83C\uDF39\uD83C\uDF39Röses";
    ByteBuffer input = ByteBuffer.wrap(in.getBytes());

    CharsetDecoder utf8Decoder = StandardCharsets.UTF_16BE.newDecoder();
    utf8Decoder.onMalformedInput(CodingErrorAction.REPLACE);
    utf8Decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
    utf8Decoder.replaceWith(" ");

    CharBuffer decoded = utf8Decoder.decode(input);

    System.out.println(decoded.toString()); //  剥擰龌맰龌륒쎶獥 
}

不。

@Test
public void testEncodeProblem() {
    String in = "Red\uD83C\uDF39\uD83C\uDF39Röses";
    byte[] bytes = in.getBytes(StandardCharsets.UTF_16BE);
    String res = new String(bytes);
    System.out.println(res); //  Red�<�9�<�9Röses
}

不。
请注意”ö" 是有效的ucs-2符号。
有什么想法/图书馆吗?

fv2wmkja

fv2wmkja1#

不幸的是,这两个代码段实际上都不起作用,这是因为您误解了utf-16编码。utf-16可以对这些表情进行编码,它不是固定宽度的。没有“用utf-16编码修复”这样的东西。有。。ucs2。不是utf-16。be部分并没有使它成为“固定宽度”,它只是锁定在末端。这就是为什么这两个都印玫瑰。不幸的是,java没有附带ucs2编码系统,这使得这项工作更加困难和丑陋。
此外,这两个代码段都会失败,因为您正在调用禁止的方法。
任何时候你把字节转换成字符,反之亦然,字符转换就发生了。你不能选择退出。然而,仍然存在一堆方法,它们不接受任何参数来指示要使用哪个字符集编码。这些是被禁止的方法:这些默认为“系统默认值”,看起来像是有人挥舞着魔杖,这样我们就可以将字符转换成字节,反之亦然,而不用担心字符编码。
解决办法是永远不要使用被禁止的方法。更好的是,告诉ide它应该将它们标记为错误。唯一的例外是,您知道api的默认值不是“平台默认值”,而是一些正常的东西-我所知道的唯一一个是 Files.* api,默认为utf-8,而不是平台默认值。因此,使用无字符集变体是可以接受的。
如果确实必须使用平台默认值(仅适用于命令行工具),请通过传递 Charset.defaultCharset() .
禁止使用的方法列表很长,但是 new String(bytes) 以及 string.getBytes() 都在上面。不要使用这些方法/构造函数。永远不会。
此外,你的第一个片段是各种困惑。您想要对字符串进行编码(字符串已经是个字符,没有编码。就是这样。那么,为什么你要在没有什么可解码的情况下制作解码器?)到utf-16,而不是解码它:

String in = "Red\uD83C\uDF39\uD83C\uDF39Röses";
CharBuffer input = CharBuffer.wrap(in);
CharsetEncoder utf16Encoder = StandardCharsets.UTF_16BE.newEncoder();
utf16Encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
utf16Encoder.replaceWith(" ");
ByteBuffer encoded = utf16Encoder.encode(input);

System.out.println(new String(encoded.array(), StandardCharsets.UTF16_BE));

或第二个代码段:

@Test
public void testEncodeProblem() {
    String in = "Red\uD83C\uDF39\uD83C\uDF39Röses";
    byte[] bytes = in.getBytes(StandardCharsets.UTF_16BE);
    String res = new String(bytes, StandardCharsets.UTF_16BE);
    System.out.println(res);
}

但是,正如我所说的,两者都只是打印玫瑰,因为它们可以用utfè16表示。
那么,如何完成这项工作呢?如果java有一个内置的ucs2编码,它将是一个简单的替换 StandardCharsets.UTF_16BEStandardCharsets.UCS2 ,但没有这样的运气。所以,我想。。。可能是“用手”:

String in = "Red\uD83C\uDF39\uD83C\uDF39Röses";
ByteArrayOutputStream out = new ByteArrayOutputStream();
in.codePoints()
    .filter(a -> a < 65536)
    .forEach(a -> {
       out.write(a >> 8);
       out.write(a);
    });

// stream is ugly, but, because codePoints() was added in a time
// when oracle had just invented the shiny hammer, they are using it
// here for smearing butter on their sandwich. Silly geese. Oh well.

byte[] result = out.toByteArray();
// given that java has no way of reading UCS2, and UTF16BE doesn't fit,
// as there are chars representable in 2 bytes in UCS2 that take 3+ in
// UTF16BE, it's not possible to print this without another loop similar to above. 
// Let's just print the bytes and check em, by hand:

for (byte r : result) System.out.print(" " + (r & 0xFF));
System.out.println();
// For the roses string, printing with UTF-16BE does actually work,
// but it won't be true for all input strings...
System.out.println(new String(result, StandardCharsets.UTF_16BE));

耶!成功!
注意: codePointAt 可以在这里工作并避免难看的流,但是cpa的输入不是在“codepoint index”中,而是在“char index”中,这使得事情变得相当复杂;对于任何代理项对,都必须增加2。
对unicode、ucs2和utf-16的一些反思:
unicode是一个巨大的表,它将0到1112064之间的任何数字(约为20位半)Map到字符、控件概念、货币、标点符号、表情符号、方框图或其他类似字符的概念。
像utf-8或us\u ascii这样的编码定义了将这些数字的一部分或全部转换成一系列字节,这样它也可以被解码回通常存储在32位中的一系列码点,因为它们不适合16位,并且没有任何体系结构可以有意义地处理例如24位之类的内容。
为了容纳ucs2/utf-16,unicode规范中没有从0xd800到0xdfff的字符,这是有意的,而且永远不会有。
这意味着ucs2和utf-16或多或少是一回事,有一个“窍门”:
对于任何低于65536的unicode数字(因此理论上可以容纳2个字节),对于utf-16编码(可以编码emoji等),utf-16编码只是。。号码。直截了当。为2字节。d800 dfff不可能发生,因为那些代码点不是故意的。
对于65536以上的任何内容,使用d800到dfff的空闲块来生成所谓的代理项对。第二个“字符”(2字节的第二个块)与我们可以用d800 dfff范围存储的11位数据组合,总共16+11=27位,足以覆盖其余的数据。
因此,utf-16将任何unicode编码点编码为2字节或4字节。
ucs-2作为一个术语已经失去了它的意义。最初,它的意思是每个“字符”正好有2个字节,不多也不少,现在仍然是这个意思,但“字符”的含义已经扭曲得无法辨认:那是玫瑰?它计为2个字符。用java试试- x.length() 返回2,而不是1。ucs-2的一个合理定义是:1 char实际上意味着1 char,每个char由2个字节表示,如果您试图存储一个不合适的char(可能是一个代理项对),那么,这些字符就不能被编码,所以崩溃或应用on unprestable character代替占位符。不幸的是,这并不是ucs-2的意思,这让我们不得不重新编写应用此操作的任何代码(放弃/替换任何代理项对,以便字节长度正好是2*码点数)。
注意,基于java char 非常接近ucs2的理想(因为它是一个16位的数字,在java规范中是硬编码的):您可以循环遍历所有字符(如java的 char )丢弃任何 c >= 0xD800 && c < 0xE000 ,以及紧接着的人物,这将摆脱玫瑰。

相关问题