La marca de orden de bytes arruina la lectura de archivos en Java

107

Estoy intentando leer archivos CSV usando Java. Algunos de los archivos pueden tener una marca de orden de bytes al principio, pero no todos. Cuando está presente, el orden de bytes se lee junto con el resto de la primera línea, lo que provoca problemas con las comparaciones de cadenas.

¿Existe una manera fácil de omitir la marca de orden de bytes cuando está presente?

¡Gracias!

Tom
fuente

Respuestas:

114

EDITAR : Hice una versión adecuada en GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Aquí hay una clase que codifiqué hace un tiempo, acabo de editar el nombre del paquete antes de pegarlo. Nada especial, es bastante similar a las soluciones publicadas en la base de datos de errores de SUN. Introdúzcalo en su código y estará bien.

/* ____________________________________________________________________________
 * 
 * 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</pre></li>
 * <li><pre>FF FE 00 00  = UTF-32, little-endian</pre></li>
 * <li><pre>FE FF        = UTF-16, big-endian</pre></li>
 * <li><pre>FF FE        = UTF-16, little-endian</pre></li>
 * <li><pre>EF BB BF     = UTF-8</pre></li>
 * </ul></p>
 * 
 * <p>Use the {@link #getBOM()} method to know whether a BOM has been detected
 * or not.
 * </p>
 * <p>Use the {@link #skipBOM()} method to remove the detected BOM from the
 * wrapped <code>InputStream</code> object.</p>
 */
public class UnicodeBOMInputStream extends InputStream
{
  /**
   * Type safe enumeration class that describes the different types of Unicode
   * BOMs.
   */
  public static final class BOM
  {
    /**
     * NONE.
     */
    public static final BOM NONE = new BOM(new byte[]{},"NONE");

    /**
     * UTF-8 BOM (EF BB BF).
     */
    public static final BOM UTF_8 = new BOM(new byte[]{(byte)0xEF,
                                                       (byte)0xBB,
                                                       (byte)0xBF},
                                            "UTF-8");

    /**
     * UTF-16, little-endian (FF FE).
     */
    public static final BOM UTF_16_LE = new BOM(new byte[]{ (byte)0xFF,
                                                            (byte)0xFE},
                                                "UTF-16 little-endian");

    /**
     * UTF-16, big-endian (FE FF).
     */
    public static final BOM UTF_16_BE = new BOM(new byte[]{ (byte)0xFE,
                                                            (byte)0xFF},
                                                "UTF-16 big-endian");

    /**
     * UTF-32, little-endian (FF FE 00 00).
     */
    public static final BOM UTF_32_LE = new BOM(new byte[]{ (byte)0xFF,
                                                            (byte)0xFE,
                                                            (byte)0x00,
                                                            (byte)0x00},
                                                "UTF-32 little-endian");

    /**
     * UTF-32, big-endian (00 00 FE FF).
     */
    public static final BOM UTF_32_BE = new BOM(new byte[]{ (byte)0x00,
                                                            (byte)0x00,
                                                            (byte)0xFE,
                                                            (byte)0xFF},
                                                "UTF-32 big-endian");

    /**
     * Returns a <code>String</code> representation of this <code>BOM</code>
     * value.
     */
    public final String toString()
    {
      return description;
    }

    /**
     * Returns the bytes corresponding to this <code>BOM</code> value.
     */
    public final byte[] getBytes()
    {
      final int     length = bytes.length;
      final byte[]  result = new byte[length];

      // Make a defensive copy
      System.arraycopy(bytes,0,result,0,length);

      return result;
    }

    private BOM(final byte bom[], final String description)
    {
      assert(bom != null)               : "invalid BOM: null is not allowed";
      assert(description != null)       : "invalid description: null is not allowed";
      assert(description.length() != 0) : "invalid description: empty string is not allowed";

      this.bytes          = bom;
      this.description  = description;
    }

            final byte    bytes[];
    private final String  description;

  } // BOM

  /**
   * Constructs a new <code>UnicodeBOMInputStream</code> that wraps the
   * specified <code>InputStream</code>.
   * 
   * @param inputStream an <code>InputStream</code>.
   * 
   * @throws NullPointerException when <code>inputStream</code> is
   * <code>null</code>.
   * @throws IOException on reading from the specified <code>InputStream</code>
   * when trying to detect the Unicode BOM.
   */
  public UnicodeBOMInputStream(final InputStream inputStream) throws  NullPointerException,
                                                                      IOException

  {
    if (inputStream == null)
      throw new NullPointerException("invalid input stream: null is not allowed");

    in = new PushbackInputStream(inputStream,4);

    final byte  bom[] = new byte[4];
    final int   read  = in.read(bom);

    switch(read)
    {
      case 4:
        if ((bom[0] == (byte)0xFF) &&
            (bom[1] == (byte)0xFE) &&
            (bom[2] == (byte)0x00) &&
            (bom[3] == (byte)0x00))
        {
          this.bom = BOM.UTF_32_LE;
          break;
        }
        else
        if ((bom[0] == (byte)0x00) &&
            (bom[1] == (byte)0x00) &&
            (bom[2] == (byte)0xFE) &&
            (bom[3] == (byte)0xFF))
        {
          this.bom = BOM.UTF_32_BE;
          break;
        }

      case 3:
        if ((bom[0] == (byte)0xEF) &&
            (bom[1] == (byte)0xBB) &&
            (bom[2] == (byte)0xBF))
        {
          this.bom = BOM.UTF_8;
          break;
        }

      case 2:
        if ((bom[0] == (byte)0xFF) &&
            (bom[1] == (byte)0xFE))
        {
          this.bom = BOM.UTF_16_LE;
          break;
        }
        else
        if ((bom[0] == (byte)0xFE) &&
            (bom[1] == (byte)0xFF))
        {
          this.bom = BOM.UTF_16_BE;
          break;
        }

      default:
        this.bom = BOM.NONE;
        break;
    }

    if (read > 0)
      in.unread(bom,0,read);
  }

  /**
   * Returns the <code>BOM</code> that was detected in the wrapped
   * <code>InputStream</code> object.
   * 
   * @return a <code>BOM</code> value.
   */
  public final BOM getBOM()
  {
    // BOM type is immutable.
    return bom;
  }

  /**
   * Skips the <code>BOM</code> that was found in the wrapped
   * <code>InputStream</code> object.
   * 
   * @return this <code>UnicodeBOMInputStream</code>.
   * 
   * @throws IOException when trying to skip the BOM from the wrapped
   * <code>InputStream</code> object.
   */
  public final synchronized UnicodeBOMInputStream skipBOM() throws IOException
  {
    if (!skipped)
    {
      in.skip(bom.bytes.length);
      skipped = true;
    }
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public int read() throws IOException
  {
    return in.read();
  }

  /**
   * {@inheritDoc}
   */
  public int read(final byte b[]) throws  IOException,
                                          NullPointerException
  {
    return in.read(b,0,b.length);
  }

  /**
   * {@inheritDoc}
   */
  public int read(final byte b[],
                  final int off,
                  final int len) throws IOException,
                                        NullPointerException
  {
    return in.read(b,off,len);
  }

  /**
   * {@inheritDoc}
   */
  public long skip(final long n) throws IOException
  {
    return in.skip(n);
  }

  /**
   * {@inheritDoc}
   */
  public int available() throws IOException
  {
    return in.available();
  }

  /**
   * {@inheritDoc}
   */
  public void close() throws IOException
  {
    in.close();
  }

  /**
   * {@inheritDoc}
   */
  public synchronized void mark(final int readlimit)
  {
    in.mark(readlimit);
  }

  /**
   * {@inheritDoc}
   */
  public synchronized void reset() throws IOException
  {
    in.reset();
  }

  /**
   * {@inheritDoc}
   */
  public boolean markSupported() 
  {
    return in.markSupported();
  }

  private final PushbackInputStream in;
  private final BOM                 bom;
  private       boolean             skipped = false;

} // UnicodeBOMInputStream

Y lo estás usando de esta manera:

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
Gregory Pakosz
fuente
2
Lo siento por las áreas de desplazamiento largas, lástima que no hay una función de archivo adjunto
Gregory Pakosz
Gracias Gregory, eso es justo lo que estoy buscando.
Tom
3
Esto debería estar en la API central de Java
Denis Kniazhev
7
Han pasado 10 años y sigo recibiendo karma por esto: D ¡Te estoy mirando Java!
Gregory Pakosz
1
Votado a favor porque la respuesta proporciona un historial sobre por qué el flujo de entrada del archivo no proporciona la opción de descartar la lista de materiales de forma predeterminada.
MxLDevs
95

La biblioteca de IO de Apache Commons tiene una función InputStreamque puede detectar y descartar listas de materiales: BOMInputStream(javadoc) :

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

Si también necesita detectar diferentes codificaciones, también puede distinguir entre varias marcas de orden de bytes diferentes, por ejemplo, UTF-8 vs UTF-16 big + little endian - detalles en el enlace del documento anterior. A continuación, puede utilizar el detectado ByteOrderMarkpara elegir un Charsetpara decodificar la secuencia. (Probablemente haya una forma más simplificada de hacer esto si necesita toda esta funcionalidad, ¿tal vez el UnicodeReader en la respuesta de BalusC?). Tenga en cuenta que, en general, no hay una muy buena manera de detectar en qué codificación están algunos bytes, pero si el flujo comienza con una lista de materiales, aparentemente esto puede ser útil.

Editar : si necesita detectar la lista de materiales en UTF-16, UTF-32, etc., entonces el constructor debería ser:

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

Upvote @ martin-charlesworth's comment :)

rescdsk
fuente
Simplemente omite la lista de materiales. Debería ser la solución perfecta para el 99% de los casos de uso.
atamanroman
7
Usé esta respuesta con éxito. Sin embargo, agregaría respetuosamente el booleanargumento para especificar si incluir o excluir la lista de materiales. Ejemplo:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith
19
También agregaría que esto solo detecta UTF-8 BOM. Si desea detectar todas las listas de materiales de utf-X, debe pasarlas al constructor BOMInputStream. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth
En cuanto al comentario de @KevinMeredith, quiero enfatizar que el constructor con booleano es más claro, pero el constructor predeterminado ya se ha deshecho de UTF-8 BOM, como sugiere BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
JavaDoc
Saltarme resuelve la mayoría de mis problemas. Si mi archivo comienza con una BOM UTF_16BE, ¿puedo crear un InputReader omitiendo la BOM y leyendo el archivo como UTF_8? Hasta ahora funciona, quiero entender si hay algún caso límite. Gracias por adelantado.
Bhaskar
31

Solución más simple:

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

Muestra de uso:

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

¡Funciona con las 5 codificaciones UTF!


fuente
1
Andrei muy agradable. Pero, ¿podría explicar por qué funciona? ¿Cómo coincide el patrón 0xFEFF con los archivos UTF-8 que parecen tener un patrón diferente y 3 bytes en lugar de 2? ¿Y cómo puede coincidir ese patrón con los endianes de UTF16 y UTF32?
Vahid Pazirandeh
1
Como puede ver, no uso el flujo de bytes, pero el flujo de caracteres se abrió con el juego de caracteres esperado. Entonces, si el primer carácter de esta secuencia es BOM, lo omito. BOM puede tener una representación de bytes diferente para cada codificación, pero este es un carácter. Por favor, lea este artículo, me ayuda: joelonsoftware.com/articles/Unicode.html
Buena solución, solo asegúrese de verificar si el archivo no está vacío para evitar IOException en el método de omisión antes de leer. Puede hacerlo llamando a if (reader.ready ()) {reader.read (possibleBOM) ...}
Nieve
Veo que ha cubierto 0xFE 0xFF, que es la marca de orden de bytes para UTF-16BE. Pero, ¿qué pasa si los primeros 3 bytes son 0xEF 0xBB 0xEF? (la marca de orden de bytes para UTF-8). Afirma que esto funciona para todos los formatos UTF-8. Lo cual podría ser cierto (no he probado tu código), pero ¿cómo funciona?
bvdb
1
Vea mi respuesta a Vahid: no abro el flujo de bytes sino el flujo de caracteres y leo un carácter de él. No importa qué codificación utf se usa para el archivo: el prefijo bom puede representarse por diferentes conteos de bytes, pero en términos de caracteres es solo un carácter
24

La API de datos de Google tiene una UnicodeReaderque detecta automáticamente la codificación.

Puedes usarlo en lugar de InputStreamReader. Aquí hay un extracto, ligeramente compacto, de su fuente, que es bastante sencillo:

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();
    }
}
BalusC
fuente
¿Parece que el enlace dice que la API de datos de Google está obsoleta? ¿Dónde debería uno buscar la API de datos de Google ahora?
SOUser
1
@XichenLi: La API de GData ha quedado obsoleta para su propósito previsto. No tenía la intención de sugerir el uso de la API de GData directamente (OP no usa ningún servicio de GData), pero tengo la intención de tomar el código fuente como ejemplo para su propia implementación. Por eso también lo incluí en mi respuesta, listo para copiar.
BalusC
Hay un error en esto. El estuche UTF-32LE es inalcanzable. Para (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)que sea cierto, entonces el caso UTF-16LE ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) ya habría coincidido.
Joshua Taylor
Dado que este código es de la API de datos de Google, publiqué el número 471 al respecto.
Joshua Taylor
13

La Apache Commons IOBiblioteca BOMInputStream ya ha sido mencionado por @rescdsk, pero no verlo menciona cómo obtener una InputStream sin BOM el BOM.

Así es como lo hice en 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
Kevin Meredith
fuente
Constructor solo lo hace arg: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. Excluye UTF-8 BOMpor defecto.
Vladimir Vagaytsev
Buen punto, Vladimir. Veo eso en sus documentos: commons.apache.org/proper/commons-io/javadocs/api-2.2/org/… :Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
Kevin Meredith
4

Para eliminar simplemente los caracteres BOM de su archivo, recomiendo usar 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

Establezca incluir en falso y se excluirán los caracteres de su lista de materiales.

Andreas Baaserud
fuente
2

Lamentablemente no. Tendrás que identificarte y saltarte. Esta página detalla lo que debe estar atento. Consulte también esta pregunta SO para obtener más detalles.

Brian Agnew
fuente
1

Tuve el mismo problema, y ​​como no estaba leyendo en un montón de archivos, hice una solución más simple. Creo que mi codificación era UTF-8 porque cuando imprimí el carácter ofensivo con la ayuda de esta página: Obtener el valor Unicode de un carácter , encontré que lo era \ufeff. Usé el código System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );para imprimir el valor Unicode ofensivo.

Una vez que tuve el valor Unicode ofensivo, lo reemplacé en la primera línea de mi archivo antes de continuar leyendo. La lógica empresarial de esa sección:

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

Esto solucionó mi problema. Luego pude seguir procesando el archivo sin problemas. Agregué que trim()solo en caso de espacios en blanco iniciales o finales, puede hacerlo o no, según sus necesidades específicas.

Amy B. Higgins
fuente
1
Eso no funcionó para mí, pero usé .replaceFirst ("\ u00EF \ u00BB \ u00BF", "") que sí.
StackUMan