Java : 스트림의 올바른 문자 세트 인코딩을 결정하는 방법


140

다음 스레드를 참조하십시오. Java App : ISO-8859-1 인코딩 파일을 올바르게 읽을 수 없습니다

입력 스트림 / 파일의 올바른 문자 세트 인코딩을 프로그래밍 방식으로 결정하는 가장 좋은 방법은 무엇입니까?

나는 다음을 사용하려고 시도했다.

File in =  new File(args[0]);
InputStreamReader r = new InputStreamReader(new FileInputStream(in));
System.out.println(r.getEncoding());

그러나 ISO8859_1로 인코딩 된 파일에서 위의 코드는 ASCII를 생성하며 올바르지 않습니다. 파일의 내용을 콘솔에 올바르게 렌더링 할 수 없습니다.


11
Eduard는 "임의의 바이트 스트림의 인코딩을 결정할 수 없습니다"라는 말이 맞습니다. 다른 모든 제안은 최상의 추측을 수행 할 수있는 방법 (및 라이브러리)을 제공합니다. 그러나 결국 그들은 여전히 ​​추측입니다.
Mihai Nita

9
Reader.getEncoding리더가 사용하도록 설정된 인코딩을 반환합니다.이 경우 기본 인코딩입니다.
Karol S

답변:


70

Java에서 인코딩을 감지하기 위해 jchardet과 유사한이 라이브러리를 사용했습니다 : http://code.google.com/p/juniversalchardet/


6
나는 이것이 더 정확한 것을 발견했다 : jchardet.sourceforge.net를 (내가 창-1252 UTF-8 ISO 8859-1로 인코딩 서유럽 언어 문서에서 테스트되었다)
조엘

1
이 juniversalchardet이 작동하지 않습니다. 파일이 100 % windows-1212로 인코딩 된 경우에도 UTF-8을 대부분 제공합니다.
Brain

1
juniversalchardet은 이제 GitHub에 있습니다 .
deamon

동유럽의 창문
-1250을

" cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt "의 파일에서 탐지하기 위해 다음 코드 스 니펫을 시도했지만 감지 된 문자 세트로 null을 얻었습니다. UniversalDetector ud = 새로운 UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (새 파일 (파일)); ud.handleData (바이트, 0, 바이트. 길이); ud.dataEnd (); detectedCharset = ud.getDetectedCharset ();
Rohit Verma

105

임의 바이트 스트림의 인코딩을 결정할 수 없습니다. 이것이 인코딩의 특성입니다. 인코딩은 바이트 값과 해당 표현 간의 매핑을 의미합니다. 따라서 모든 인코딩은 "올바른"것이 될 수 있습니다.

GetEncoding이 () 메소드 (판독 세워졌다 부호화 반환 javadoc는 스트림을 위해). 인코딩을 추측하지 않습니다.

일부 스트림은이를 생성하는 데 사용 된 인코딩 (XML, HTML)을 알려줍니다. 그러나 임의의 바이트 스트림은 아닙니다.

어쨌든 필요한 경우 직접 인코딩을 추측 할 수 있습니다. 모든 언어는 모든 문자마다 공통된 빈도를 갖습니다. 영어에서는 문자 e가 매우 자주 나타나지만 ê는 거의 나타나지 않습니다. ISO-8859-1 스트림에는 일반적으로 0x00 문자가 없습니다. 그러나 UTF-16 스트림에는 많은 것이 있습니다.

또는 : 사용자에게 요청할 수 있습니다. 이미 다른 인코딩으로 파일 스 니펫을 제공하는 애플리케이션을 보았으며 "올바른"것을 선택하도록 요청했습니다.


18
이것은 실제로 질문에 대답하지 않습니다. op는 아마도 docs.codehaus.org/display/GUESSENC/Home 또는 icu-project.org/apiref/icu4j/com/ibm/icu/text/… 또는 jchardet.sourceforge.net을
Christoffer Hammarström

23
그렇다면 메모장 + + 편집기는 파일을 열고 올바른 문자를 표시하는 방법을 어떻게 알 수 있습니까?
mmm

12
@Hamidam 그것은 운 좋게도 당신에게 올바른 캐릭터를 보여줍니다. 잘못 추측했을 때 (종종 그렇지 않은 경우) 인코딩을 변경할 수있는 옵션 (메뉴 >> 인코딩)이 있습니다.
Pacerier

15
@Eduard : "따라서 모든 인코딩은"올바르다 ". 별로 요 많은 텍스트 인코딩에는 유효하지 않은 여러 패턴이 있는데, 이는 텍스트가 해당 인코딩 이 아닐 수 있음을 나타내는 플래그입니다 . 실제로 파일의 처음 2 바이트를 고려하면 조합의 38 % 만 유효한 UTF8입니다. 우연히 유효한 UTF8 인 첫 5 개의 코드 포인트의 확률은 .77 % 미만입니다. 마찬가지로 UTF16BE 및 LE는 일반적으로 많은 수의 0 바이트와 위치에 의해 쉽게 식별됩니다.
Mooing Duck

38

이것을 확인하십시오 : http://site.icu-project.org/ (icu4j) IOStream에서 문자 세트를 감지하는 라이브러리가 다음과 같이 간단 할 수 있습니다.

BufferedInputStream bis = new BufferedInputStream(input);
CharsetDetector cd = new CharsetDetector();
cd.setText(bis);
CharsetMatch cm = cd.detect();

if (cm != null) {
   reader = cm.getReader();
   charset = cm.getName();
}else {
   throw new UnsupportedCharsetException()
}

2
나는 시도했지만 크게 실패 : 나는 "öäüß"를 포함하는 이클립스에 두 개의 텍스트 파일을 만들었습니다. 하나는 iso 인코딩으로, 하나는 utf8로 설정됩니다. 둘 다 utf8로 감지됩니다! 그래서 나는 내 HD 어딘가에 안전한 파일을 시도했다. 그런 다음 hd로 하나는 편집기로 편집하고 다른 하나는 notepad ++로 편집하여 두 개의 새 파일을 만들었습니다. 두 경우 모두 "Big5"(중국어)가 감지되었습니다!
dermoritz

2
편집 : 좋아, cm.getConfidence () 확인해야합니다-내 짧은 "äöüß"와 함께 자신감은 10입니다. 그래서 나는 어떤 자신감이 충분한 지 결정해야하지만-이 노력에 대해 절대적으로 괜찮습니다 (문자셋 감지)
dermoritz


27

내가 가장 좋아하는 것은 다음과 같습니다.

TikaEncodingDetector

의존:

<dependency>
  <groupId>org.apache.any23</groupId>
  <artifactId>apache-any23-encoding</artifactId>
  <version>1.1</version>
</dependency>

견본:

public static Charset guessCharset(InputStream is) throws IOException {
  return Charset.forName(new TikaEncodingDetector().guessEncoding(is));    
}

추측 인코딩

의존:

<dependency>
  <groupId>org.codehaus.guessencoding</groupId>
  <artifactId>guessencoding</artifactId>
  <version>1.4</version>
  <type>jar</type>
</dependency>

견본:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }

2
참고 : TikaEncodingDetector 1.1 은 실제로 ICU4J 3.4 CharsetDectector 클래스 의 얇은 래퍼 입니다.
Stephan

불행히도 두 라이브러리 모두 작동하지 않습니다. 어떤 경우에는 독일어 Umlaute가 ISO-8859-1 및 US-ASCII 인 UTF-8 파일을 식별합니다.
Brain

1
@Brain : 테스트 한 파일이 실제로 UTF-8 형식이며 BOM ( en.wikipedia.org/wiki/Byte_order_mark )이 포함되어 있습니까?
Benny Neugebauer

@BennyNeugebauer 파일은 BOM이없는 UTF-8입니다. 인코딩을 변경하고 "Umlaute"가 여전히 표시되는지 확인하면서 Notepad ++로 확인했습니다.
Brain

13

파일을 디코딩 하고 "잘못된 입력"또는 "매핑 불가능한 문자"오류를 관찰 하여 특정 문자 세트에 대해 파일의 유효성 을 확실히 검증 할 수 있습니다 . 물론, 이것은 문자셋이 잘못되었는지 알려줍니다. 그것이 정확한지 알려주지 않습니다. 이를 위해서는 디코딩 된 결과를 평가하기위한 비교 기준이 필요합니다. 예를 들어 문자가 일부 하위 세트로 제한되는지 또는 텍스트가 엄격한 형식을 준수하는지 여부를 미리 알고 있습니까? 결론은 문자셋 탐지가 보장없이 추측 할 수 있다는 것입니다.CharsetDecoder


12

어떤 라이브러리를 사용해야합니까?

이 글을 쓰는 시점에서 다음과 같은 세 가지 라이브러리가 있습니다.

Apache Any23 은 ICU4j 3.4를 사용하기 때문에 포함하지 않습니다 .

어느 것이 올바른 문자 세트를 감지했는지 (또는 가능한 한 가깝게) 말하는지?

위의 각 라이브러리에서 감지 한 문자 세트를 인증하는 것은 불가능합니다. 그러나 차례로 요청하고 반환 된 응답의 점수를 매길 수 있습니다.

반환 된 응답의 점수를 매기는 방법?

각 응답에는 한 지점이 할당 될 수 있습니다. 응답이 많을수록 탐지 된 문자 집합의 신뢰도가 높아집니다. 이것은 간단한 채점 방법입니다. 다른 사람들을 정교하게 만들 수 있습니다.

샘플 코드가 있습니까?

다음은 이전 행에서 설명한 전략을 구현하는 전체 스 니펫입니다.

public static String guessEncoding(InputStream input) throws IOException {
    // Load input data
    long count = 0;
    int n = 0, EOF = -1;
    byte[] buffer = new byte[4096];
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    while ((EOF != (n = input.read(buffer))) && (count <= Integer.MAX_VALUE)) {
        output.write(buffer, 0, n);
        count += n;
    }
    
    if (count > Integer.MAX_VALUE) {
        throw new RuntimeException("Inputstream too large.");
    }

    byte[] data = output.toByteArray();

    // Detect encoding
    Map<String, int[]> encodingsScores = new HashMap<>();

    // * GuessEncoding
    updateEncodingsScores(encodingsScores, new CharsetToolkit(data).guessEncoding().displayName());

    // * ICU4j
    CharsetDetector charsetDetector = new CharsetDetector();
    charsetDetector.setText(data);
    charsetDetector.enableInputFilter(true);
    CharsetMatch cm = charsetDetector.detect();
    if (cm != null) {
        updateEncodingsScores(encodingsScores, cm.getName());
    }

    // * juniversalchardset
    UniversalDetector universalDetector = new UniversalDetector(null);
    universalDetector.handleData(data, 0, data.length);
    universalDetector.dataEnd();
    String encodingName = universalDetector.getDetectedCharset();
    if (encodingName != null) {
        updateEncodingsScores(encodingsScores, encodingName);
    }

    // Find winning encoding
    Map.Entry<String, int[]> maxEntry = null;
    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        if (maxEntry == null || (e.getValue()[0] > maxEntry.getValue()[0])) {
            maxEntry = e;
        }
    }

    String winningEncoding = maxEntry.getKey();
    //dumpEncodingsScores(encodingsScores);
    return winningEncoding;
}

private static void updateEncodingsScores(Map<String, int[]> encodingsScores, String encoding) {
    String encodingName = encoding.toLowerCase();
    int[] encodingScore = encodingsScores.get(encodingName);

    if (encodingScore == null) {
        encodingsScores.put(encodingName, new int[] { 1 });
    } else {
        encodingScore[0]++;
    }
}    

private static void dumpEncodingsScores(Map<String, int[]> encodingsScores) {
    System.out.println(toString(encodingsScores));
}

private static String toString(Map<String, int[]> encodingsScores) {
    String GLUE = ", ";
    StringBuilder sb = new StringBuilder();

    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        sb.append(e.getKey() + ":" + e.getValue()[0] + GLUE);
    }
    int len = sb.length();
    sb.delete(len - GLUE.length(), len);

    return "{ " + sb.toString() + " }";
}

개선 :guessEncoding방법은 입력 스트림을 완전히 읽습니다. 큰 입력 스트림의 경우 이것이 문제가 될 수 있습니다. 이 모든 라이브러리는 전체 입력 스트림을 읽습니다. 이는 문자셋을 탐지하는 데 많은 시간이 소요됨을 의미합니다.

초기 데이터로드를 몇 바이트로 제한하고 그 몇 바이트에서만 문자 세트 감지를 수행 할 수 있습니다.


8

위의 libs는 파일의 시작 부분에 BOM이있는 경우에만 작동하는 간단한 BOM 검출기입니다. 텍스트를 스캔하는 http://jchardet.sourceforge.net/ 을 살펴보십시오.


18
팁은 있지만이 사이트에는 "위"가 없습니다. 참조하는 라이브러리를 언급하십시오.
McDowell

6

내가 아는 한,이 문맥에는 모든 유형의 문제에 적합한 일반적인 라이브러리가 없습니다. 따라서 각 문제점에 대해 기존 라이브러리를 테스트하고 문제점의 제한 조건을 만족시키는 최상의 라이브러리를 선택해야하지만, 그 중 어느 것도 적절하지 않은 경우가 있습니다. 이 경우 자신의 인코딩 감지기를 작성할 수 있습니다! 내가 쓴대로 ...

IBM ICU4j 및 Mozilla JCharDet을 내장 구성 요소로 사용하여 HTML 웹 페이지의 문자 세트 인코딩을 감지하는 메타 Java 도구를 작성했습니다. 여기에 당신이 내 도구를 찾을 수 있습니다, 다른 어떤 전에 README 섹션을 참조하십시오. 또한 내 논문 과 그 참고 문헌 에서이 문제의 기본 개념을 찾을 수 있습니다 .

Bellow 나는 내가 일하면서 경험 한 몇 가지 유용한 의견을 제시했다.

  • 그것은 본질적으로 통계 데이터를 기반으로 어떤 실제로 발생하기 때문에 문자셋 검출은, 완벽한 프로세스없는 추측 하지 검출
  • icu4j는 이러한 맥락에서 IBM의 주요 툴입니다.
  • TikaEncodingDetector와 Lucene-ICU4j는 모두 icu4j를 사용하고 있으며 정확도는 내 테스트에서 icu4j와 유의미한 차이가 없었습니다 (기억할 때 최대 % 1).
  • icu4j는 jchardet보다 훨씬 일반적이며, icu4j는 IBM 계열 인코딩에 약간 편향되어 있지만 jchardet은 utf-8에 강하게 편향되어 있습니다
  • HTML 세계에서 UTF-8이 널리 사용되기 때문에; jchardet은 전체적으로 icu4j보다 나은 선택이지만 최선의 선택은 아닙니다!
  • icu4j는 EUC-KR, EUC-JP, SHIFT_JIS, BIG5 및 GB 제품군 인코딩과 같은 동아시아 특정 인코딩에 적합합니다.
  • icu4j와 jchardet은 모두 Windows-1251 및 Windows-1256 인코딩을 사용하는 HTML 페이지를 처리하는 데 어려움을 겪고 있습니다. Windows-1251 일명 cp1251은 러시아어와 같은 키릴 기반 언어에 널리 사용되며 Windows-1256 일명 cp1256은 아랍어에 널리 사용됩니다
  • 거의 모든 인코딩 탐지 도구는 통계적 방법을 사용하므로 출력의 정확도는 입력의 크기와 내용에 크게 좌우됩니다
  • 일부 인코딩은 본질적으로 부분 차이만으로 동일하므로 일부 경우 추측되거나 감지 된 인코딩은 거짓이지만 동시에 사실 일 수 있습니다! Windows-1252 및 ISO-8859-1에 관해서. (내 논문의 5.2 섹션 아래 마지막 단락 참조)


5

ICU4J를 사용하는 경우 ( http://icu-project.org/apiref/icu4j/ )

내 코드는 다음과 같습니다.

String charset = "ISO-8859-1"; //Default chartset, put whatever you want

byte[] fileContent = null;
FileInputStream fin = null;

//create FileInputStream object
fin = new FileInputStream(file.getPath());

/*
 * Create byte array large enough to hold the content of the file.
 * Use File.length to determine size of the file in bytes.
 */
fileContent = new byte[(int) file.length()];

/*
 * To read content of the file in byte array, use
 * int read(byte[] byteArray) method of java FileInputStream class.
 *
 */
fin.read(fileContent);

byte[] data =  fileContent;

CharsetDetector detector = new CharsetDetector();
detector.setText(data);

CharsetMatch cm = detector.detect();

if (cm != null) {
    int confidence = cm.getConfidence();
    System.out.println("Encoding: " + cm.getName() + " - Confidence: " + confidence + "%");
    //Here you have the encode name and the confidence
    //In my case if the confidence is > 50 I return the encode, else I return the default value
    if (confidence > 50) {
        charset = cm.getName();
    }
}

모든 try-catch에 필요합니다.

나는 이것이 당신을 위해 작동하기를 바랍니다.


IMO,이 답변은 완벽합니다. ICU4j를 사용하려면 stackoverflow.com/a/4013565/363573 대신 대신 사용하십시오 .
Stephan


2

ISO8859_1 파일의 경우 ASCII와 쉽게 구별 할 수있는 방법이 없습니다. 그러나 유니 코드 파일의 경우 일반적으로 파일의 처음 몇 바이트를 기준으로이를 감지 할 수 있습니다.

UTF-8 및 UTF-16 파일은 파일 의 맨 처음에 BOM ( Byte Order Mark )을 포함합니다. BOM은 너비가 0이 아닌 비 분리 공간입니다.

불행히도, 역사적 이유로 Java는 이것을 자동으로 감지하지 않습니다. 메모장과 같은 프로그램은 BOM을 확인하고 적절한 인코딩을 사용합니다. 유닉스 또는 Cygwin을 사용하여 파일 명령으로 BOM을 확인할 수 있습니다. 예를 들면 다음과 같습니다.

$ file sample2.sql 
sample2.sql: Unicode text, UTF-16, big-endian

Java의 경우이 코드를 확인하여 공통 파일 형식을 감지하고 올바른 인코딩을 선택하십시오 . 파일을 읽고 올바른 인코딩을 자동으로 지정하는 방법


15
모든 UTF-8 또는 UTF-16 파일에 BOM이있는 것은 아니며, 필요하지 않으므로 UTF-8 BOM은 사용하지 않는 것이 좋습니다.
Christoffer Hammarström

1

TikaEncodingDetector의 대안은 Tika AutoDetectReader 를 사용하는 것 입니다.

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();

Tike AutoDetectReader는 ServiceLoader와 함께로드 된 EncodingDetector를 사용합니다. 어떤 EncodingDetector 구현을 사용하십니까?
Stephan

-1

평범한 자바에서 :

final String[] encodings = { "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-16" };

List<String> lines;

for (String encoding : encodings) {
    try {
        lines = Files.readAllLines(path, Charset.forName(encoding));
        for (String line : lines) {
            // do something...
        }
        break;
    } catch (IOException ioe) {
        System.out.println(encoding + " failed, trying next.");
    }
}

이 접근법은 하나가 작동하거나 인코딩이 끝날 때까지 하나씩 인코딩을 시도합니다. (BTW 내 인코딩 목록에는 모든 Java 플랫폼 ( https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html ) 에 필요한 문자 세트 구현이므로 해당 항목 만 있습니다 )


그러나 ISO-8859-1 (당신이 나열하지 않은 다른 많은 것들 중에서)은 항상 성공할 것입니다. 물론 이것은 단지 추측 일 뿐이며, 텍스트 파일 통신에 필수적인 손실 된 메타 데이터는 복구 할 수 없습니다.
Tom Blodget

안녕하세요 @TomBlodget, 인코딩 순서가 달라야한다고 제안하고 있습니까?
Andres

3
나는 많은 사람들이 "일할 것"이라고 말하지만 오직 하나만이 "옳다"고 말합니다. ISO-8859-1은 항상 "작동"하기 때문에 테스트 할 필요가 없습니다.
Tom Blodget

-12

당신은에서 해당 문자 세트를 선택할 수 생성자 :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");

8
여기서 요점은 문자셋이 프로그래밍 방식으로 결정될 수 있는지 확인하는 것이 었습니다.
Joel

1
아니, 당신을 위해 그것을 추측하지 않습니다. 당신은 그것을 제공해야합니다.
Kevin

1
여기에 대한 답변 중 일부에서 제안한 휴리스틱 방법이있을 수 있습니다. stackoverflow.com/questions/457655/java-charset-and-windows/…
Joel
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.