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

14 minutos de lectura

La marca de orden de bytes arruina la lectura de
Tomás

Estoy tratando de 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 los bytes se lee junto con el resto de la primera línea, lo que provoca problemas con las comparaciones de cadenas.

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

1646747891 461 La marca de orden de bytes arruina la lectura de
Gregorio Pakosz

EDITAR: Hice un lanzamiento adecuado en GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Aquí hay una clase que codifiqué hace un tiempo, solo edité el nombre del paquete antes de pegar. Nada especial, es bastante similar a las soluciones publicadas en la base de datos de errores de SUN. Incorpórelo 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

  • Perdón por las áreas de desplazamiento largas, lástima que no hay una función de archivo adjunto

    – Gregorio Pakosz

    2 de diciembre de 2009 a las 20:22

  • Gracias Gregory, eso es justo lo que estoy buscando.

    – Tomás

    2 de diciembre de 2009 a las 22:01

  • Esto debería estar en el núcleo de la API de Java

    – Denis Kniazhev

    7 de diciembre de 2012 a las 16:42

  • Han pasado 10 años y sigo recibiendo karma por esto 😀 ¡Te estoy mirando Java!

    – Gregorio Pakosz

    22 de mayo de 2015 a las 10:33

  • Votado a favor porque la respuesta proporciona un historial sobre por qué el flujo de entrada de archivos no proporciona la opción de descartar BOM de forma predeterminada.

    – MXLDevs

    30 de abril de 2018 a las 19:09

1646747892 309 La marca de orden de bytes arruina la lectura de
rescdsk

los Apache Commons E/S la biblioteca tiene un InputStream que 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 frente a UTF-16 big + little endian: detalles en el enlace de documentación anterior. A continuación, puede utilizar el detectado ByteOrderMark para elegir un Charset para decodificar la transmisión. (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 manera muy buena de detectar en qué codificación están algunos bytes, pero si la transmisión 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)

Vota a favor el comentario de @martin-charlesworth 🙂

  • Simplemente omite la lista de materiales. Debería ser la solución perfecta para el 99% de los casos de uso.

    – atamanroman

    7 de mayo de 2012 a las 7:39

  • Utilicé esta respuesta con éxito. Sin embargo, agregaría respetuosamente el boolean arg para especificar si incluir o excluir la lista de materiales. Ejemplo: BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM

    –Kevin Meredith

    7 oct 2013 a las 17:12


  • También agregaría que esto solo detecta UTF-8 BOM. Si desea detectar todas las listas de materiales 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);

    – Martín Charlesworth

    26 de junio de 2014 a las 16:29

  • En cuanto al comentario de @KevinMeredith, quiero enfatizar que el constructor con booleano es más claro, pero el constructor predeterminado ya se deshizo de UTF-8 BOM, como sugiere JavaDoc: BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.

    – WesternGun

    15 de septiembre de 2017 a las 12:18


  • Saltar resuelve la mayoría de mis problemas. Si mi archivo comienza con BOM UTF_16BE, ¿puedo crear un InputReader omitiendo BOM y leyendo el archivo como UTF_8? Hasta ahora funciona, quiero entender si hay algún caso extremo. Gracias por adelantado.

    – Bhaskar

    26 mayo 2020 a las 21:22

Solución más sencilla:

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!

  • Muy bueno Andrei. Pero, ¿podrías explicar por qué funciona? ¿Cómo el patrón 0xFEFF coincide con éxito con los archivos UTF-8 que parecen tener un patrón diferente y 3 bytes en lugar de 2? ¿Y cómo puede ese patrón coincidir con ambos extremos de UTF16 y UTF32?

    – Vahid Pazirandeh

    27 mayo 2014 a las 19:08

  • 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, lee este artículo, me ayuda: joelonsoftware.com/articles/Unicode.html

    usuario1092126

    28 mayo 2014 a las 22:21


  • 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

    17 de junio de 2014 a las 13:49

  • Veo que ha cubierto 0xFE 0xFF, que es la marca de orden de bytes para UTF-16BE. Pero, ¿y si los primeros 3 bytes son 0xEF 0xBB 0xEF? (la marca de orden de bytes para UTF-8). Usted afirma que esto funciona para todos los formatos UTF-8. Lo cual podría ser cierto (no he probado su código), pero ¿cómo funciona?

    – bvdb

    7 julio 2016 a las 8:40


  • Vea mi respuesta a Vahid: no abro el flujo de bytes sino el flujo de caracteres y leo un carácter. No importa qué codificación utf se usó para el archivo: el prefijo bom puede representarse mediante un recuento diferente de bytes, pero en términos de caracteres, es solo un carácter

    usuario1092126

    19 de julio de 2016 a las 22:58


1646747892 704 La marca de orden de bytes arruina la lectura de
BalusC

API de datos de Google tiene un UnicodeReader que detecta automáticamente la codificación.

Puedes usarlo en lugar de InputStreamReader. Aquí hay un extracto -ligeramente compactado- 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();
    }
}

1646747893 115 La marca de orden de bytes arruina la lectura de
kevin meredith

los Apache Commons IO de la biblioteca BOMInputStream ya ha sido mencionado por @rescdsk, pero no lo vi mencionar cómo obtener un InputStream sin la lista de materiales.

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

  • El constructor de un solo argumento lo hace: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. excluye UTF-8 BOM por defecto.

    – Vladimir Vagaytsev

    15/07/2016 a las 17:05


  • Buen punto, Vladímir. Veo que 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

    15/07/2016 a las 17:10

La marca de orden de bytes arruina la lectura de
Andreas Baaserud

Para simplemente eliminar los caracteres BOM de su archivo, recomiendo usar E/S común de Apache

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 include en false y se excluirán los caracteres de la lista de materiales.

  • El constructor de un solo argumento lo hace: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. excluye UTF-8 BOM por defecto.

    – Vladimir Vagaytsev

    15/07/2016 a las 17:05


  • Buen punto, Vladímir. Veo que 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

    15/07/2016 a las 17:10

La marca de orden de bytes arruina la lectura de
Comunidad

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

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad