Java: Cách xác định mã hóa bộ ký tự chính xác của luồng


140

Với tham chiếu đến luồng sau: Ứng dụng Java: Không thể đọc tệp được mã hóa iso-8859-1 một cách chính xác

Cách tốt nhất để xác định theo cách lập trình mã hóa bộ ký tự chính xác của một đầu vào / tệp là gì?

Tôi đã thử sử dụng như sau:

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

Nhưng trên một tệp mà tôi biết được mã hóa bằng ISO8859_1, đoạn mã trên mang lại ASCII, không chính xác và không cho phép tôi hiển thị chính xác nội dung của tệp trở lại bàn điều khiển.


11
Eduard đã đúng, "Bạn không thể xác định mã hóa của luồng byte tùy ý". Tất cả các đề xuất khác cung cấp cho bạn cách (và thư viện) để đoán tốt nhất. Nhưng cuối cùng họ vẫn đoán.
Mihai Nita

9
Reader.getEncodingtrả về mã hóa mà trình đọc đã được thiết lập để sử dụng, trong trường hợp của bạn là mã hóa mặc định.
Karol S

Câu trả lời:


70

Tôi đã sử dụng thư viện này, tương tự như jchardet để phát hiện mã hóa trong Java: http://code.google.com.vn/p/juniversalchardet/


6
Tôi thấy rằng điều này chính xác hơn: jchardet.sourceforge.net (Tôi đã thử nghiệm trên các tài liệu ngôn ngữ Tây Âu được mã hóa theo ISO 8859-1, windows-1252, utf-8)
Joel

1
Juniversalchardet này không hoạt động. Nó cung cấp UTF-8 hầu hết thời gian, ngay cả khi tệp được mã hóa 100% windows-1212.
Não

1
Juniversalchardet hiện đã có trên GitHub .
deamon

Nó không phát hiện các cửa sổ Đông Âu-1250
Bernhard Döbler

Tôi đã thử đoạn mã sau để phát hiện trên tệp từ " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " nhưng nhận được null là bộ ký tự được phát hiện. UniversalDetector ud = new UniversalDetector (null); byte [] byte = FileUtils.readFileToByteArray (Tệp mới (tệp)); ud.handleData (byte, 0, byte.length); ud.dataEnd (); phát hiệnCharset = ud.getDetectedCharset ();
Rohit Verma

105

Bạn không thể xác định mã hóa của một luồng byte tùy ý. Đây là bản chất của mã hóa. Mã hóa có nghĩa là ánh xạ giữa một giá trị byte và biểu diễn của nó. Vì vậy, mọi mã hóa "có thể" là đúng.

Phương thức getEncoding () sẽ trả về mã hóa đã được thiết lập (đọc JavaDoc ) cho luồng. Nó sẽ không đoán được mã hóa cho bạn.

Một số luồng cho bạn biết mã hóa nào đã được sử dụng để tạo chúng: XML, HTML. Nhưng không phải là một luồng byte tùy ý.

Dù sao, bạn có thể cố gắng tự đoán một mã hóa nếu bạn phải. Mỗi ngôn ngữ có một tần số chung cho mỗi char. Trong tiếng Anh, char e xuất hiện rất thường xuyên nhưng ê sẽ xuất hiện rất rất hiếm khi. Trong luồng ISO-8859-1 thường không có ký tự 0x00. Nhưng một luồng UTF-16 có rất nhiều trong số họ.

Hoặc: bạn có thể hỏi người dùng. Tôi đã thấy các ứng dụng hiển thị cho bạn một đoạn của tệp theo các bảng mã khác nhau và yêu cầu bạn chọn "chính xác".


18
Điều này không thực sự trả lời câu hỏi. Các op lẽ nên sử dụng docs.codehaus.org/display/GUESSENC/Home hoặc icu-project.org/apiref/icu4j/com/ibm/icu/text/... hoặc jchardet.sourceforge.net
Christoffer Hammarström

23
Vậy làm thế nào để trình soạn thảo, notepad ++ của tôi biết cách mở tệp và hiển thị cho tôi các ký tự đúng?
mmm

12
@ Hamidam thật may mắn vì nó cho bạn thấy những nhân vật phù hợp. Khi nó đoán sai (và nó thường xảy ra), có một tùy chọn (Menu >> Mã hóa) cho phép bạn thay đổi mã hóa.
Pacerier

15
@Eduard: "Vì vậy, mọi mã hóa" có thể "là quyền." không hoàn toàn đúng. Nhiều mã hóa văn bản có một số mẫu không hợp lệ, đó là một cờ mà văn bản có thể không phải là mã hóa. Trong thực tế, với hai byte đầu tiên của một tệp, chỉ có 38% kết hợp là UTF8 hợp lệ. Tỷ lệ cược của 5 điểm mã đầu tiên là UTF8 hợp lệ tình cờ là ít hơn 0,7%. Tương tự như vậy, UTF16BE và LE thường dễ dàng được xác định bởi số lượng lớn byte không và vị trí của chúng.
Vịt Mooing

38

hãy xem điều này: http://site.icu-project.org/ (icu4j) họ có thư viện để phát hiện bộ ký tự từ IOStream có thể đơn giản như thế này:

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
tôi đã thử nhưng thất bại rất nhiều: tôi đã tạo 2 tệp văn bản trong nhật thực cả hai đều chứa "öäüß". Một bộ được mã hóa iso và một thành utf8 - cả hai đều được phát hiện là utf8! Vì vậy, tôi đã thử một tệp được lưu ở đâu đó trên hd (windows) của mình - tệp này được phát hiện chính xác ("windows-1252"). Sau đó, tôi đã tạo hai tệp mới trên hd một tệp được chỉnh sửa bằng trình chỉnh sửa tệp kia bằng notepad ++. trong cả hai trường hợp "Big5" (tiếng Trung) đã được phát hiện!
dermoritz

2
EDIT: Ok tôi nên kiểm tra cm.getConfidence () - với độ tin cậy "äöüß" ngắn của tôi là 10. Vì vậy, tôi phải quyết định sự tự tin nào là đủ tốt - nhưng điều đó hoàn toàn ổn cho nỗ lực này (phát hiện bộ ký tự)
dermoritz

1
Liên kết trực tiếp đến mã mẫu: userguide.icu-project.org/conversion/detection
james.garriss

27

Đây là mục yêu thích của tôi:

TikaEncodingDetector

Phụ thuộc:

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

Mẫu vật:

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

Đoán mã hóa

Phụ thuộc:

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

Mẫu vật:

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

2
Nota: TikaEncodingDetector 1.1 thực sự là một trình bao bọc mỏng xung quanh lớp ICU4J 3.4 CharsetDectector .
Stephan

Thật không may, cả hai lib đều không hoạt động. Trong một trường hợp, nó xác định tệp UTF-8 với Umlaute của Đức là ISO-8859-1 và US-ASCII.
Não

1
@Brain: Tệp đã được thử nghiệm của bạn có thực sự ở định dạng UTF-8 không và nó có bao gồm BOM ( en.wikipedia.org/wiki/Byte_order_mark ) không?
Benny Neugebauer

@BennyNeugebauer tệp là UTF-8 không có BOM. Tôi đã kiểm tra nó bằng Notepad ++, cũng bằng cách thay đổi mã hóa và khẳng định rằng "Umlaute" vẫn hiển thị.
Não

13

Bạn chắc chắn có thể xác nhận tệp cho một bộ ký tự cụ thể bằng cách giải mã nó với một CharsetDecodervà xem ra các lỗi "không đúng định dạng" hoặc "ký tự không thể chỉnh sửa". Tất nhiên, điều này chỉ cho bạn biết nếu một bộ ký tự sai; Nó không cho bạn biết nếu nó đúng. Đối với điều đó, bạn cần một cơ sở so sánh để đánh giá kết quả được giải mã, ví dụ bạn có biết trước nếu các ký tự được giới hạn trong một số tập hợp con, hoặc liệu văn bản có tuân thủ một số định dạng nghiêm ngặt không? Điểm mấu chốt là phát hiện bộ ký tự là phỏng đoán mà không có bất kỳ đảm bảo nào.


12

Sử dụng thư viện nào?

Theo văn bản này, chúng là ba thư viện xuất hiện:

Tôi không bao gồm Apache Any23 vì nó sử dụng ICU4j 3.4 dưới mui xe.

Làm thế nào để biết ai đã phát hiện bộ ký tự bên phải (hoặc càng gần càng tốt)?

Không thể xác nhận bộ ký tự được phát hiện bởi mỗi thư viện ở trên. Tuy nhiên, có thể lần lượt hỏi họ và cho điểm trả lời.

Làm thế nào để ghi điểm trả lời?

Mỗi phản hồi có thể được chỉ định một điểm. Phản hồi càng có nhiều điểm, bảng mã được phát hiện càng tự tin. Đây là một phương pháp tính điểm đơn giản. Bạn có thể xây dựng những người khác.

Có mã mẫu nào không?

Dưới đây là một đoạn đầy đủ thực hiện chiến lược được mô tả trong các dòng trước.

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() + " }";
}

Cải tiến: CácguessEncoding phương pháp đọc inputstream hoàn toàn. Đối với đầu vào lớn, điều này có thể là một mối quan tâm. Tất cả các thư viện sẽ đọc toàn bộ đầu vào. Điều này có nghĩa là tiêu thụ thời gian lớn để phát hiện bộ ký tự.

Có thể giới hạn tải dữ liệu ban đầu xuống một vài byte và chỉ thực hiện phát hiện bộ ký tự trên vài byte đó.


8

Các lib ở trên là các trình phát hiện BOM đơn giản, tất nhiên chỉ hoạt động nếu có BOM ở đầu tệp. Hãy xem http://jchardet.sourceforge.net/ để quét văn bản


18
chỉ là mẹo, nhưng không có "ở trên" trên trang web này - hãy xem xét việc nêu rõ các thư viện mà bạn đang đề cập đến.
McDowell

6

Theo tôi biết, không có thư viện chung trong bối cảnh này để phù hợp cho tất cả các loại vấn đề. Vì vậy, đối với mỗi vấn đề, bạn nên kiểm tra các thư viện hiện có và chọn một thư viện tốt nhất thỏa mãn các ràng buộc của vấn đề của bạn, nhưng thường thì không có vấn đề nào phù hợp. Trong những trường hợp này, bạn có thể viết Trình phát hiện mã hóa của riêng mình! Như tôi đã viết ...

Tôi đã viết một công cụ java java để phát hiện mã hóa bộ ký tự của các trang Web HTML, sử dụng IBM ICU4j và Mozilla JCharDet làm các thành phần tích hợp. Ở đây bạn có thể tìm thấy công cụ của tôi, xin vui lòng đọc phần README trước bất cứ điều gì khác. Ngoài ra, bạn có thể tìm thấy một số khái niệm cơ bản về vấn đề này trong bài viết của tôi và trong các tài liệu tham khảo.

Dưới đây tôi đã cung cấp một số ý kiến ​​hữu ích mà tôi đã trải nghiệm trong công việc của mình:

  • Phát hiện bộ ký tự không phải là một quá trình hoàn hảo, bởi vì về cơ bản nó dựa trên dữ liệu thống kê và những gì thực sự xảy ra là đoán không phát hiện ra
  • icu4j là công cụ chính trong bối cảnh này của IBM, imho
  • Cả TikaEncodingDetector và Lucene-ICU4j đều đang sử dụng icu4j và độ chính xác của chúng không có sự khác biệt có ý nghĩa so với icu4j trong các thử nghiệm của tôi (nhiều nhất là% 1, như tôi nhớ)
  • icu4j chung chung hơn nhiều so với jchardet, icu4j chỉ hơi thiên về mã hóa gia đình IBM trong khi jchardet thiên vị mạnh mẽ với utf-8
  • Do việc sử dụng rộng rãi UTF-8 trong thế giới HTML; Nhìn chung, jchardet là một lựa chọn tốt hơn icu4j, nhưng không phải là lựa chọn tốt nhất!
  • icu4j rất phù hợp với các bảng mã cụ thể ở Đông Á như EUC-KR, EUC-JP, SHIFT_JIS, BIG5 và mã hóa gia đình GB
  • Cả icu4j và jchardet đều thất bại trong việc xử lý các trang HTML với mã hóa Windows-1251 và Windows-1256. Windows-1251 aka cp1251 được sử dụng rộng rãi cho các ngôn ngữ dựa trên Cyrillic như tiếng Nga và Windows-1256 aka cp1256 được sử dụng rộng rãi cho tiếng Ả Rập
  • Hầu như tất cả các công cụ phát hiện mã hóa đều sử dụng các phương pháp thống kê, vì vậy độ chính xác của đầu ra phụ thuộc rất nhiều vào kích thước và nội dung của đầu vào
  • Một số mã hóa về cơ bản là giống nhau chỉ với một sự khác biệt một phần, vì vậy trong một số trường hợp, mã hóa được đoán hoặc được phát hiện có thể sai nhưng đồng thời là đúng! Về Windows-1252 và ISO-8859-1. (tham khảo đoạn cuối cùng trong phần 5.2 của bài viết của tôi)


5

Nếu bạn sử dụng ICU4J ( http://icu-project.org/apiref/icu4j/ )

Đây là mã của tôi:

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();
    }
}

Hãy nhớ đặt tất cả các thử bắt cần nó.

Tôi mong công việc này phù hợp với bạn.


IMO, câu trả lời này là hoàn hảo. Nếu bạn muốn sử dụng ICU4j, hãy thử cái này thay thế: stackoverflow.com/a/4013565/363573 .
Stephan


2

Đối với các tệp ISO8859_1, không có cách nào dễ dàng để phân biệt chúng với ASCII. Tuy nhiên, đối với các tệp Unicode, người ta thường có thể phát hiện điều này dựa trên một vài byte đầu tiên của tệp.

Các tệp UTF-8 và UTF-16 bao gồm Dấu thứ tự Byte (BOM) ở đầu tệp. BOM là một không gian không phá vỡ có chiều rộng bằng không.

Thật không may, vì lý do lịch sử, Java không tự động phát hiện điều này. Các chương trình như Notepad sẽ kiểm tra BOM và sử dụng mã hóa phù hợp. Sử dụng unix hoặc Cygwin, bạn có thể kiểm tra BOM bằng lệnh tập tin. Ví dụ:

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

Đối với Java, tôi khuyên bạn nên kiểm tra mã này, mã này sẽ phát hiện các định dạng tệp phổ biến và chọn mã hóa chính xác: Cách đọc tệp và tự động chỉ định mã hóa chính xác


15
Không phải tất cả các tệp UTF-8 hoặc UTF-16 đều có BOM, vì không bắt buộc và BOM UTF-8 không được khuyến khích.
Christoffer Hammarström

1

Một thay thế cho TikaEncodingDetector là sử dụng Tika AutoDetectReader .

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

Tike AutoDetectReader sử dụng EncodingDetector được tải với ServiceLoader. Những triển khai EncodingDetector nào bạn sử dụng?
Stephan

-1

Trong Java đơn giản:

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.");
    }
}

Cách tiếp cận này sẽ thử mã hóa từng cái một cho đến khi một cái hoạt động hoặc chúng ta hết chúng. (BTW danh sách mã hóa của tôi chỉ có các mục đó vì chúng là các cài đặt bộ ký tự được yêu cầu trên mọi nền tảng Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Nhưng ISO-8859-1 (trong số nhiều thứ khác mà bạn chưa liệt kê) sẽ luôn thành công. Và, tất nhiên, đây chỉ là phỏng đoán, không thể phục hồi siêu dữ liệu bị mất, điều cần thiết cho giao tiếp tệp văn bản.
Tom Blodget

Xin chào @TomBlodget, bạn có gợi ý rằng thứ tự mã hóa phải khác nhau không?
Andres

3
Tôi nói rằng nhiều người sẽ "làm việc" nhưng chỉ có một là "đúng". Và bạn không cần phải kiểm tra ISO-8859-1 vì nó sẽ luôn "hoạt động".
Tom Blodget

-12

Bạn có thể chọn bộ char thích hợp trong Trình xây dựng không :

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

8
Vấn đề ở đây là để xem liệu bộ ký tự có thể được xác định theo chương trình hay không.
Joel

1
Không, nó sẽ không đoán nó cho bạn. Bạn phải cung cấp nó.
Kevin

1
Có thể có một phương pháp heuristic, như được đề xuất bởi một số câu trả lời ở đây stackoverflow.com/questions/457655/java-charset-and-windows/ Lỗi
Joel
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.