Marca de orden de bytes daña lectura de archivo en Java

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 bytes se lee junto con el rest de la primera línea, lo que causa problemas con la comparación de cadenas.

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

¡Gracias!

EDITAR : He hecho un lanzamiento apropiado en GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Aquí hay una clase que codifiqué hace un tiempo, solo edité 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. Incorpóralo en tu código y estarás 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 UnicodeBOMInputStream class wraps any * InputStream and detects the presence of any Unicode BOM * (Byte Order Mark) at its beginning, as defined by * RFC 3629 - UTF-8, a transformation format of ISO 10646 * * 

The * Unicode FAQ * defines 5 types of BOMs:

    *
  • 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
  • *

* *

Use the {@link #getBOM()} method to know whether a BOM has been detected * or not. *

*

Use the {@link #skipBOM()} method to remove the detected BOM from the * wrapped InputStream object.

*/ 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 String representation of this BOM * value. */ public final String toString() { return description; } /** * Returns the bytes corresponding to this BOM 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 UnicodeBOMInputStream that wraps the * specified InputStream. * * @param inputStream an InputStream. * * @throws NullPointerException when inputStream is * null. * @throws IOException on reading from the specified InputStream * 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 BOM that was detected in the wrapped * InputStream object. * * @return a BOM value. */ public final BOM getBOM() { // BOM type is immutable. return bom; } /** * Skips the BOM that was found in the wrapped * InputStream object. * * @return this UnicodeBOMInputStream. * * @throws IOException when trying to skip the BOM from the wrapped * InputStream 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 

La biblioteca Apache Commons IO tiene un InputStream que puede detectar y descartar BOMInputStream : 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 de doc anterior. A continuación, puede usar el ByteOrderMark detectado para elegir un Charset para decodificar la transmisión. (Probablemente haya una manera más simplificada de hacerlo si necesita toda esta funcionalidad, ¿quizás el UnicodeReader en la respuesta de BalusC?). Tenga en cuenta que, en general, no hay una forma muy buena de detectar en qué encoding se encuentran algunos bytes, pero si la secuencia 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 debe ser:

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

Comentario de Upvote @ martin-charlesworth 🙂

Una 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 todas las 5 codificaciones UTF!

Google Data API tiene un UnicodeReader que detecta automágicamente la encoding.

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

BOMInputStream de la Biblioteca de Apache Commons IO ya ha sido mencionado por @rescdsk, pero no lo vi mencionar cómo obtener un InputStream sin la 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 

Lamentablemente no. Tendrá que identificarse y saltearse. Esta página detalla lo que debe observar. También vea esta pregunta SO para más detalles.

Para eliminar simplemente los caracteres de la BOM de su archivo, lo recomiendo usando Apache Common IO

 public BOMInputStream(InputStream delegate, boolean include) Constructs a new BOM InputStream that detects aa 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 

Establecer incluir en falso y los caracteres de su BOM serán excluidos.

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 encoding fue UTF-8 porque cuando imprimí el carácter ofensivo con la ayuda de esta página: obtuve el valor de un carácter unicode , encontré que era \ufeff . System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) ); 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 seguir leyendo. La lógica comercial 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. Añadí trim() solo en caso de espacio en blanco inicial o posterior, puedes hacerlo o no, según tus necesidades específicas.