Znacznik kolejności bajtów utrudnia odczyt plików w Javie

107

Próbuję odczytać pliki CSV przy użyciu języka Java. Niektóre pliki mogą mieć znacznik kolejności bajtów na początku, ale nie wszystkie. Gdy występuje, kolejność bajtów jest odczytywana wraz z resztą pierwszego wiersza, powodując w ten sposób problemy z porównaniami łańcuchów.

Czy istnieje łatwy sposób na pominięcie znaku kolejności bajtów, gdy jest obecny?

Dzięki!

Tomek
źródło

Odpowiedzi:

114

EDYCJA : zrobiłem poprawne wydanie na GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Oto klasa, którą kodowałem jakiś czas temu, właśnie zmieniłem nazwę pakietu przed wklejeniem. Nic specjalnego, jest dość podobny do rozwiązań opublikowanych w bazie błędów SUN. Włącz to do swojego kodu i wszystko w porządku.

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

I używasz tego w ten sposób:

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
źródło
2
Przepraszam za długie obszary przewijania, szkoda, że ​​nie ma funkcji załączników
Gregory Pakosz
Dzięki Gregory, właśnie tego szukam.
Tom
3
Powinno to być w rdzeniu Java API
Denis Kniazhev,
7
Minęło 10 lat i wciąż otrzymuję za to karmę: D Patrzę na ciebie Java!
Gregory Pakosz
1
Zaopiniowano, ponieważ odpowiedź zawiera historię dotyczącą tego, dlaczego strumień wejściowy pliku nie zapewnia domyślnie opcji odrzucenia BOM.
MxLDevs
95

Biblioteka Apache Commons IO ma funkcję InputStreamwykrywania i odrzucania zestawień komponentów: BOMInputStream(javadoc) :

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

Jeśli potrzebujesz również wykryć różne kodowania, może również rozróżnić różne znaczniki kolejności bajtów, np. UTF-8 kontra UTF-16 duży + mały endian - szczegóły w linku do dokumentu powyżej. Następnie możesz użyć wykrytego, ByteOrderMarkaby wybrać Charsetdo dekodowania strumienia. (Prawdopodobnie istnieje bardziej usprawniony sposób, aby to zrobić, jeśli potrzebujesz całej tej funkcjonalności - może UnicodeReader w odpowiedzi BalusC?). Zwróć uwagę, że ogólnie rzecz biorąc, nie ma dobrego sposobu na wykrycie kodowania niektórych bajtów, ale jeśli strumień zaczyna się od BOM, najwyraźniej może to być pomocne.

Edycja : Jeśli chcesz wykryć BOM w UTF-16, UTF-32 itp., Konstruktor powinien być:

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

Głosuj za komentarzem @ martin-charlesworth :)

rescdsk
źródło
Po prostu pomija BOM. Powinien być idealnym rozwiązaniem dla 99% przypadków użycia.
atamanroman
7
Z powodzeniem użyłem tej odpowiedzi. Jednak z szacunkiem dodałbym booleanargument dotyczący określenia, czy należy uwzględnić, czy wykluczyć zestawienie komponentów. Przykład:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith
19
Dodam również, że wykrywa to tylko BOM UTF-8. Jeśli chcesz wykryć wszystkie BOM utf-X, musisz przekazać je do konstruktora BOMInputStream. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth
Jeśli chodzi o komentarz @KevinMeredith, chcę podkreślić, że konstruktor z wartością boolean jest bardziej przejrzysty, ale domyślny konstruktor pozbył się już UTF-8 BOM, jak sugeruje JavaDoc:BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
WesternGun
Pomijanie rozwiązuje większość moich problemów. Jeśli mój plik zaczyna się od BOM UTF_16BE, czy mogę utworzyć InputReader, pomijając BOM i odczytując plik jako UTF_8? Jak dotąd to działa, chcę zrozumieć, czy jest jakaś skrajna sprawa? Z góry dziękuję.
Bhaskar
31

Bardziej proste rozwiązanie:

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

Próbka użycia:

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

Działa ze wszystkimi 5 kodowaniami UTF!


źródło
1
Bardzo miły Andrei. Ale czy mógłbyś wyjaśnić, dlaczego to działa? W jaki sposób wzorzec 0xFEFF pomyślnie pasuje do plików UTF-8, które wydają się mieć inny wzorzec i 3 bajty zamiast 2? Jak ten wzorzec może pasować do obu endianów UTF16 i UTF32?
Vahid Pazirandeh
1
Jak widać - nie używam strumienia bajtów, ale strumień znaków jest otwierany z oczekiwanym zestawem znaków. Jeśli więc pierwszy znak z tego strumienia to BOM - pomijam go. Zestawienie komponentów może mieć różne reprezentacje bajtów dla każdego kodowania, ale jest to jeden znak. Przeczytaj ten artykuł, to mi pomaga: joelonsoftware.com/articles/Unicode.html
Niezłe rozwiązanie, po prostu sprawdź, czy plik nie jest pusty, aby uniknąć IOException w metodzie pominięcia przed przeczytaniem. Możesz to zrobić, dzwoniąc do if (reader.ready ()) {reader.read (possibleBOM) ...}
Snow
Widzę, że zakryłeś 0xFE 0xFF, który jest znacznikiem kolejności bajtów dla UTF-16BE. Ale co, jeśli pierwsze 3 bajty to 0xEF 0xBB 0xEF? (znak kolejności bajtów dla UTF-8). Twierdzisz, że działa to dla wszystkich formatów UTF-8. Co może być prawdą (nie testowałem Twojego kodu), ale jak to działa?
bvdb
1
Zobacz moją odpowiedź dla Vahida: otwieram nie strumień bajtów, ale strumień znaków i czytam z niego jeden znak. Nieważne, jakie kodowanie utf używane dla przedrostka pliku - bom może być reprezentowane przez różną liczbę bajtów, ale pod względem znaków to tylko jeden znak
24

Google Data API ma, UnicodeReaderktóry automagicznie wykrywa kodowanie.

Możesz go użyć zamiast InputStreamReader. Oto - lekko zagęszczony - wyciąg z jego źródła, który jest dość prosty:

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
źródło
Wygląda na to, że link mówi, że interfejs API danych Google jest przestarzały? Gdzie teraz szukać Google Data API?
SOUser
1
@XichenLi: Interfejs API GData jest przestarzały ze względu na swój cel. Nie zamierzałem sugerować bezpośredniego korzystania z GData API (OP nie korzysta z żadnej usługi GData), ale zamierzam przejąć kod źródłowy jako przykład dla własnej implementacji. Dlatego też zawarłem to w mojej odpowiedzi, gotowe do kopiowania.
BalusC,
Jest w tym błąd. Obudowa UTF-32LE jest nieosiągalna. Aby (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)była prawdziwa, przypadek UTF-16LE ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) już by się dopasował.
Joshua Taylor
Ponieważ ten kod pochodzi z Google Data API, opublikowałem numer 471 na jego temat.
Joshua Taylor
13

The Apache Commons IOBiblioteki BOMInputStream już wspomniano przez @rescdsk, ale nie widzę go wymienić jak uzyskać InputStream bez BOM.

Oto, jak zrobiłem to w Scali.

 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
źródło
Pojedynczy konstruktor arg to robi: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. UTF-8 BOMDomyślnie wyklucza .
Vladimir Vagaytsev
Słuszna uwaga, Vladimir. Widzę to w jego dokumentacji - 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

Aby po prostu usunąć znaki BOM z pliku, zalecam użycie 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

Ustaw włącz na false, a znaki zestawienia komponentów zostaną wykluczone.

Andreas Baaserud
źródło
2

Niestety nie. Będziesz musiał zidentyfikować się i pominąć siebie. Ta strona zawiera szczegółowe informacje na temat tego, na co musisz uważać. Zobacz również to pytanie SO, aby uzyskać więcej informacji.

Brian Agnew
źródło
1

Miałem ten sam problem, a ponieważ nie czytałem w kilku plikach, zastosowałem prostsze rozwiązanie. Myślę, że moje kodowanie to UTF-8, ponieważ kiedy wydrukowałem obraźliwy znak za pomocą tej strony: Uzyskaj wartość Unicode znaku , stwierdziłem, że to był \ufeff. Użyłem kodu System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );do wydrukowania niewłaściwej wartości Unicode.

Kiedy już miałem obraźliwą wartość Unicode, zastąpiłem ją w pierwszym wierszu mojego pliku, zanim zacząłem czytać. Logika biznesowa tej sekcji:

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

To rozwiązało mój problem. Wtedy mogłem kontynuować przetwarzanie pliku bez problemu. Dodałem trim()tylko w przypadku wiodących lub końcowych białych znaków, możesz to zrobić lub nie, w zależności od twoich konkretnych potrzeb.

Amy B. Higgins
źródło
1
To nie zadziałało, ale użyłem .replaceFirst ("\ u00EF \ u00BB \ u00BF", ""), co zadziałało.
StackUMan