代码之家  ›  专栏  ›  技术社区  ›  GreyCat

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

  •  7
  • GreyCat  · 技术社区  · 14 年前

    我以前从未有过类似的Java IO API经验,现在我真的很沮丧。我觉得很难相信这是多么的奇怪和复杂,做一件简单的工作是多么的困难。

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

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

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

    Java IO APIs很快就过时了;事实上,我看到了两种读取行的方法(以 \n )从常规本地文件:

    • RandomAccessFile getFilePointer() seek(long pos) ,但是 readLine() 读取非UTF8字符串(甚至不读取字节数组),但是非常奇怪的字符串,它没有缓冲(这可能意味着 read*() 调用将被转换为单个不可靠的操作系统 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字符串(或者至少允许像“read everything till the next”这样的操作 \n个 ")

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

    7 回复  |  直到 14 年前
        1
  •  6
  •   Ken Bloom    14 年前
    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。

        2
  •  6
  •   scube    13 年前

    我写这段代码是为了使用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
  •  1
  •   Martijn Verburg    14 年前

    对于@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();
        }
    }
    
        4
  •  0
  •   Ken Bloom    14 年前

    从一开始 RandomAccessFile 使用 read readFully pos1 pos2 . 假设我们将读取的数据存储在一个名为 rawBytes .

    然后创建 BufferedReader 使用

    new BufferedReader(new InputStreamReader(new ByteArrayInputStream(rawBytes)))
    

    然后你可以打电话 readLine 缓冲区读取 .

    警告:这可能比你能做的 缓冲区读取 寻找正确的位置本身,因为它将所有东西都预加载到内存中。

        5
  •  0
  •   Jonathan B    14 年前

    我认为这种混乱是由UTF-8编码和双字节字符的可能性造成的。

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

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

    既然您声明使用的是UTF-8字符,那么您需要reader类。在这种情况下,我建议使用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 );
    ...
    
        6
  •  0
  •   Jeff Terrell Ph.D.    10 年前

    我来晚了,但我在自己的项目中遇到了这个问题。

    经过多次遍历Javadocs和堆栈溢出之后,我想我找到了一个简单的解决方案。

    在你的随机访问文件中寻找合适的位置之后,我在这里调用 raFile ,请执行以下操作:

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

    那么你应该可以打电话 br.readLine() 尽情享受吧,这比打电话要快得多 raFile.readLine() .

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

        7
  •  -1
  •   AlexR    14 年前

    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没有被阻塞——这是它的主要优势。您可以在internet上搜索任何“nio java教程”并阅读相关内容。但它比常规的IO复杂,大多数应用程序都不需要它。

    推荐文章