Byte order mark mengacaukan pembacaan file di Java

107

Saya mencoba membaca file CSV menggunakan Java. Beberapa file mungkin memiliki tanda urutan byte di awal, tetapi tidak semua. Saat ini, urutan byte dibaca bersama dengan baris pertama lainnya, sehingga menyebabkan masalah dengan perbandingan string.

Apakah ada cara mudah untuk melewati tanda urutan byte jika ada?

Terima kasih!

Tom
sumber

Jawaban:

114

EDIT : Saya telah membuat rilis yang tepat di GitHub: https://github.com/gpakosz/UnicodeBOMInputStream


Ini adalah kelas yang saya kodekan beberapa waktu lalu, saya baru saja mengedit nama paket sebelum menempel. Tidak ada yang istimewa, ini sangat mirip dengan solusi yang diposting di database bug SUN. Gabungkan dalam kode Anda dan Anda baik-baik saja.

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

Dan Anda menggunakannya dengan cara ini:

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
sumber
2
Maaf untuk area scrolling yang panjang,
sayang
Terima kasih Gregory, itulah yang saya cari.
Tom
3
Ini harus dalam inti Java API
Denis Kniazhev
7
10 tahun telah berlalu dan aku masih menerima karma untuk ini: D Aku melihatmu Java!
Gregory Pakosz
1
Suara positif karena jawaban memberikan riwayat tentang mengapa aliran input file tidak menyediakan opsi untuk membuang BOM secara default.
MxLDevs
94

The Apache Commons IO perpustakaan memiliki InputStreamyang dapat mendeteksi dan membuang BOMs: BOMInputStream(javadoc) :

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

Jika Anda juga perlu mendeteksi pengkodean yang berbeda, itu juga dapat membedakan di antara berbagai tanda urutan byte yang berbeda, misalnya UTF-8 vs. UTF-16 big + little endian - detailnya ada di tautan dokumen di atas. Anda kemudian dapat menggunakan file yang terdeteksiByteOrderMark untuk memilih Charsetuntuk memecahkan kode aliran. (Mungkin ada cara yang lebih efisien untuk melakukan ini jika Anda membutuhkan semua fungsi ini - mungkin UnicodeReader dalam jawaban BalusC?). Perhatikan bahwa, secara umum, tidak ada cara yang baik untuk mendeteksi pengkodean beberapa byte, tetapi jika streaming dimulai dengan BOM, tampaknya ini bisa membantu.

Edit : Jika Anda perlu mendeteksi BOM dalam UTF-16, UTF-32, dll, maka konstruktornya harus:

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

Suka komentar @ martin-charlesworth :)

rescdsk
sumber
Lewati saja BOM. Harus menjadi solusi sempurna untuk 99% kasus penggunaan.
atamanroman
7
Saya menggunakan jawaban ini dengan sukses. Namun, saya dengan hormat akan menambahkan booleanargumen untuk menentukan apakah akan menyertakan atau mengecualikan BOM. Contoh:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith
19
Saya juga menambahkan bahwa ini hanya mendeteksi UTF-8 BOM. Jika Anda ingin mendeteksi semua utf-X BOM, Anda harus meneruskannya ke konstruktor BOMInputStream. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth
Adapun komentar dari @KevinMeredith, saya ingin menekankan bahwa konstruktor dengan boolean lebih jelas, tetapi konstruktor default telah menyingkirkan UTF-8 BOM, seperti yang disarankan JavaDoc:BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
WesternGun
Melewatkan sebagian besar masalah saya. Jika file saya dimulai dengan BOM UTF_16BE, dapatkah saya membuat InputReader dengan melewatkan BOM dan membaca file sebagai UTF_8? Sejauh ini berhasil, saya ingin memahami jika ada kasus tepi? Terima kasih sebelumnya.
Bhaskar
31

Solusi yang lebih sederhana:

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

Contoh penggunaan:

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

Ia bekerja dengan semua 5 pengkodean UTF!


sumber
1
Andrei yang sangat baik. Tapi bisakah Anda menjelaskan mengapa itu berhasil? Bagaimana pola 0xFEFF berhasil mencocokkan file UTF-8 yang tampaknya memiliki pola dan 3 byte berbeda, bukan 2? Dan bagaimana pola tersebut dapat cocok dengan kedua ujung UTF16 dan UTF32?
Vahid Pazirandeh
1
Seperti yang Anda lihat - Saya tidak menggunakan aliran byte tetapi aliran karakter dibuka dengan charset yang diharapkan. Jadi jika karakter pertama dari aliran ini adalah BOM - saya lewatkan saja. BOM dapat memiliki representasi byte yang berbeda untuk setiap pengkodean, tetapi ini adalah satu karakter. Silakan baca artikel ini, ini membantu saya: joelonsoftware.com/articles/Unicode.html
Solusi bagus, pastikan untuk memeriksa apakah file tidak kosong untuk menghindari IOException dalam metode lewati sebelum membaca. Anda dapat melakukannya dengan menelepon if (reader.ready ()) {reader.read (maybeBOM) ...}
Snow
Saya melihat Anda telah menutupi 0xFE 0xFF, yang merupakan Tanda pesanan Byte untuk UTF-16BE. Tetapi, bagaimana jika 3 byte pertama adalah 0xEF 0xBB 0xEF? (tanda urutan byte untuk UTF-8). Anda mengklaim bahwa ini berfungsi untuk semua format UTF-8. Mana yang mungkin benar (saya belum menguji kode Anda), tetapi bagaimana cara kerjanya?
bvdb
1
Lihat jawaban saya untuk Vahid: Saya tidak membuka aliran byte tetapi aliran karakter dan membaca satu karakter darinya. Tidak peduli pengkodean utf apa yang digunakan untuk awalan file - bom dapat diwakili oleh jumlah byte yang berbeda, tetapi dalam hal karakter itu hanya satu karakter
24

Google Data API memiliki UnicodeReaderyang secara otomatis mendeteksi pengkodean.

Anda dapat menggunakannya sebagai pengganti InputStreamReader. Berikut adalah ekstrak -sedikit kompak- dari sumbernya yang cukup mudah:

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
sumber
Tampaknya tautan mengatakan Google Data API sudah usang? Di mana sebaiknya mencari Google Data API sekarang?
SOUser
1
@XichenLi: GData API tidak lagi digunakan untuk tujuan yang dimaksudkan. Saya tidak bermaksud untuk menyarankan untuk menggunakan API GData secara langsung (OP tidak menggunakan layanan GData apa pun), tetapi saya bermaksud untuk mengambil alih kode sumber sebagai contoh untuk implementasi Anda sendiri. Itu juga mengapa saya memasukkannya dalam jawaban saya, siap untuk copypaste.
BalusC
Ada bug dalam hal ini. Kasing UTF-32LE tidak dapat dijangkau. Agar (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)menjadi benar, maka UTF-16LE case ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) akan sudah cocok.
Joshua Taylor
Karena kode ini berasal dari Google Data API, saya memposting masalah 471 tentangnya.
Joshua Taylor
13

The Apache Commons IOPerpustakaan BOMInputStream telah disebutkan oleh @rescdsk, tapi saya tidak melihatnya lagi bagaimana untuk mendapatkan InputStream tanpa BOM tersebut.

Inilah cara saya melakukannya di 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
sumber
Tunggal arg konstruktor melakukannya: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. Ini tidak termasuk UTF-8 BOMsecara default.
Vladimir Vagaytsev
Poin yang bagus, Vladimir. Saya melihat itu di dokumennya - 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

Untuk hanya menghapus karakter BOM dari file Anda, saya merekomendasikan menggunakan 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

Setel sertakan ke salah dan karakter BOM Anda akan dikecualikan.

Andreas Baaserud
sumber
2

Sayangnya tidak. Anda harus mengidentifikasi dan melewatkan diri Anda sendiri.Halaman ini merinci apa yang harus Anda perhatikan. Lihat juga pertanyaan SO ini untuk lebih jelasnya.

Brian Agnew
sumber
1

Saya memiliki masalah yang sama, dan karena saya tidak membaca banyak file, saya melakukan solusi yang lebih sederhana. Saya pikir pengkodean saya adalah UTF-8 karena ketika saya mencetak karakter yang menyinggung dengan bantuan halaman ini: Dapatkan nilai unicode dari karakter yang saya temukan itu \ufeff. Saya menggunakan kodeSystem.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) ); untuk mencetak nilai unicode yang menyinggung.

Setelah saya mendapatkan nilai unicode yang menyinggung, saya menggantinya di baris pertama file saya sebelum saya melanjutkan membaca. Logika bisnis dari bagian itu:

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

Ini memperbaiki masalah saya. Kemudian saya dapat melanjutkan memproses file tersebut tanpa masalah. Saya menambahkan trim()jika ada spasi kosong di depan atau di belakang, Anda dapat melakukannya atau tidak, berdasarkan kebutuhan spesifik Anda.

Amy B Higgins
sumber
1
Itu tidak berhasil untuk saya, tapi saya menggunakan .replaceFirst ("\ u00EF \ u00BB \ u00BF", "") yang berhasil.
StackUMan