A marca de ordem de bytes atrapalha a leitura de arquivos em Java

107

Estou tentando ler arquivos CSV usando Java. Alguns dos arquivos podem ter uma marca de ordem de bytes no início, mas não todos. Quando presente, a ordem de bytes é lida junto com o resto da primeira linha, causando problemas com comparações de strings.

Existe uma maneira fácil de pular a marca de ordem de byte quando ela está presente?

Obrigado!

Tom
fonte

Respostas:

114

EDIT : Eu fiz um lançamento adequado no GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Aqui está uma classe que codifiquei há um tempo, acabei de editar o nome do pacote antes de colar. Nada de especial, é bastante semelhante às soluções postadas no banco de dados de bugs do SUN. Incorpore-o em seu código e está tudo bem.

/* ____________________________________________________________________________
 * 
 * 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

E você está usando desta forma:

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
fonte
2
Desculpe pelas longas áreas de rolagem, pena que não há recurso de anexo
Gregory Pakosz
Obrigado Gregory, isso é exatamente o que estou procurando.
Tom
3
Isso deve estar no núcleo da API Java
Denis Kniazhev
7
10 anos se passaram e ainda estou recebendo carma por isso: D Estou olhando para você Java!
Gregory Pakosz
1
Votado porque a resposta fornece um histórico sobre o motivo pelo qual o fluxo de entrada do arquivo não oferece a opção de descartar o BOM por padrão.
MxLDevs
94

A biblioteca Apache Commons IO tem um InputStreamque pode detectar e descartar BOMs: BOMInputStream(javadoc) :

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

Se você também precisa detectar codificações diferentes, ele também pode distinguir entre várias marcas de ordem de byte diferentes, por exemplo, UTF-8 vs. UTF-16 big + little endian - detalhes no link doc acima. Você pode então usar o detectado ByteOrderMarkpara escolher umCharset para decodificar o fluxo. (Provavelmente existe uma maneira mais simplificada de fazer isso se você precisar de toda essa funcionalidade - talvez o UnicodeReader na resposta de BalusC?). Observe que, em geral, não há uma maneira muito boa de detectar em qual codificação alguns bytes estão, mas se o fluxo começar com um BOM, aparentemente isso pode ser útil.

Editar : se você precisa detectar o BOM em UTF-16, UTF-32, etc, o construtor deve ser:

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

Comentário de Upvote @ martin-charlesworth :)

rescdsk
fonte
Apenas pula o BOM. Deve ser a solução perfeita para 99% dos casos de uso.
atamanroman
7
Usei essa resposta com sucesso. No entanto, eu respeitosamente adicionaria o booleanargumento para especificar se devo incluir ou excluir o BOM. Exemplo:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith,
19
Eu também acrescentaria que isso detecta apenas BOM UTF-8. Se você deseja detectar todos os BOMs utf-X, você precisa passá-los para o construtor BOMInputStream. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth
Quanto ao comentário de @KevinMeredith, quero enfatizar que o construtor com boolean é mais claro, mas o construtor padrão já se livrou do BOM UTF-8, como sugere o JavaDoc:BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
WesternGun
Pular resolve a maioria dos meus problemas. Se meu arquivo começar com um BOM UTF_16BE, posso criar um InputReader pulando o BOM e lendo o arquivo como UTF_8? Até agora funciona, quero entender se há algum caso extremo? Desde já, obrigado.
Bhaskar
31

Solução mais simples:

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

Amostra de uso:

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

Funciona com todas as 5 codificações UTF!


fonte
1
Muito bom Andrei. Mas você poderia explicar por que funciona? Como o padrão 0xFEFF combina com êxito os arquivos UTF-8 que parecem ter um padrão diferente e 3 bytes em vez de 2? E como esse padrão pode corresponder aos dois endians de UTF16 e UTF32?
Vahid Pazirandeh
1
Como você pode ver - eu não uso o fluxo de bytes, mas o fluxo de caracteres aberto com o conjunto de caracteres esperado. Portanto, se o primeiro caractere desse fluxo for BOM, eu pulo. O BOM pode ter uma representação de byte diferente para cada codificação, mas este é um caractere. Leia este artigo, ele me ajuda: joelonsoftware.com/articles/Unicode.html
Boa solução, apenas certifique-se de verificar se o arquivo não está vazio para evitar IOException no método skip antes de ler. Você pode fazer isso chamando if (reader.ready ()) {reader.read (possibleBOM) ...}
Neve
Vejo que você cobriu 0xFE 0xFF, que é a Marca de Ordem de Byte para UTF-16BE. Mas, e se os primeiros 3 bytes forem 0xEF 0xBB 0xEF? (a marca de ordem de byte para UTF-8). Você afirma que isso funciona para todos os formatos UTF-8. O que pode ser verdade (não testei seu código), mas como funciona?
bvdb
1
Veja minha resposta para Vahid: eu não abro o fluxo de bytes, mas o fluxo de caracteres e leio um caractere dele. Não importa qual codificação utf usada para arquivo - prefixo bom pode ser representado por diferentes contagens de bytes, mas em termos de caracteres é apenas um caractere
24

A API de dados do Google possui um dispositivo UnicodeReaderque detecta automaticamente a codificação.

Você pode usá-lo em vez de InputStreamReader. Aqui está um extrato levemente compactado de sua fonte que é bastante simples:

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
fonte
Parece que o link diz que a API de dados do Google está obsoleta? Onde procurar a API de dados do Google agora?
SOUser
1
@XichenLi: A API GData foi descontinuada para sua finalidade. Não tive a intenção de sugerir o uso da API GData diretamente (OP não está usando nenhum serviço GData), mas pretendo assumir o código-fonte como exemplo para sua própria implementação. Também é por isso que incluí em minha resposta, pronto para ser copiado.
BalusC
Há um bug nisso. O caso UTF-32LE está inacessível. Para (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)ser verdadeiro, então o caso UTF-16LE ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) já teria correspondido.
Joshua Taylor
Como esse código é da API de dados do Google, postei a edição 471 sobre ele.
Joshua Taylor
13

O BOMInputStreamApache Commons IO da Biblioteca já foi mencionado por @rescdsk, mas não vi menção de como obter um sem o BOM.InputStream

Veja como fiz no 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
fonte
Construtor arg única faz: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. Exclui UTF-8 BOMpor padrão.
Vladimir Vagaytsev
Bem pensado, Vladimir. Vejo isso em seus 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 simplesmente remover os caracteres BOM do seu arquivo, recomendo usar o 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

Defina incluir como falso e seus caracteres BOM serão excluídos.

Andreas Baaserud
fonte
2

Infelizmente, não. Você terá que se identificar e pular. Esta página detalha o que você deve observar. Veja também esta pergunta do SO para mais detalhes.

Brian Agnew
fonte
1

Tive o mesmo problema e, como não estava lendo um monte de arquivos, fiz uma solução mais simples. Acho que minha codificação era UTF-8 porque quando imprimi o caractere ofensivo com a ajuda desta página: Obtenha o valor unicode de um caractere , descobri que era \ufeff. Usei o código System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );para imprimir o valor Unicode ofensivo.

Assim que obtive o valor Unicode ofensivo, substituí-o na primeira linha do meu arquivo antes de continuar lendo. A lógica de negócios dessa seção:

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

Isso resolveu meu problema. Então, consegui processar o arquivo sem nenhum problema. Eu adicionei trim()apenas no caso de espaço em branco à esquerda ou à direita, você pode fazer isso ou não, com base em quais são suas necessidades específicas.

Amy B Higgins
fonte
1
Isso não funcionou para mim, mas usei .replaceFirst ("\ u00EF \ u00BB \ u00BF", "") que funcionou.
StackUMan