Java에서 유효하지 않은 문자를 대체하여 UTF8 문자열을 UCS-2로 변환

Nov 17 2020

UTF8에 스팅이 있습니다.

"빨간색 🌹🌹 로제 스"

유효한 UCS-2 (또는 BOM이없는 고정 크기 UTF-16BE, 동일한 항목) 인코딩으로 변환해야하므로 출력은 다음과 같습니다. "Red Röses"as the "🌹"out of range of 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 기호입니다.

아이디어 / 도서관이 있습니까?

답변

3 rzwitserloot Nov 17 2020 at 01:13

불행히도 두 조각 모두 실제로 작동하지 않으며 UTF-16 인코딩을 오해하기 때문입니다. UTF-16 CAN 이 폭 고정되지 않고, 그 이모티콘을 인코딩한다. 'UTF-16 인코딩으로 고정'과 같은 것은 없습니다. 저기 .. UCS2. UTF-16이 아닙니다. BE 부분은 '고정 된 너비'로 만드는 것이 아니라 단지 엔디안으로 잠 깁니다. 이것이이 두 가지 모두 장미를 인쇄하는 이유입니다. 안타깝게도 Java는 UCS2 인코딩 시스템과 함께 제공되지 않으므로이 작업이 더 어렵고 추악 해집니다.

또한 금지 된 메서드를 호출하기 때문에 두 조각 모두 실패합니다.

바이트를 문자로 또는 그 반대로 변환 할 때마다 문자 변환이 발생 합니다. 선택 해제 할 수 없습니다. 그럼에도 불구하고 어떤 문자 집합 인코딩을 사용하고 싶은지 나타내는 매개 변수를 사용하지 않는 많은 메서드가 존재합니다. 금지 된 방법은 다음과 같습니다. 기본값은 '시스템 기본값'이며, 누군가가 마술 지팡이를 흔들어 문자 인코딩에 대해 걱정하지 않고 문자를 바이트로 또는 그 반대로 변환 할 수 있도록 만든 것처럼 보입니다.

해결책은 금지 된 방법을 사용하지 않는 것입니다. 더 좋은 방법은 IDE에 오류로 플래그를 지정해야한다는 것입니다. 유일한 예외는 API의 기본값이 '플랫폼 기본값'이 아니라 정상적인 것으로 알고있는 경우입니다. 내가 아는 유일한 것은 Files.*API이며, 플랫폼 기본값이 아닌 UTF-8입니다. 따라서 charset-less 변형을 사용하는 것이 허용됩니다.

플랫폼 기본값 (명령 줄 도구에만 해당)이 있어야하는 경우 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 으로 표현 가능 하기 때문 입니다.

그래서, 어떻게 일을 끝내는가? 했다 자바는 교체 같은 간단한 것, 내장 UCS2 인코딩을 가지고 StandardCharsets.UTF_16BE함께 StandardCharsets.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));

예이! 성공!

NB : codePointAt작동하고 여기서 추악한 스트림을 피할 수 있지만, cPA의 입력은 '코드 포인트 인덱스'가 아니라 '문자 인덱스'에 있으므로 문제가 다소 복잡해집니다. 모든 대리 쌍에 대해 2 씩 증가시켜야합니다.


유니 코드, UCS2 및 UTF-16에 대한 일부 내부 검사 :

유니 코드는 0에서 1,112,064 (약 20 비트) 사이의 숫자를 문자, 제어 개념, 통화, 구두점, 그림 이모티콘, 상자 그리기 또는 기타 문자와 같은 개념에 매핑하는 거대한 테이블입니다.

UTF-8 또는 US_ASCII와 같은 인코딩은 이러한 숫자의 일부 또는 전부를 일련의 바이트로 변환하여 일반적으로 32 비트로 저장되는 일련의 코드 포인트로 다시 디코딩 할 수 있습니다. 16에 맞지 않으며, 24 비트 또는 기타 등등을 의미있게 다루는 아키텍처는 없습니다.

UCS2 / UTF-16 을 수용하기 위해 유니 코드 사양 에는 0xD800에서 0xDFFF까지 의 문자 가 없으며 이는 의도적 인 것이며 절대 없을 것입니다.

이것은 UCS2와 UTF-16이 하나의 '트릭'으로 거의 동일하다는 것을 의미합니다.

65536 미만의 유니 코드 번호 (이론적으로 2 바이트에 맞을 수 있음)의 경우 UTF-16 인코딩 (이모 지 등을 인코딩 할 수 있음)의 경우 UTF-16 인코딩은 숫자입니다. 똑바로. 2 바이트로. D800-DFFF는 발생할 수 없습니다. 이러한 코드 포인트는 의도적으로 아무 것도 아니기 때문입니다.

65536 이상의 경우 에는 소위 대리 쌍을 생성하기 위해 D800에서 DFFF까지의 무료 블록이 사용됩니다. 두 번째 '문자'(2 바이트의 두 번째 블록)는 D800-DFFF 범위에 저장할 수있는 데이터의 11 비트와 결합하여 총 16 + 11 = 27 비트로 나머지를 처리하기에 충분합니다.

따라서 UTF-16은 모든 유니 코드 코드 포인트를 2 바이트 또는 4 바이트로 인코딩합니다.

용어로서의 UCS-2는 대부분 그 의미를 잃었습니다. 원래는 '문자'당 정확히 2 바이트를 의미하며 더 이상도 그 이하도 아니고 여전히 의미하지만 '문자'의 의미는 인식 할 수 없을 정도로 왜곡되었습니다. 2 자로 계산됩니다. 자바에서 시도해보세요-1이 x.length()아닌 2를 반환합니다. UCS-2의 다소 건전한 정의는 다음과 같습니다. 1 문자는 실제로 1 문자를 의미하고 각 문자는 2 바이트로 표시되며 맞지 않는 문자를 저장하려고하면 ( 대리 쌍이 될 것입니다), 음, 그것들은 인코딩 될 수 없기 때문에 충돌하거나 복제 할 수없는 문자 대신 자리 표시자를 적용합니다. 안타깝게도 UCS-2가 의미하는 바는 (항상) 아닙니다. 따라서이 작업을 적용하는 코드를 작성해야합니다 (바이트 길이가 정확히 2 * number가되도록 모든 대리 쌍을 삭제 / 대체로 교체해야 함). 코드 포인트) 우리 자신.

이 대리 쌍 항목은 Java char가 UCS2의 이상에 매우 가깝다는 사실을 기반으로 다른 전략을 제공합니다 (Java 스펙에 하드 코딩 된 16 비트 숫자라는 점에서). 모든 문자 (에서와 같이, 자바의 char) 및 폐기 아무것도 등이 c >= 0xD800 && c < 0xE000, 뿐만 아니라 바로 다음 문자 장미 제거합니다.