2010-04-13 12 views
11

Czytam dane z pliku, który ma niestety dwa typy kodowania znaków.Problem z buforowaniem InputStreamReader

Istnieje nagłówek i treść. Nagłówek zawsze znajduje się w ASCII i definiuje zestaw znaków, w którym kodowane jest ciało.

Nagłówek nie ma ustalonej długości i musi być uruchamiany przez parser w celu ustalenia jego zawartości/długości.

Plik może być dość duży, więc należy unikać umieszczania całej zawartości w pamięci.

Więc zacząłem od jednego InputStream. Zawijam go początkowo za pomocą InputStreamReader z ASCII i dekoduję nagłówek i wypakowuję zestaw znaków dla ciała. Wszystko dobrze.

Następnie tworzę nowy InputStreamReader z poprawnym zestawem znaków, upuszczam go na tym samym InputStream i zaczynam próbować czytać ciało.

Niestety wygląda na to, że javadoc potwierdza to, że InputStreamReader może zdecydować się na odczyt z wyprzedzeniem dla celów efektywności. Czytanie nagłówka przeżuwa część/całe ciało.

Czy ktoś ma jakieś sugestie dotyczące obejścia tego problemu? Czy ręczne tworzenie CharsetDecoder i karmienie w jednym bajcie, ale dobry pomysł (ewentualnie w niestandardowej implementacji Reader?)

Z góry dziękuję.

EDYCJA: Moje ostateczne rozwiązanie było napisanie InputStreamReader, który nie ma buforowanie, aby upewnić się, że mogę parsować nagłówek bez żucia części ciała. Chociaż nie jest to bardzo wydajne, zawijam surowy InputStream za pomocą BufferedInputStream, więc nie będzie problemu.

// An InputStreamReader that only consumes as many bytes as is necessary 
// It does not do any read-ahead. 
public class InputStreamReaderUnbuffered extends Reader 
{ 
    private final CharsetDecoder charsetDecoder; 
    private final InputStream inputStream; 
    private final ByteBuffer byteBuffer = ByteBuffer.allocate(1); 

    public InputStreamReaderUnbuffered(InputStream inputStream, Charset charset) 
    { 
     this.inputStream = inputStream; 
     charsetDecoder = charset.newDecoder(); 
    } 

    @Override 
    public int read() throws IOException 
    { 
     boolean middleOfReading = false; 

     while (true) 
     { 
      int b = inputStream.read(); 

      if (b == -1) 
      { 
       if (middleOfReading) 
        throw new IOException("Unexpected end of stream, byte truncated"); 

       return -1; 
      } 

      byteBuffer.clear(); 
      byteBuffer.put((byte)b); 
      byteBuffer.flip(); 

      CharBuffer charBuffer = charsetDecoder.decode(byteBuffer); 

      // although this is theoretically possible this would violate the unbuffered nature 
      // of this class so we throw an exception 
      if (charBuffer.length() > 1) 
       throw new IOException("Decoded multiple characters from one byte!"); 

      if (charBuffer.length() == 1) 
       return charBuffer.get(); 

      middleOfReading = true; 
     } 
    } 

    public int read(char[] cbuf, int off, int len) throws IOException 
    { 
     for (int i = 0; i < len; i++) 
     { 
      int ch = read(); 

      if (ch == -1) 
       return i == 0 ? -1 : i; 

      cbuf[ i ] = (char)ch; 
     } 

     return len; 
    } 

    public void close() throws IOException 
    { 
     inputStream.close(); 
    } 
} 
+1

Może się mylę, ale od tej chwili myślałem, że plik może mieć tylko jeden typ kodowania w tym samym czasie. – Roman

+4

@Roman: Możesz robić cokolwiek chcesz z plikami; są po prostu ciągami bajtów. Możesz więc zapisać grupę bajtów, które mają być interpretowane jako ASCII, a następnie wypisać kilka bajtów, które mają być interpretowane jako UTF-16, a jeszcze więcej bajtów powinno być interpretowanych jako UTF-32. Nie mówię, że to dobry pomysł, chociaż przypadek użycia OP jest z pewnością uzasadniony (musisz mieć * jakiś * sposób wskazania, jakie kodowanie pliku używa, mimo wszystko). –

+0

@Mike Q - Dobry pomysł InputStreamReaderUnbuffered. Proponuję osobną odpowiedź - zasługuje na uwagę :) –

Odpowiedz

3

Dlaczego nie używasz 2 InputStream s? Jeden do czytania nagłówka i drugi do ciała.

Drugie InputStream powinno skip bajty nagłówka.

+0

Dzięki, myślę, że będę musiał to zrobić. –

+0

Skąd wiesz, co pominąć? Musisz przeczytać nagłówek, aby wiedzieć, gdzie się kończy. Po rozpoczęciu odczytu nagłówka za pomocą InputStreaReader można żuć bajtów z ciała. –

1

Moja pierwsza myśl to zamknąć strumień i ponownie go otworzyć, używając InputStream#skip, aby pominąć nagłówek, a następnie przekazać strumień do nowego InputStreamReader.

Jeśli naprawdę, naprawdę nie chcesz ponownie otwierać pliku, możesz użyć file descriptors, aby uzyskać więcej niż jeden strumień do pliku, chociaż być może będziesz musiał użyć wielu pozycji w pliku (ponieważ możesz Zakładam, że możesz zresetować pozycję za pomocą reset, może nie być obsługiwana).

+0

Jeśli utworzysz wiele 'FileInputStream's z tym samym' FileDescriptor', wtedy będą zachowywać się tak, jakby były tego samego strumienia. –

+0

@ Tom: Tak, zakładałem, że użyje ich w seriach, a nie równolegle, i że zresetuje pozycję między używaniem jednego a drugim. Ale nie możesz założyć, że możesz zresetować pozycję ... (Nie sądzę, że będą zachowywać się jak * ten sam strumień *, myślę, że byłoby gorzej niż to, po prostu dzielą rzeczywistą pozycję pliku. buforowanie w poszczególnych instancjach może teoretycznie sprawić, że będzie to naprawdę bolesne, jeśli spróbujesz użyć ich równolegle.) –

1

Sugeruję ponowne przeczytanie strumienia od nowa z nowym InputStreamReader. Załóżmy, że obsługiwane jest InputStream.mark.

3

Oto pseudo kod.

  1. Zastosowanie InputStream, ale nie owijać Reader wokół niego.
  2. Przeczytaj bajty zawierające nagłówek i zapisz je do ByteArrayOutputStream.
  3. Tworzenie ByteArrayInputStream z ByteArrayOutputStream i zdekodować nagłówek, tym razem owinąć ByteArrayInputStream do Reader z ASCII charset.
  4. Obliczyć długość wejścia innego niż ascii i odczytać tę liczbę bajtów w innym ByteArrayOutputStream.
  5. Utwórz kolejną ByteArrayInputStream z drugiego ByteArrayOutputStream i zawinąć go z Reader z charset od nagłówka .
+0

Dzięki za twoją sugestię. Niestety, nagłówek nie ma ustalonej długości, ani w postaci binarnej, ani w postaci znaków, więc muszę przetworzyć go przez dekoder z zestawem znaków, aby obliczyć jego strukturę, a tym samym długość. Należy również unikać czytania całej zawartości w buforze wewnętrznym. –

1

jeszcze łatwiej:

Jak powiedziałeś, twój nagłówek jest zawsze w ASCII. Więc czytaj nagłówek bezpośrednio z InputStream, a kiedy skończysz z nim, stworzyć Reader z poprawnym kodowaniem i odczytać z niego

private Reader reader; 
private InputStream stream; 

public void read() { 
    int c = 0; 
    while ((c = stream.read()) != -1) { 
     // Read encoding 
     if (headerFullyRead) { 
      reader = new InputStreamReader(stream, encoding); 
      break; 
     } 
    } 
    while ((c = reader.read()) != -1) { 
     // Handle rest of file 
    } 
} 
+0

Dzięki. W końcu poszedłem z innym rozwiązaniem, które miało napisać InputStreamReaderUnbuffered, który robi dokładnie to samo co InputStreamReader, ale nie ma wewnętrznego bufora, więc nigdy nie czytasz za dużo. Zobacz moją edycję. –

1

Jeśli owinąć InputStream i ograniczyć wszystko czyta tylko 1 bajt na czas wydaje się wyłączać buforowanie wewnątrz InputStreamReader.

W ten sposób nie musimy przepisywać logiki InputStreamReader.

public class OneByteReadInputStream extends InputStream 
{ 
    private final InputStream inputStream; 

    public OneByteReadInputStream(InputStream inputStream) 
    { 
     this.inputStream = inputStream; 
    } 

    @Override 
    public int read() throws IOException 
    { 
     return inputStream.read(); 
    } 

    @Override 
    public int read(byte[] b, int off, int len) throws IOException 
    { 
     return super.read(b, off, 1); 
    } 
} 

Aby skonstruować:

new InputStreamReader(new OneByteReadInputStream(inputStream));