【问题标题】:Java: reading strings from a random access file with buffered inputJava:从带有缓冲输入的随机访问文件中读取字符串
【发布时间】:2010-11-29 15:19:38
【问题描述】:

我以前从未接触过 Java IO API,现在我真的很沮丧。我很难相信它是多么的奇怪和复杂,完成一项简单的任务是多么困难。

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

例如,在大多数脚本语言中,它会是一个非常简单的 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 字符串(甚至不是字节数组),但非常奇怪的字符串编码损坏,并且它没有缓冲(这可能意味着每个 @ 987654330@ 调用将被转换为单个操作系统 read() => 相当慢)。
  • 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”之类的操作)

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

【问题讨论】:

  • RandomAccessFile 用于二进制数据。尽管它可以使用writeUTF/readUTF 存储和检索UTF-8 字符串,但正如您所发现的,它的readLine(以及一般DataInput 接口的readLine)在UTF-8 上不起作用。
  • 是否允许使用 openJDK 7(测试版)或 Apache Commons IO 等 3rd 方库?
  • @Martijn:无论如何,请发布您的 OpenJDK 7 和 Apache Commons IO 解决方案。我很好奇,可能其他人也是。
  • @Martijn Verburg:我不能使用 JDK 7,但欢迎任何第三方库。请回答,很有趣:)
  • @Ken Bloom - 我尝试了 Java 7 版本,但它仍然非常冗长,并且在运行时使用最新的 openJDK 构建实际上失败了 :(。唯一的优点是您可以使用多个线程从同一个文件并行读取/写入。无论如何我已经发布了。我承认还没有查找公共文件 I/O 的东西,我假设他们有一个比 JDK 1.5/1.6 更简单的 API ,我接下来看看

标签: java nio bufferedreader fileinputstream random-access


【解决方案1】:
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。

【讨论】:

  • 谢谢!很遗憾原始的 Java API 不会包含这样的功能,但至少我们有一个像 BoundedStream 这样的解决方法。我还发现 Google 的 Guava 包含完全相似的 LimitedInputStream 类。
【解决方案2】:

我编写这段代码是为了使用 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");

    }
}
}

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 如何使用 NIO 一次读取一行文件?这样的事情有可能吗?
    • 我已经修复了示例代码,以便阅读工作 - 将找出阅读一行的最佳方式(这是一个有用的练习,因为我正在向 nio 反馈 API '问题' -开发邮件列表)
    • 我在javakb.com/Uwe/Forum.aspx/java-programmer/7117/…这里看到了答案——使用java.util.Scanner对频道进行操作
    • 可以,是的,我刚刚意识到我使用了常规的 FileChannel 示例而不是 AsynchronousFileChannel 示例,所以我在上面调整了我的 cmets。这都是很强大的东西,但我认为它仍然需要一些更高级别的 API 抽象才能流行起来。
    • AFAICT,GreyCat 仍然无法限制读者的阅读范围不超过pos2
    【解决方案4】:

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

    然后使用

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

    然后您可以在BufferedReader 上拨打readLine

    警告:这可能会比您让BufferedReader 自行寻找正确的位置使用更多的内存,因为它会将所有内容预加载到内存中。

    【讨论】:

    • 这不是我的选择:我在内存有限的计算机上处​​理多个 GB 文件。
    【解决方案5】:

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

    UTF8 没有指定单个字符中有多少字节。我从您的帖子中假设您使用的是单字节字符。例如,412 个字节意味着 411 个字符。但如果字符串使用双字节字符,您将得到 206 字符。

    最初的 java.io 包没有很好地处理这种多字节混淆。因此,他们添加了更多类来专门处理字符串。该软件包混合了两种不同类型的文件处理程序(在整理命名法之前,它们可能会令人困惑)。 stream 类提供直接数据 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 );
    ...
    

    【讨论】:

    • 请注意,读者会跳过字符,而不是字节。这可以防止在使用未知字符集时出现歧义——是单字节还是双字节?在你的情况下,我假设它都是单字节字符,所以 "pos" = characters。
    • 我使用完整的 unicode 集——在 UTF8 编码中每个字符有多个字节——这没有问题。 FileReader 具有与 BufferedReader 基本相同的接口,但您建议一次将全部字节范围读取到内存中 - 但我做不到,我在 RAM 相当有限的机器上处理数 GB 范围。
    • 你不必那样做,只是方便。更长的方法是使用 skip() 到达正确的点,然后使用 read() 从流中获取单个字符。由于该方法中没有缓冲,因此您可以对内存占用进行大量控制。 (您仍然可以通过使用 BufferedReader 包装 FileReader 来获得缓冲的好处。如果您需要限制内存占用,可以将 BufferedReader 初始化为非常特定的大小。)
    【解决方案6】:

    我在这里聚会迟到了,但我在自己的项目中遇到了这个问题。

    在对Javadocs和Stack Overflow进行了多次遍历之后,我想我找到了一个简单的解决方案。

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

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

    那么你应该可以随心所欲地拨打br.readLine(),这将比拨打raFile.readLine()快得多。

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

    【讨论】:

    • 这个问题是它仍然没有解决跟踪读取的字节数的问题。我有一个约束,我需要返回 n 字节的块(例如 64K 字节)。 UTF8 字符串将从 JDK 源中正确处理。阅读 RandomAccessFile 的源码真的很有启发性; Oracle 应该为 readLine 的实现感到羞耻,特别是因为他们在 BufferedReader 中有一个正确的工作......更糟糕的是,他们尽其所能将其定为最终版本,因此我们甚至无法修复它
    • 好点:这不支持在某个位置停止读取。不过,包装此代码以跟踪读取的字节似乎很简单。另外,我对您对 RandomAccessFile 实施的挫败感表示同情!
    【解决方案7】:

    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 更复杂,大多数应用程序不需要它。

    【讨论】:

    • 好吧,我已经阅读了 Javadocs 中的大部分“意识形态差异”解释,但是,回到我最初的问题:没有简单的方法(比如 5-6-10 行),我说得对吗? ) 完全按照我在脚本语言中演示的那样做?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-22
    相关资源
    最近更新 更多