Bayt sırası işareti, Java'da dosya okumayı hızlandırır


107

Java kullanarak CSV dosyalarını okumaya çalışıyorum. Bazı dosyaların başında bayt sırası işareti olabilir, ancak hepsinde değil. Varsa, bayt sırası ilk satırın geri kalanıyla birlikte okunur ve bu da dizge karşılaştırmalarında sorunlara neden olur.

Bayt sırası işaretini varken atlamanın kolay bir yolu var mı?

Teşekkürler!


Yanıtlar:


114

DÜZENLEME : GitHub'da uygun bir yayın yaptım: https://github.com/gpakosz/UnicodeBOMInputStream


İşte bir süre önce kodladığım bir sınıf, yapıştırmadan önce paket adını yeni düzenledim. Özel bir şey yok, SUN'un hata veritabanında yayınlanan çözümlere oldukça benziyor. Bunu kodunuza ekleyin ve iyisiniz.

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

Ve bunu şu şekilde kullanıyorsunuz:

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

2
Uzun kaydırma alanları için özür dilerim, çok kötü ek özelliği olmaması
Gregory Pakosz

Teşekkürler Gregory, tam da aradığım bu.
Tom

3
Bu, temel Java API'sinde olmalıdır
Denis Kniazhev

7
10 yıl geçti ve bunun için hala karma alıyorum: D Sana bakıyorum Java!
Gregory Pakosz

1
Yanıt, dosya giriş akışının neden BOM'u varsayılan olarak atma seçeneği sağlamadığına ilişkin geçmiş sağladığından oy verildi.
MxLDevs

95

Apache Commons IO kütüphane, bir sahiptir InputStreamalgılayabilir ve bu ıskarta malzeme listeleri: BOMInputStream(javadoc) :

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

Farklı kodlamaları da tespit etmeniz gerekiyorsa, çeşitli farklı bayt sırası işaretlerini de ayırt edebilir, örneğin UTF-8 ve UTF-16 big + little endian - ayrıntılar yukarıdaki belge bağlantısında. Akışın kodunu çözmek için ByteOrderMarkbir seçmek için algılanan öğesini kullanabilirsiniz Charset. (Tüm bu işlevselliğe ihtiyacınız varsa, bunu yapmanın muhtemelen daha akıcı bir yolu vardır - belki BalusC'nin cevabındaki UnicodeReader olabilir mi?). Genel olarak, bazı baytların hangi kodlamada olduğunu tespit etmenin çok iyi bir yolu olmadığını, ancak akış bir BOM ile başlıyorsa, görünüşe göre bu yardımcı olabilir.

Düzenleme : BOM'u UTF-16, UTF-32, vb. Olarak algılamanız gerekiyorsa, kurucu şöyle olmalıdır:

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

@ Martin-charlesworth'un yorumuna olumlu oy verin :)


Sadece ürün reçetesini atlar. Kullanım durumlarının% 99'u için mükemmel çözüm olmalıdır.
atamanroman

7
Bu cevabı başarıyla kullandım. Bununla birlikte, booleanBOM'nin dahil edilip edilmeyeceğini belirtmek için argümanı saygıyla eklerim . Örnek:BOMInputStream bomIn = new BOMInputStream(in, false); // don't include the BOM
Kevin Meredith

19
Ayrıca bunun yalnızca UTF-8 BOM'u algıladığını da ekleyeceğim. Tüm utf-X BOM'larını algılamak istiyorsanız, bunları BOMInputStream yapıcısına iletmeniz gerekir. BOMInputStream bomIn = new BOMInputStream(is, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_32LE);
Martin Charlesworth

@KevinMeredith'in yorumuna gelince, Boole'lu kurucunun daha net olduğunu vurgulamak istiyorum, ancak BOMInputStream(InputStream delegate) Constructs a new BOM InputStream that excludes a ByteOrderMark.UTF_8 BOM.
JavaDoc'un

Atlamak sorunlarımın çoğunu çözer. Dosyam bir BOM UTF_16BE ile başlıyorsa, BOM'u atlayarak ve dosyayı UTF_8 olarak okuyarak bir InputReader oluşturabilir miyim? Şimdiye kadar işe yarıyor, herhangi bir uç durum olup olmadığını anlamak istiyorum? Şimdiden teşekkürler.
Bhaskar

31

Daha basit çözüm:

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

Kullanım örneği:

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

Tüm 5 UTF kodlamasıyla çalışır!


1
Çok güzel Andrei. Ama neden işe yaradığını açıklayabilir misin? 0xFEFF kalıbı, farklı bir kalıba ve 2 yerine 3 bayta sahip gibi görünen UTF-8 dosyalarıyla nasıl başarılı bir şekilde eşleşir? Ve bu kalıp hem UTF16 hem de UTF32'nin endianslarıyla nasıl eşleşebilir?
Vahid Pazirandeh

1
Gördüğünüz gibi bayt akışını kullanmıyorum ama karakter akışı beklenen karakter kümesiyle açıldı. Yani bu akıştaki ilk karakter BOM ise - onu atlarım. BOM, her kodlama için farklı bayt temsiline sahip olabilir, ancak bu bir karakterdir. Lütfen bu makaleyi okuyun, bana yardımcı oluyor: joelonsoftware.com/articles/Unicode.html

Güzel çözüm, okumadan önce atlama yönteminde IOException'ı önlemek için dosyanın boş olup olmadığını kontrol ettiğinizden emin olun. Bunu if (reader.ready ()) {reader.read (olasıBOM) ...} arayarak yapabilirsiniz
Snow

UTF-16BE için Bayt sırası İşareti olan 0xFE 0xFF'i kapladığınızı görüyorum. Peki ya ilk 3 bayt 0xEF 0xBB 0xEF ise? (UTF-8 için bayt sırası işareti). Bunun tüm UTF-8 formatları için çalıştığını iddia ediyorsunuz. Hangisi doğru olabilir (kodunuzu test etmedim), ama o zaman nasıl çalışıyor?
bvdb

1
Vahid'e cevabımı görün: Bayt akışını değil, karakter akışını açıyorum ve ondan bir karakter okuyorum. Dosya için hangi utf kodlamasının kullanıldığını

24

Google Veri API'sında , UnicodeReaderkodlamayı otomatik olarak algılayan bir bulunur.

Bunun yerine kullanabilirsiniz InputStreamReader. İşte kaynağının oldukça basit olan -hafifçe sıkıştırılmış- bir özü:

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

Görünüşe göre bağlantı Google Data API'nin kullanımdan kaldırıldığını söylüyor? Google Data API şimdi nerede aranmalı?
SOUser

1
@XichenLi: GData API, amacına uygun olarak kullanımdan kaldırıldı. GData API'yi doğrudan kullanmayı önermek niyetinde değildim (OP herhangi bir GData hizmeti kullanmıyor), ancak kendi uygulamanız için örnek olarak kaynak kodunu devralmayı planlıyorum. Bu yüzden kopyalamaya hazır olarak cevabıma ekledim.
BalusC

Bunda bir hata var. UTF-32LE vakasına ulaşılamıyor. (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)Doğru olabilmesi için UTF-16LE durumu ( (bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) zaten eşleşmiş olacaktır.
Joshua Taylor

Bu kod Google Data API'den geldiğinden, bununla ilgili 471 numaralı sorunu yayınladım .
Joshua Taylor

13

Apache Commons IOKütüphane BOMInputStream zaten @rescdsk tarafından söz edilmiştir, ama bir nasıl söz görmedik InputStream olmadan BOM.

İşte Scala'da bunu nasıl yaptım.

 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

Tek arg yapıcı bunu yapar: public BOMInputStream(InputStream delegate) { this(delegate, false, ByteOrderMark.UTF_8); }. UTF-8 BOMVarsayılan olarak hariç tutar .
Vladimir Vagaytsev

İyi nokta Vladimir. Bunu belgelerinde görüyorum - 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

Basitçe BOM karakterlerini dosyanızdan kaldırmak için Apache Common IO'yu kullanmanızı tavsiye ederim.

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

İnclude değerini false olarak ayarlayın ve BOM karakterleriniz hariç tutulacaktır.


2

Ne yazık ki hayır. Kendinizi tanımlamanız ve atlamanız gerekecek. Bu sayfa neye dikkat etmeniz gerektiğini ayrıntılarıyla anlatıyor. Daha fazla ayrıntı için bu SO sorusuna da bakın .


1

Aynı sorunu yaşadım ve bir sürü dosyayı okumadığım için daha basit bir çözüm yaptım. Kodlamamın UTF-8 olduğunu düşünüyorum çünkü bu sayfanın yardımıyla rahatsız edici karakteri yazdırdığımda: Bir karakterin unicode değerini al , öyle olduğunu buldum \ufeff. Kodu System.out.println( "\\u" + Integer.toHexString(str.charAt(0) | 0x10000).substring(1) );, sorun teşkil eden unicode değerini yazdırmak için kullandım .

Sorun yaratan unicode değerini aldıktan sonra, okumaya devam etmeden önce dosyamın ilk satırına onu değiştirdim. O bölümün iş mantığı:

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

Bu benim sorunumu çözdü. Sonra dosyayı sorunsuz bir şekilde işlemeye devam edebildim. Ben eklendi trim()sadece lider veya sonlarındaki boşluk durumunda, siz, belirli ihtiyaçları ne dayalı olduğunu veya olmasın yapabilirsiniz.


1
Bu benim için işe yaramadı, ancak işe yarayan .replaceFirst ("\ u00EF \ u00BB \ u00BF", "") kullandım.
StackUMan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.