Das Byte-Bestellzeichen vermasselt das Lesen von Dateien in Java

107

Ich versuche, CSV-Dateien mit Java zu lesen. Einige der Dateien haben am Anfang möglicherweise eine Bytereihenfolge, aber nicht alle. Wenn vorhanden, wird die Bytereihenfolge zusammen mit dem Rest der ersten Zeile gelesen, was zu Problemen beim Vergleichen von Zeichenfolgen führt.

Gibt es eine einfache Möglichkeit, das Byte-Bestellzeichen zu überspringen, wenn es vorhanden ist?

Vielen Dank!

Tom
quelle

Antworten:

114

BEARBEITEN : Ich habe eine ordnungsgemäße Version auf GitHub erstellt: https://github.com/gpakosz/UnicodeBOMInputStream


Hier ist eine Klasse, die ich vor einiger Zeit codiert habe. Ich habe gerade den Paketnamen vor dem Einfügen bearbeitet. Nichts Besonderes, es ist den in der Fehlerdatenbank von SUN veröffentlichten Lösungen ziemlich ähnlich. Integrieren Sie es in Ihren Code und es geht Ihnen gut.

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

Und Sie verwenden es so:

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
quelle
2
Entschuldigung für die langen Bildlaufbereiche, schade, dass es keine
Anhangsfunktion
Danke Gregory, genau das suche ich.
Tom
3
Dies sollte in der Java-Kern-API sein
Denis Kniazhev
7
10 Jahre sind vergangen und ich bekomme immer noch Karma dafür: D Ich sehe dich an Java!
Gregory Pakosz
1
Upvoted, da die Antwort einen Verlauf darüber enthält, warum der Dateieingabestream nicht die Option bietet, Stücklisten standardmäßig zu verwerfen.
MxLDevs
94

Die Apache Commons IO- Bibliothek verfügt über eine InputStream, die Stücklisten erkennen und verwerfen kann: BOMInputStream(javadoc) :

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

Wenn Sie auch unterschiedliche Codierungen erkennen müssen, können Sie auch zwischen verschiedenen Byte-Reihenfolge-Markierungen unterscheiden, z. B. UTF-8 vs. UTF-16 Big + Little Endian - Details unter dem obigen Doc-Link. Sie können dann das Erkannte verwenden ByteOrderMark, um a Charsetzum Dekodieren des Streams auszuwählen . (Es gibt wahrscheinlich eine optimierte Möglichkeit, dies zu tun, wenn Sie all diese Funktionen benötigen - vielleicht den UnicodeReader in der Antwort von BalusC?). Beachten Sie, dass es im Allgemeinen keine sehr gute Möglichkeit gibt, die Codierung einiger Bytes zu erkennen. Wenn der Stream jedoch mit einer Stückliste beginnt, kann dies anscheinend hilfreich sein.

Bearbeiten : Wenn Sie die Stückliste in UTF-16, UTF-32 usw. erkennen müssen, sollte der Konstruktor wie folgt lauten:

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

Kommentar von Upvote @ martin-charlesworth :)

residdsk
quelle
Überspringt einfach die Stückliste. Sollte für 99% der Anwendungsfälle die perfekte Lösung sein.
Atamanroman
7
Ich habe diese Antwort erfolgreich verwendet. Ich würde jedoch respektvoll das booleanArgument hinzufügen, um anzugeben, ob die Stückliste eingeschlossen oder ausgeschlossen werden soll. Beispiel:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith
19
Ich würde auch hinzufügen, dass dies nur UTF-8-Stücklisten erkennt. Wenn Sie alle utf-X-Stücklisten erkennen möchten, müssen Sie sie an den BOMInputStream-Konstruktor übergeben. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth
In Bezug auf den Kommentar von @KevinMeredith möchte ich betonen, dass der Konstruktor mit Boolescher Wert klarer ist, aber der Standardkonstruktor UTF-8 BOM bereits entfernt hat, wie der JavaDoc vorschlägt:BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
WesternGun
Das Überspringen löst die meisten meiner Probleme. Wenn meine Datei mit einer Stückliste UTF_16BE beginnt, kann ich einen InputReader erstellen, indem ich die Stückliste überspringe und die Datei als UTF_8 lese? Soweit es funktioniert, möchte ich verstehen, ob es einen Randfall gibt? Danke im Voraus.
Bhaskar
31

Einfachere Lösung:

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

Anwendungsbeispiel:

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

Es funktioniert mit allen 5 UTF-Codierungen!


quelle
1
Sehr schöner Andrei. Aber können Sie erklären, warum es funktioniert? Wie passt das Muster 0xFEFF erfolgreich zu UTF-8-Dateien, die ein anderes Muster und 3 Bytes anstelle von 2 zu haben scheinen? Und wie kann dieses Muster mit beiden Endianern von UTF16 und UTF32 übereinstimmen?
Vahid Pazirandeh
1
Wie Sie sehen können, verwende ich keinen Byte-Stream, sondern einen Zeichen-Stream, der mit dem erwarteten Zeichensatz geöffnet wurde. Wenn also das erste Zeichen aus diesem Stream Stückliste ist, überspringe ich es. Die Stückliste kann für jede Codierung eine andere Bytedarstellung haben, dies ist jedoch ein Zeichen. Bitte lesen Sie diesen Artikel, es hilft mir: joelonsoftware.com/articles/Unicode.html
Gute Lösung, stellen Sie vor dem Lesen sicher, dass die Datei nicht leer ist, um eine IOException in der Sprungmethode zu vermeiden. Sie können dies tun, indem Sie if (reader.ready ()) {reader.read (manyBOM) ...} aufrufen
Snow
Ich sehe, Sie haben 0xFE 0xFF behandelt, das ist die Byte-Ordnungsmarke für UTF-16BE. Aber was ist, wenn die ersten 3 Bytes 0xEF 0xBB 0xEF sind? (die Bytereihenfolge für UTF-8). Sie behaupten, dass dies für alle UTF-8-Formate funktioniert. Was könnte wahr sein (ich habe Ihren Code nicht getestet), aber wie funktioniert es dann?
Bvdb
1
Siehe meine Antwort auf Vahid: Ich öffne nicht den Byte-Stream, sondern den Zeichen-Stream und lese ein Zeichen daraus. Es ist egal, welche Utf-Codierung für das Dateibom-Präfix durch eine unterschiedliche Anzahl von Bytes dargestellt werden kann, aber in Bezug auf die Zeichen ist es nur ein Zeichen
24

Die Google Data API verfügt über eine, UnicodeReaderdie die Codierung automatisch erkennt.

Sie können es anstelle von verwenden InputStreamReader. Hier ist ein leicht komprimierter Auszug seiner Quelle, der ziemlich einfach ist:

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
quelle
Es scheint, dass der Link sagt, dass Google Data API veraltet ist? Wo sollte man jetzt nach der Google Data API suchen?
SOUser
1
@XichenLi: Die GData-API ist für ihren beabsichtigten Zweck veraltet. Ich wollte nicht vorschlagen, die GData-API direkt zu verwenden (OP verwendet keinen GData-Dienst), aber ich beabsichtige, den Quellcode als Beispiel für Ihre eigene Implementierung zu übernehmen. Deshalb habe ich es auch in meine Antwort aufgenommen, bereit für die Kopypaste.
BalusC
Darin liegt ein Fehler. Der UTF-32LE-Fall ist nicht erreichbar. Um (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)wahr zu sein, hätte der UTF-16LE-Fall ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) bereits übereinstimmen können.
Joshua Taylor
Da dieser Code von der Google Data API stammt, habe ich Ausgabe 471 darüber veröffentlicht.
Joshua Taylor
13

Der BOMInputStreamApache Commons IO der Bibliothek wurde bereits von @rescdsk erwähnt, aber ich habe nicht erwähnt, wie man einen InputStream ohne die Stückliste erhält .

So habe ich es in Scala gemacht.

 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
quelle
Single arg Konstruktor macht es : public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. Es schließt UTF-8 BOMstandardmäßig aus.
Vladimir Vagaytsev
Guter Punkt, Vladimir. Ich sehe das in seinen Dokumenten - 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

Um die Stücklistenzeichen einfach aus Ihrer Datei zu entfernen, empfehle ich die Verwendung von 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

Wenn Sie include auf false setzen, werden Ihre Stücklistenzeichen ausgeschlossen.

Andreas Baaserud
quelle
2

Leider nicht. Sie müssen sich identifizieren und überspringen. Auf dieser Seite erfahren Sie, worauf Sie achten müssen. Siehe auch diese SO-Frage für weitere Details.

Brian Agnew
quelle
1

Ich hatte das gleiche Problem und weil ich nicht in einer Reihe von Dateien gelesen habe, habe ich eine einfachere Lösung gefunden. Ich glaube, meine Codierung war UTF-8, weil ich beim Ausdrucken des betreffenden Zeichens mithilfe dieser Seite Folgendes festgestellt habe : Unicode-Wert eines Zeichens abrufen\ufeff . Ich habe den Code verwendet System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );, um den fehlerhaften Unicode-Wert auszudrucken.

Sobald ich den fehlerhaften Unicode-Wert hatte, ersetzte ich ihn in der ersten Zeile meiner Datei, bevor ich weiter las. Die Geschäftslogik dieses Abschnitts:

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

Dies hat mein Problem behoben. Dann konnte ich die Datei ohne Probleme weiter verarbeiten. Ich habe hinzugefügt, trim()nur für den Fall eines führenden oder nachfolgenden Leerzeichens, dass Sie dies tun können oder nicht, je nachdem, was Ihre spezifischen Anforderungen sind.

Amy B. Higgins
quelle
1
Das hat bei mir nicht funktioniert, aber ich habe .replaceFirst ("\ u00EF \ u00BB \ u00BF", "") verwendet, was funktioniert hat.
StackUMan