Java:使用缓冲输入从随机访问文件中读取字符串

时间:2010-11-29 15:19:39

标签: java nio bufferedreader fileinputstream random-access

我以前从未接触过Java IO API的经验,现在我真的很沮丧。我发现很难相信它是多么奇怪和复杂,以及做一个简单的任务有多难。

我的任务:我有2个位置(起始字节,结束字节),pos1pos2。我需要读取这两个字节之间的行(包括起始字节,不包括结尾字节),并将它们用作UTF8字符串对象。

例如,在大多数脚本语言中,它将是一个非常简单的1-2-3-liner(在Ruby中,但对于Python,Perl等基本相同):

f = File.open("file.txt").seek(pos1)
while f.pos < pos2 {
  s = f.readline
  # do something with "s" here
}

Java IO API很快就会出现问题;)实际上,我看到了两种从常规本地文件中读取行(以\n结尾)的方法:

  • RandomAccessFilegetFilePointer()seek(long pos),但它是readLine()读取非UTF8字符串(甚至不是字节数组),但编码损坏的字符串很奇怪,它没有缓冲(这可能意味着每个read*()调用将被转换为单个不正常的操作系统read() =&gt;相当慢。)
  • BufferedReader有很好的readLine()方法,它甚至可以用skip(long n)进行搜索,但它无法确定已读取的偶数字节数,没有提及文件中的当前位置。

我试过使用类似的东西:

    FileInputStream fis = new FileInputStream(fileName);
    FileChannel fc = fis.getChannel();
    BufferedReader br = new BufferedReader(
            new InputStreamReader(
                    fis,
                    CHARSET_UTF8
            )
    );

...然后使用fc.position()获取当前文件读取位置,fc.position(newPosition)设置一个,但它似乎不适用于我的情况:看起来它返回缓冲区的位置由BufferedReader完成预填充,或类似的东西 - 这些计数器似乎以16K为增量进行四舍五入。

我是否真的必须自己实现它,即文件阅读器界面:

  • 允许我在文件中获取/设置位置
  • 缓冲文件读取操作
  • 允许读取UTF8字符串(或者至少允许“读取所有内容直到下一个\n”等操作)

有没有比自己实施更快的方法?我在监督什么吗?

7 个答案:

答案 0 :(得分:6)

import org.apache.commons.io.input.BoundedInputStream

FileInputStream file = new FileInputStream(filename);
file.skip(pos1);
BufferedReader br = new BufferedReader(
   new InputStreamReader(new BoundedInputStream(file,pos2-pos1))
);

如果您不关心pos2,那么您不需要Apache Commons IO。

答案 1 :(得分:6)

我使用randomaccessfiles

编写了这段代码来读取utf-8
//File: CyclicBuffer.java
public class CyclicBuffer {
private static final int size = 3;
private FileChannel channel;
private ByteBuffer buffer = ByteBuffer.allocate(size);

public CyclicBuffer(FileChannel channel) {
    this.channel = channel;
}

private int read() throws IOException {
    return channel.read(buffer);
}

/**
 * Returns the byte read
 *
 * @return byte read -1 - end of file reached
 * @throws IOException
 */
public byte get() throws IOException {
    if (buffer.hasRemaining()) {
        return buffer.get();
    } else {
        buffer.clear();
        int eof = read();
        if (eof == -1) {
            return (byte) eof;
        }
        buffer.flip();
        return buffer.get();
    }
}
}
//File: UTFRandomFileLineReader.java


public class UTFRandomFileLineReader {
private final Charset charset = Charset.forName("utf-8");
private CyclicBuffer buffer;
private ByteBuffer temp = ByteBuffer.allocate(4096);
private boolean eof = false;

public UTFRandomFileLineReader(FileChannel channel) {
    this.buffer = new CyclicBuffer(channel);
}

public String readLine() throws IOException {
    if (eof) {
        return null;
    }
    byte x = 0;
    temp.clear();

    while ((byte) -1 != (x = (buffer.get())) &amp;&amp; x != '\n') {
        if (temp.position() == temp.capacity()) {
            temp = addCapacity(temp);
        }
        temp.put(x);
    }
    if (x == -1) {
        eof = true;
    }
    temp.flip();
    if (temp.hasRemaining()) {
        return charset.decode(temp).toString();
    } else {
        return null;
    }
}

private ByteBuffer addCapacity(ByteBuffer temp) {
    ByteBuffer t = ByteBuffer.allocate(temp.capacity() + 1024);
    temp.flip();
    t.put(temp);
    return t;
}

public static void main(String[] args) throws IOException {
    RandomAccessFile file = new RandomAccessFile("/Users/sachins/utf8.txt",
            "r");
    UTFRandomFileLineReader reader = new UTFRandomFileLineReader(file
            .getChannel());
    int i = 1;
    while (true) {
        String s = reader.readLine();
        if (s == null)
            break;
        System.out.println("\n line  " + i++);
        s = s + "\n";
        for (byte b : s.getBytes(Charset.forName("utf-8"))) {
            System.out.printf("%x", b);
        }
        System.out.printf("\n");

    }
}
}

答案 2 :(得分:1)

对于@Ken Bloom快速浏览Java 7版本。注意:我认为这不是最有效的方法,我仍然在理解NIO.2,Oracle已经开始了他们的教程here

另请注意,这不是使用Java 7的新ARM语法(它处理基于文件的资源的异常处理),它不能在我拥有的最新openJDK构建中工作。但如果人们想要查看语法,请告诉我。

/* 
 * Paths uses the default file system, note no exception thrown at this stage if 
 * file is missing
 */
Path file = Paths.get("C:/Projects/timesheet.txt");
ByteBuffer readBuffer = ByteBuffer.allocate(readBufferSize);
FileChannel fc = null;
try
{
    /*
     * newByteChannel is a SeekableByteChannel - this is the fun new construct that 
     * supports asynch file based I/O, e.g. If you declared an AsynchronousFileChannel 
     * you could read and write to that channel simultaneously with multiple threads.
     */
    fc = (FileChannel)file.newByteChannel(StandardOpenOption.READ);
    fc.position(startPosition);
    while (fc.read(readBuffer) != -1)
    {
        readBuffer.rewind();
        System.out.println(Charset.forName(encoding).decode(readBuffer));
        readBuffer.flip();
    }
}

答案 3 :(得分:0)

RandomAccessFile开始,使用readreadFully获取pos1pos2之间的字节数组。假设我们已将读取的数据存储在名为rawBytes的变量中。

然后使用

创建BufferedReader
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(rawBytes)))

然后,您可以在readLine上致电BufferedReader

警告:这可能会占用更多内存,而不是让BufferedReader寻找到正确的位置本身,因为它会将所有内容预先加载到内存中。

答案 4 :(得分:0)

我认为混淆是由UTF-8编码和双字节字符的可能性引起的。

UTF8未指定单个字符中的字节数。我假设您使用的是单字节字符。例如,412个字节意味着411个字符。但是如果字符串使用双字节字符,那么你将获得206个字符。

原始的java.io包没有很好地处理这种多字节混淆。因此,他们添加了更多类来专门处理字符串。该软件包混合了两种不同类型的文件处理程序(在命名法整理出来之前,它们可能会混淆)。 类提供直接数据I / O而无需任何转换。 reader 类将文件转换为完全支持多字节字符的字符串。这可能有助于澄清部分问题。

由于您声明使用的是UTF-8字符,因此您需要读者类。在这种情况下,我建议FileReader。 FileReader中的skip()方法允许您传递X个字符,然后开始阅读文本。或者,我更喜欢重载的read()方法,因为它允许您一次获取所有文本。

如果您认为“字节”是单个字符,请尝试以下方法:

FileReader fr = new FileReader( new File("x.txt") );
char[] buffer = new char[ pos2 - pos ];
fr.read( buffer, pos, buffer.length );
...

答案 5 :(得分:0)

我来这里参加派对很晚,但我在自己的项目中遇到了这个问题。

经过Javadocs和Stack Overflow的大量遍历后,我想我找到了一个简单的解决方案。

在我在这里调用raFile的RandomAccessFile中寻找合适的位置后,请执行以下操作:

FileDescriptor fd = raFile.getFD();
FileReader     fr = new FileReader(fd);
BufferedReader br = new BufferedReader(fr);

然后,您应该可以拨打br.readLine()内容,这比调用raFile.readLine()要快得多。

我不确定的一件事是UTF8字符串是否被正确处理。

答案 6 :(得分:-1)

java IO API非常灵活。不幸的是,有时灵活性使它变得冗长。这里的主要思想是有许多流,编写器和读者实现包装器模式。例如,BufferedInputStream包装任何其他InputStream。输出流也是如此。

流和读取器/写入器之间的区别在于流与字节一起工作,而读取器/写入器与字符一起工作。

幸运的是,一些流,编写器和读取器具有简化编码的方便构造器。如果你想阅读文件,你只需要说

    InputStream in = new FileInputStream("/usr/home/me/myfile.txt");
    if (in.markSupported()) {
        in.skip(1024);
        in.read();
    }

这并不像你害怕那么复杂。

频道不同。它是所谓的“新IO”或nio的一部分。新的IO没有被阻止 - 这是它的主要优势。您可以在互联网上搜索任何“nio java教程”并阅读它。但它比常规IO更复杂,大多数应用程序都不需要。