Порядок следования байтов затрудняет чтение файлов в Java - PullRequest
102 голосов
/ 02 декабря 2009

Я пытаюсь читать файлы CSV, используя Java. Некоторые из файлов могут иметь метку порядка байтов в начале, но не все. При наличии порядок байтов читается вместе с остальной частью первой строки, что вызывает проблемы со сравнением строк.

Есть ли простой способ пропустить метку порядка байтов, когда она присутствует?

Спасибо!

Ответы [ 8 ]

112 голосов
/ 02 декабря 2009

РЕДАКТИРОВАТЬ : Я сделал правильный релиз на GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Вот класс, который я кодировал некоторое время назад, я просто отредактировал имя пакета перед вставкой. Ничего особенного, это очень похоже на решения, опубликованные в базе данных ошибок SUN. Включите его в свой код, и все в порядке.

<code>/* ____________________________________________________________________________
 * 
 * File:    UnicodeBOMInputStream.java
 * Author:  Gregory Pakosz.
 * Date:    02 - November - 2005    
 * ____________________________________________________________________________
 */
package com.stackoverflow.answer;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

/**
 * The <code>UnicodeBOMInputStream</code> class wraps any
 * <code>InputStream</code> and detects the presence of any Unicode BOM
 * (Byte Order Mark) at its beginning, as defined by
 * <a href="http://www.faqs.org/rfcs/rfc3629.html">RFC 3629 - UTF-8, a transformation format of ISO 10646</a>
 * 
 * <p>The
 * <a href="http://www.unicode.org/unicode/faq/utf_bom.html">Unicode FAQ</a>
 * defines 5 types of BOMs:<ul>
 * <li><pre>00 00 FE FF  = UTF-32, big-endian
*
FF FE 00 00  = UTF-32, little-endian
*
FE FF        = UTF-16, big-endian
*
FF FE        = UTF-16, little-endian
*
EF BB BF     = UTF-8
* * *

Используйте метод {@link #getBOM ()}, чтобы узнать, была ли обнаружена спецификация * или нет. *

*

Используйте метод {@link #skipBOM ()} для удаления обнаруженной спецификации из * завернутый InputStream объект.

* / открытый класс UnicodeBOMInputStream расширяет InputStream { / ** * Введите безопасный класс перечисления, который описывает различные типы Unicode * Спецификации. * / публичная статическая конечная спецификация класса { / ** * НИКТО. * / public static final BOM NONE = новая BOM (новый байт [] {}, "NONE"); / ** * UTF-8 BOM (EF BB BF). * / публичная статическая конечная спецификация UTF_8 = новая спецификация (новый байт [] {(байт) 0xEF, (Байт) 0xBB, (Байт) 0xBF}, "UTF-8"); / ** * UTF-16, little-endian (FF FE). * / открытая статическая конечная спецификация UTF_16_LE = новая спецификация (новый байт [] {(байт) 0xFF, (Байт) 0xFE}, «UTF-16 little-endian»); / ** * UTF-16, big-endian (FE FF). * / публичная статическая конечная спецификация UTF_16_BE = новая спецификация (новый байт [] {(байт) 0xFE, (Байт) 0xFF}, «UTF-16 big-endian»); / ** * UTF-32 с прямым порядком байтов (FF FE 00 00). * / открытая статическая конечная спецификация UTF_32_LE = новая спецификация (новый байт [] {(байт) 0xFF, (Байт) 0xFE, (Байт) 0x00, (Байт) 0x00}, «UTF-32 little-endian»); / ** * UTF-32, big-endian (00 00 FE FF). * / открытая статическая конечная спецификация UTF_32_BE = новая спецификация (новый байт [] {(байт) 0x00, (Байт) 0x00, (Байт) 0xFE, (Байт) 0xFF}, «UTF-32 big-endian»); / ** * Возвращает String представление этого BOM * значение. * / public final String toString () { вернуть описание; } / ** * Возвращает байты, соответствующие этому BOM значению. * / открытый финальный байт [] getBytes () { final int length = bytes.length; конечный байт [] результат = новый байт [длина]; // Сделать защитную копию System.arraycopy (байты, 0, результат, 0, длина); вернуть результат; } частная спецификация (финальный байт [], финальное описание строки) { assert (bom! = null): "недопустимая спецификация: null не разрешен"; assert (description! = null): "неверное описание: null не разрешен"; assert (description.length ()! = 0): "неверное описание: пустая строка не допускается"; this.bytes = bom; this.description = description; } последние байты []; личное финальное описание строки; } // Спецификация / ** * Создает новый UnicodeBOMInputStream, который оборачивает * указано InputStream. * * @param inputStream an InputStream. * * @throws NullPointerException, когда inputStream * null. * @ выбрасывает IOException при чтении из указанного InputStream * при попытке обнаружить спецификацию Unicode. * / public UnicodeBOMInputStream (final InputStream inputStream) генерирует исключение NullPointerException,IOException { if (inputStream == null) бросить новое исключение NullPointerException («неверный поток ввода: ноль не разрешен»); in = new PushbackInputStream (inputStream, 4); последний байт bom [] = новый байт [4]; final int read = in.read (bom); Переключатель (чтение) { дело 4: if ((bom [0] == (byte) 0xFF) && (bom [1] == (byte) 0xFE) && (bom [2] == (byte) 0x00) && (bom [3] == (byte) 0x00)) { this.bom = BOM.UTF_32_LE; перерыв; } еще if ((bom [0] == (byte) 0x00) && (bom [1] == (byte) 0x00) && (bom [2] == (byte) 0xFE) && (bom [3] == (byte) 0xFF)) { this.bom = BOM.UTF_32_BE; перерыв; } случай 3: if ((bom [0] == (byte) 0xEF) && (bom [1] == (byte) 0xBB) && (bom [2] == (byte) 0xBF)) { this.bom = BOM.UTF_8; перерыв; } случай 2: if ((bom [0] == (byte) 0xFF) && (bom [1] == (byte) 0xFE)) { this.bom = BOM.UTF_16_LE; перерыв; } еще if ((bom [0] == (byte) 0xFE) && (bom [1] == (byte) 0xFF)) { this.bom = BOM.UTF_16_BE; перерыв; } дефолт: this.bom = BOM.NONE; перерыв; } если (читать> 0) in.unread (бом, 0, чтение); } / ** * Возвращает BOM, который был обнаружен в упаковке * InputStream объект. * * @ вернуть значение BOM. * / публичная финальная спецификация getBOM () { // Тип спецификации неизменен. вернуть бомбу; } / ** * Пропускает BOM, который был найден в упаковке * InputStream объект. * * @ вернуть это UnicodeBOMInputStream. * * @ выбрасывает IOException при попытке пропустить спецификацию из упакованного * InputStream объект. * / публичная финальная синхронизированная UnicodeBOMInputStream skipBOM () выдает IOException { если (! пропущено) { in.skip (bom.bytes.length); пропущено = верно; } верни это; } / ** * {@inheritDoc} * / public int read () выбрасывает IOException { вернуть in.read (); } / ** * {@inheritDoc} * / public int read (последний байт b []) выдает IOException, Исключение нулевого указателя { вернуть in.read (b, 0, b.length); } / ** * {@inheritDoc} * / public int read (последний байт b [], окончательный int off, final int len) выдает IOException, Исключение нулевого указателя { вернуть in.read (b, off, len); } / ** * {@inheritDoc} * / public long skip (final long n) выдает IOException { вернуть in.skip (n); } / ** * {@inheritDoc} * / public int available () выдает IOException { вернуть in.available (); } / ** * {@inheritDoc} * / public void close () выбрасывает IOException { in.close (); } / ** * {@inheritDoc} * / общедоступный синхронизированный знак пустоты { in.mark (readlimit); } / ** * {@inheritDoc} * / public синхронизированный void reset () выбрасывает IOException { in.reset (); } / ** * {@inheritDoc} * / public boolean markSupported () { return in.markSupported (); } закрытый финал PushbackInputStream in; закрытый финал BOM bom; приватный логический пропуск = false; } // UnicodeBOMInputStream

И вы используете это так:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;

public final class UnicodeBOMInputStreamUsage
{
  public static void main(final String[] args) throws Exception
  {
    FileInputStream fis = new FileInputStream("test/offending_bom.txt");
    UnicodeBOMInputStream ubis = new UnicodeBOMInputStream(fis);

    System.out.println("detected BOM: " + ubis.getBOM());

    System.out.print("Reading the content of the file without skipping the BOM: ");
    InputStreamReader isr = new InputStreamReader(ubis);
    BufferedReader br = new BufferedReader(isr);

    System.out.println(br.readLine());

    br.close();
    isr.close();
    ubis.close();
    fis.close();

    fis = new FileInputStream("test/offending_bom.txt");
    ubis = new UnicodeBOMInputStream(fis);
    isr = new InputStreamReader(ubis);
    br = new BufferedReader(isr);

    ubis.skipBOM();

    System.out.print("Reading the content of the file after skipping the BOM: ");
    System.out.println(br.readLine());

    br.close();
    isr.close();
    ubis.close();
    fis.close();
  }

} // UnicodeBOMInputStreamUsage
87 голосов
/ 12 сентября 2011

Библиотека Apache Commons IO имеет InputStream, которая может обнаруживать и отбрасывать спецификации: BOMInputStream (javadoc) :

BOMInputStream bomIn = new BOMInputStream(in);
int firstNonBOMByte = bomIn.read(); // Skips BOM
if (bomIn.hasBOM()) {
    // has a UTF-8 BOM
}

Если вам также необходимо обнаружить разные кодировки, он также может различать различные метки порядка байтов, например UTF-8 против UTF-16 big + little endian - подробности в ссылке на документ выше. Затем вы можете использовать обнаруженный ByteOrderMark, чтобы выбрать Charset для декодирования потока. (Вероятно, есть более упрощенный способ сделать это, если вам нужны все эти функции - может быть, UnicodeReader в ответе BalusC?). Обратите внимание, что в общем случае не очень хороший способ определить, в какой кодировке находятся некоторые байты, но если поток начинается с спецификации, очевидно, это может быть полезно.

Редактировать : Если вам нужно определить спецификацию в UTF-16, UTF-32 и т. Д., Тогда конструктор должен быть:

new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE,
        ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE)

Комментарий Upvote @ martin-charlesworth:)

29 голосов
/ 16 августа 2013

Более простое решение:

public class BOMSkipper
{
    public static void skip(Reader reader) throws IOException
    {
        reader.mark(1);
        char[] possibleBOM = new char[1];
        reader.read(possibleBOM);

        if (possibleBOM[0] != '\ufeff')
        {
            reader.reset();
        }
    }
}

Образец использования:

BufferedReader input = new BufferedReader(new InputStreamReader(new FileInputStream(file), fileExpectedCharset));
BOMSkipper.skip(input);
//Now UTF prefix not present:
input.readLine();
...

Работает со всеми 5 кодировками UTF!

24 голосов
/ 02 декабря 2009

API данных Google имеет UnicodeReader, который автоматически определяет кодировку.

Вы можете использовать его вместо InputStreamReader. Вот немного сжатый фрагмент его источника, который довольно прост:

public class UnicodeReader extends Reader {
    private static final int BOM_SIZE = 4;
    private final InputStreamReader reader;

    /**
     * Construct UnicodeReader
     * @param in Input stream.
     * @param defaultEncoding Default encoding to be used if BOM is not found,
     * or <code>null</code> to use system default encoding.
     * @throws IOException If an I/O error occurs.
     */
    public UnicodeReader(InputStream in, String defaultEncoding) throws IOException {
        byte bom[] = new byte[BOM_SIZE];
        String encoding;
        int unread;
        PushbackInputStream pushbackStream = new PushbackInputStream(in, BOM_SIZE);
        int n = pushbackStream.read(bom, 0, bom.length);

        // Read ahead four bytes and check for BOM marks.
        if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) {
            encoding = "UTF-8";
            unread = n - 3;
        } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) {
            encoding = "UTF-16BE";
            unread = n - 2;
        } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) {
            encoding = "UTF-16LE";
            unread = n - 2;
        } else if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) {
            encoding = "UTF-32BE";
            unread = n - 4;
        } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) {
            encoding = "UTF-32LE";
            unread = n - 4;
        } else {
            encoding = defaultEncoding;
            unread = n;
        }

        // Unread bytes if necessary and skip BOM marks.
        if (unread > 0) {
            pushbackStream.unread(bom, (n - unread), unread);
        } else if (unread < -1) {
            pushbackStream.unread(bom, 0, 0);
        }

        // Use given encoding.
        if (encoding == null) {
            reader = new InputStreamReader(pushbackStream);
        } else {
            reader = new InputStreamReader(pushbackStream, encoding);
        }
    }

    public String getEncoding() {
        return reader.getEncoding();
    }

    public int read(char[] cbuf, int off, int len) throws IOException {
        return reader.read(cbuf, off, len);
    }

    public void close() throws IOException {
        reader.close();
    }
}
13 голосов
/ 24 сентября 2013

Библиотека Apache Commons IO BOMInputStream уже упоминалась @rescdsk, но я не видел упоминания о том, как получить InputStream без спецификации.

Вот как я это сделал в Scala.

 import java.io._
 val file = new File(path_to_xml_file_with_BOM)
 val fileInpStream = new FileInputStream(file)   
 val bomIn = new BOMInputStream(fileInpStream, 
         false); // false means don't include BOM
4 голосов
/ 26 ноября 2015

Чтобы просто удалить символы спецификации из вашего файла, я рекомендую использовать Apache Common IO

public BOMInputStream(InputStream delegate,
              boolean include)
Constructs a new BOM InputStream that detects a a ByteOrderMark.UTF_8 and optionally includes it.
Parameters:
delegate - the InputStream to delegate to
include - true to include the UTF-8 BOM or false to exclude it

Установите include на false, и ваши символы спецификации будут исключены.

2 голосов
/ 02 декабря 2009

К сожалению, нет. Вам придется идентифицировать себя и пропустить себя. На этой странице подробно описано, на что вам нужно смотреть. Также см. этот ТАК вопрос для более подробной информации.

0 голосов
/ 17 января 2018

У меня была такая же проблема, и, поскольку я не читал кучу файлов, я сделал более простое решение. Я думаю, что моя кодировка была UTF-8, потому что, когда я распечатал оскорбительный символ с помощью этой страницы: Получить значение в Unicode символа , я обнаружил, что это было \ufeff. Я использовал код System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );, чтобы распечатать ошибочное значение Юникода.

Как только у меня появилось неверное значение Юникода, я заменил его в первой строке моего файла перед тем, как продолжить чтение. Бизнес логика этого раздела:

String str = reader.readLine().trim();
str = str.replace("\ufeff", "");

Это исправило мою проблему. Тогда я смог продолжить обработку файла без проблем. Я добавил trim() только в случае начальных или конечных пробелов, вы можете сделать это или нет, в зависимости от ваших конкретных потребностей.

...