2010-04-13 40 views
11

Tôi đang đọc dữ liệu từ một tệp có, thật không may, hai loại mã hóa ký tự.InputStreamReader buffer issue

Có tiêu đề và nội dung. Tiêu đề luôn ở dạng ASCII và xác định bộ ký tự mà nội dung được mã hóa.

Tiêu đề không có độ dài cố định và phải được chạy qua trình phân tích cú pháp để xác định nội dung/độ dài của nó.

Tệp cũng có thể khá lớn nên tôi cần tránh đưa toàn bộ nội dung vào bộ nhớ.

Vì vậy, tôi bắt đầu với một InputStream duy nhất. Tôi quấn nó ban đầu với một InputStreamReader với ASCII và giải mã tiêu đề và trích xuất các ký tự đặt cho cơ thể. Tất cả đều tốt.

Sau đó, tôi tạo một InputStreamReader mới với bộ ký tự chính xác, thả nó trên cùng một InputStream và bắt đầu cố gắng đọc nội dung.

Thật không may là nó xuất hiện, javadoc xác nhận điều này, InputStreamReader có thể chọn đọc trước cho các mục đích hiệu quả. Vì vậy, việc đọc tiêu đề nhai một số/tất cả của cơ thể.

Có ai có bất kỳ đề xuất nào để giải quyết vấn đề này không? Có thể tạo CharsetDecoder theo cách thủ công và nạp vào một byte tại một thời điểm hay không nhưng ý tưởng hay (có thể được bao bọc trong triển khai Reader tùy chỉnh?)

Xin cảm ơn trước.

EDIT: Giải pháp cuối cùng của tôi là viết InputStreamReader không có bộ đệm để đảm bảo tôi có thể phân tích cú pháp tiêu đề mà không cần nhai một phần cơ thể. Mặc dù đây không phải là một cách hiệu quả khủng khiếp nhưng tôi đưa InputStream nguyên bản vào một BufferedInputStream để nó không phải là một vấn đề.

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

Có lẽ tôi là sai, nhưng kể từ thời điểm này tôi nghĩ rằng tập tin có thể chỉ có một loại mã hóa cùng một lúc. – Roman

+4

@Roman: Bạn có thể làm bất cứ điều gì bạn muốn với các tập tin; chúng chỉ là chuỗi các byte. Vì vậy, bạn có thể viết ra một loạt các byte được hiểu là ASCII, sau đó viết ra một bó nhiều byte hơn được hiểu là UTF-16 và thậm chí nhiều byte hơn được hiểu là UTF-32. Tôi không nói đó là một ý tưởng tốt, mặc dù trường hợp sử dụng của OP là chắc chắn hợp lý (bạn phải có * một số * cách chỉ ra những gì mã hóa một tập tin sử dụng, sau khi tất cả). –

+0

@Mike Q - Ý tưởng tốt là InputStreamReaderUnbuffered. Tôi đề nghị một câu trả lời riêng biệt - nó xứng đáng được chú ý :) –

Trả lời

3

Tại sao bạn không sử dụng 2 InputStream? Một để đọc tiêu đề và phần đầu khác cho phần thân.

Thứ hai InputStream nên skip byte tiêu đề.

+0

Cảm ơn tôi nghĩ tôi sẽ phải làm điều này. –

+0

Làm thế nào để bạn biết những gì để bỏ qua? Bạn cần phải đọc tiêu đề để biết nó kết thúc ở đâu. Khi bạn bắt đầu đọc tiêu đề bằng InputStreaReader, nó có thể nhai các byte từ phần thân. –

1

Suy nghĩ đầu tiên của tôi là đóng luồng và mở lại luồng bằng cách sử dụng InputStream#skip để bỏ qua tiêu đề trước khi cung cấp luồng mới InputStreamReader.

Nếu bạn thực sự không muốn mở lại tệp, bạn có thể sử dụng file descriptors để nhận nhiều luồng vào tệp, mặc dù bạn có thể phải sử dụng channels để có nhiều vị trí trong tệp (vì bạn có thể Giả sử bạn có thể đặt lại vị trí với reset, nó có thể không được hỗ trợ).

+0

Nếu bạn tạo nhiều 'FileInputStream' với cùng một 'FileDescriptor', thì chúng sẽ hoạt động như thể chúng là cùng một luồng. –

+0

@Tom: Vâng, tôi đã giả sử anh ta sẽ sử dụng chúng theo chuỗi, không song song, và rằng anh ta sẽ thiết lập lại vị trí giữa việc sử dụng một và sử dụng cái kia. Nhưng bạn không thể giả sử bạn có thể đặt lại vị trí ... (Tôi không nghĩ rằng họ sẽ cư xử giống như * cùng một luồng *, tôi nghĩ rằng nó sẽ tồi tệ hơn thế, họ chỉ chia sẻ vị trí tệp thực tế. caching trong các cá thể cá thể có thể trong lý thuyết làm cho điều đó thực sự, thực sự lộn xộn nếu bạn cố gắng sử dụng chúng song song.) –

1

Tôi khuyên bạn nên đọc lại luồng ngay từ đầu bằng một InputStreamReader mới. Có lẽ giả định rằng InputStream.mark được hỗ trợ.

3

Đây là mã giả.

  1. Sử dụng InputStream, nhưng không quấn Reader xung quanh.
  2. Đọc các byte chứa tiêu đề và lưu trữ chúng vào ByteArrayOutputStream.
  3. Tạo ByteArrayInputStream từ ByteArrayOutputStream và giải mã tiêu đề, lần này quấn ByteArrayInputStream vào Reader với ASCII charset.
  4. Tính toán độ dài đầu vào không ascii và đọc số byte đó vào một số khác ByteArrayOutputStream.
  5. Tạo ByteArrayInputStream khác từ thứ hai ByteArrayOutputStream và quấn nó với Reader với charset từ tiêu đề.
+0

Cảm ơn đề xuất của bạn. Thật không may tiêu đề không phải là độ dài cố định, theo thuật ngữ nhị phân hoặc ký tự, vì vậy tôi cần phải phân tích cú pháp thông qua bộ giải mã Charset để tìm ra cấu trúc của nó và do đó độ dài của nó. Tôi cũng cần tránh đọc toàn bộ nội dung vào bộ đệm trong. –

1

Nó thậm chí còn dễ dàng hơn:

Như bạn nói, tiêu đề của bạn luôn ở trong ASCII. Vì vậy, đọc các tiêu đề trực tiếp từ InputStream, và khi bạn đang thực hiện với nó, tạo Reader với mã hóa chính xác và đọc từ nó

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

Cảm ơn. Cuối cùng tôi đã đi với một giải pháp đó là để viết một InputStreamReaderUnbuffered mà không chính xác giống như InputStreamReader nhưng không có bộ đệm nội bộ, do đó bạn không bao giờ đọc quá nhiều. Xem chỉnh sửa của tôi. –

1

Nếu bạn quấn InputStream và hạn chế tất cả các lần đọc chỉ 1 byte tại một thời gian, dường như vô hiệu hóa bộ đệm bên trong InputStreamReader.

Bằng cách này, chúng tôi không phải viết lại logic 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); 
    } 
} 

Để xây dựng:

new InputStreamReader(new OneByteReadInputStream(inputStream));