【问题标题】:Java : Read last n lines of a HUGE fileJava:读取一个巨大文件的最后 n 行
【发布时间】:2011-05-06 12:29:26
【问题描述】:

我想读取一个非常大的文件的最后 n 行,而不是使用 Java 将整个文件读入任何缓冲区/内存区域。

我查看了 JDK API 和 Apache Commons I/O,但找不到适合此用途的。

我正在考虑在 UNIX 中使用 tail 或更少的方式。我认为他们不会加载整个文件然后显示文件的最后几行。在 Java 中也应该有类似的方法来做同样的事情。

【问题讨论】:

标签: java file-io large-files


【解决方案1】:
package com.uday;

import java.io.File;
import java.io.RandomAccessFile;

public class TailN {
    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();

        TailN tailN = new TailN();
        File file = new File("/Users/udakkuma/Documents/workspace/uday_cancel_feature/TestOOPS/src/file.txt");
        tailN.readFromLast(file);

        System.out.println("Execution Time : " + (System.currentTimeMillis() - startTime));

    }

    public void readFromLast(File file) throws Exception {
        int lines = 3;
        int readLines = 0;
        StringBuilder builder = new StringBuilder();
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            long fileLength = file.length() - 1;
            // Set the pointer at the last of the file
            randomAccessFile.seek(fileLength);

            for (long pointer = fileLength; pointer >= 0; pointer--) {
                randomAccessFile.seek(pointer);
                char c;
                // read from the last, one char at the time
                c = (char) randomAccessFile.read();
                // break when end of the line
                if (c == '\n') {
                    readLines++;
                    if (readLines == lines)
                        break;
                }
                builder.append(c);
                fileLength = fileLength - pointer;
            }
            // Since line is read from the last so it is in reverse order. Use reverse
            // method to make it correct order
            builder.reverse();
            System.out.println(builder.toString());
        }

    }
}

【讨论】:

    【解决方案2】:

    ReversedLinesFileReader 可以在Apache Commons IO java 库中找到。

        int n_lines = 1000;
        ReversedLinesFileReader object = new ReversedLinesFileReader(new File(path));
        String result="";
        for(int i=0;i<n_lines;i++){
            String line=object.readLine();
            if(line==null)
                break;
            result+=line;
        }
        return result;
    

    【讨论】:

    • 为什么这条评论没有评论就被否决了?我相信这非常能给出问题的正确和最佳答案
    • @Wisienkas 因为没有关于 ReversedLinesFileReader 类的信息。这个类不是标准 jdk 的一部分。
    • 嗨@RakeshS 是对的。它是 Apache Commons IO 的一部分。
    【解决方案3】:

    RandomAccessFile 是一个很好的起点,正如其他答案所述。不过,有一个重要的警告

    如果您的文件未使用每个字符一个字节的编码,readLine() 方法将不适合您。而readUTF() 在任何情况下都不起作用。 (它读取一个以字符数开头的字符串...)

    相反,您需要确保以尊重编码字符边界的方式查找行尾标记。对于固定长度编码(例如 UTF-16 或 UTF-32 的风格),您需要从可被字符大小(以字节为单位)整除的字节位置开始提取字符。对于可变长度编码(例如 UTF-8),您需要搜索一个 必须 是字符第一个字节的字节。

    对于 UTF-8,字符的第一个字节将为 0xxxxxxx110xxxxx1110xxxx11110xxx。其他任何内容要么是第二个/第三个字节,要么是非法的 UTF-8 序列。请参阅The Unicode Standard, Version 5.2, Chapter 3.9,表 3-7。这意味着,正如评论讨论所指出的,正确编码的 UTF-8 流中的任何 0x0A 和 0x0D 字节都将表示 LF 或 CR 字符。因此,如果我们可以假设不使用其他类型的 Unicode 行分隔符(0x2028、0x2029 和 0x0085),那么简单地计算 0x0A 和 0x0D 字节是一种有效的实现策略(对于 UTF-8)。你不能这么假设,那么代码会更复杂。

    确定正确的字符边界后,您只需调用new String(...) 传递字节数组、偏移量、计数和编码,然后重复调用String.lastIndexOf(...) 来计算行尾数。

    【讨论】:

    • +1 用于提及警告。我认为对于 UTF-8,问题可能通过扫描 '\n' 变得更简单......至少这就是 Jon Skeet 在他对related question 的回答中所暗示的......似乎 '\n' 只能在 UTF-8 中作为有效字符出现,并且永远不会出现在“额外字节”中......
    • 是的,对于 UTF-8,这很简单。 UTF-8 将字符编码为单个字节(所有 ASCII 字符)或多个字节(所有其他 Unicode 字符)。对我们来说幸运的是,换行符是一个 ASCII 字符,在 UTF-8 中,没有多字节字符包含也是有效 ASCII 字符的字节。也就是说,如果您扫描一个字节数组以查找 ASCII 换行符并找到它,您知道它是一个换行符,而不是其他一些多字节字符的一部分。我写了一个blog post 有一个很好的表格来说明这一点。
    • 问题在于 1) 字节 0x0a 不是换行符的字符编码(例如 UTF-16),以及 2) 存在其他 Unicode 行分隔符码点的事实;例如0x20280x20290x0085
    • 是的,简单的场景只适用于 UTF-8,并且当换行符被编码为 CRLF 或只是 LF 时......但我认为在实践中这涵盖了大多数现实世界的场景。 UTF-16 在文本文件编码方面非常罕见(它经常在内存中使用,但在文件中不经常使用),我不知道有多少编辑器会插入那些其他 Unicode 行分隔符......跨度>
    【解决方案4】:

    这是为此而工作的。

        private static void printLastNLines(String filePath, int n) {
        File file = new File(filePath);
        StringBuilder builder = new StringBuilder();
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
            long pos = file.length() - 1;
            randomAccessFile.seek(pos);
    
            for (long i = pos - 1; i >= 0; i--) {
                randomAccessFile.seek(i);
                char c = (char) randomAccessFile.read();
                if (c == '\n') {
                    n--;
                    if (n == 0) {
                        break;
                    }
                }
                builder.append(c);
            }
            builder.reverse();
            System.out.println(builder.toString());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    【讨论】:

      【解决方案5】:

      我发现使用apache commons-io api 中的ReversedLinesFileReader 是最简单的方法。 此方法将为您提供文件从底部到顶部的行,您可以指定 n_lines 值来指定行数。

      import org.apache.commons.io.input.ReversedLinesFileReader;
      
      
      File file = new File("D:\\file_name.xml");
      int n_lines = 10;
      int counter = 0; 
      ReversedLinesFileReader object = new ReversedLinesFileReader(file);
      while(counter < n_lines) {
          System.out.println(object.readLine());
          counter++;
      }
      

      【讨论】:

      • 注意:每次调用readLine(),光标都会前进。所以这段代码实际上会错过每一行,因为while 语句中readLine() 的输出没有被捕获。
      • 这段代码有点错误,因为 readLine() 被调用了两次。正如 aapierce 所提到的。但要满分 ReversedLinesFileReader
      • @aapierce 你和 vinksharma 的 cmets 已经过时了,对吧? Mise 的编辑解决了我猜的问题。当 cmets 不符合帖子本身的当前版本时,这有点令人困惑。
      • @DanielEisenreich 是的,自从我 3 年前添加评论以来,似乎答案已被编辑。现在如何编辑我的评论对我来说并不明显。对不起!
      【解决方案6】:

      我有类似的问题,但我不明白其他解决方案。

      我用过这个。我希望那是简单的代码。

      // String filePathName = (direction and file name).
      File f = new File(filePathName);
      long fileLength = f.length(); // Take size of file [bites].
      long fileLength_toRead = 0;
      if (fileLength > 2000) {
          // My file content is a table, I know one row has about e.g. 100 bites / characters. 
          // I used 1000 bites before file end to point where start read.
          // If you don't know line length, use @paxdiablo advice.
          fileLength_toRead = fileLength - 1000;
      }
      try (RandomAccessFile raf = new RandomAccessFile(filePathName, "r")) { // This row manage open and close file.
          raf.seek(fileLength_toRead); // File will begin read at this bite. 
          String rowInFile = raf.readLine(); // First readed line usualy is not whole, I needn't it.
          rowInFile = raf.readLine();
          while (rowInFile != null) {
              // Here I can readed lines (rowInFile) add to String[] array or ArriyList<String>.
              // Later I can work with rows from array - last row is sometimes empty, etc.
              rowInFile = raf.readLine();
          }
      }
      catch (IOException e) {
          //
      }
      

      【讨论】:

        【解决方案7】:

        CircularFifoBuffer 来自 apache commons 。回答How to read last 5 lines of a .txt file into java的类似问题

        请注意,在 Apache Commons Collections 4 中,此类似乎已重命名为 CircularFifoQueue

        【讨论】:

        • 我检查了你提到的类,虽然它确实可以用来跟踪文件中的最后 5 行,但我认为这里的挑战不是跟踪这些行,而是在文件中找到开始阅读的点,以及如何到达那个点。
        【解决方案8】:

        我发现 RandomAccessFile 和其他 Buffer Reader 类对我来说太慢了。没有什么比tail -&lt;#lines&gt; 更快了。所以这对我来说是最好的解决方案。

        public String getLastNLogLines(File file, int nLines) {
            StringBuilder s = new StringBuilder();
            try {
                Process p = Runtime.getRuntime().exec("tail -"+nLines+" "+file);
                java.io.BufferedReader input = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
                String line = null;
            //Here we first read the next line into the variable
            //line and then check for the EOF condition, which
            //is the return value of null
            while((line = input.readLine()) != null){
                    s.append(line+'\n');
                }
            } catch (java.io.IOException e) {
                e.printStackTrace();
            }
            return s.toString();
        }
        

        【讨论】:

        • 执行到tail 本身可能是一个非常昂贵的提议,具体取决于您拥有多少内存。而且它也是 Unix 特定的。
        • 不是通用解决方案。与 tail 类似,可以使用多个实用程序。这不是问题。
        【解决方案9】:

        这是我发现的最好的方法。简单且非常快速且内存高效。

        public static void tail(File src, OutputStream out, int maxLines) throws FileNotFoundException, IOException {
            BufferedReader reader = new BufferedReader(new FileReader(src));
            String[] lines = new String[maxLines];
            int lastNdx = 0;
            for (String line=reader.readLine(); line != null; line=reader.readLine()) {
                if (lastNdx == lines.length) {
                    lastNdx = 0;
                }
                lines[lastNdx++] = line;
            }
        
            OutputStreamWriter writer = new OutputStreamWriter(out);
            for (int ndx=lastNdx; ndx != lastNdx-1; ndx++) {
                if (ndx == lines.length) {
                    ndx = 0;
                }
                writer.write(lines[ndx]);
                writer.write("\n");
            }
        
            writer.flush();
        }
        

        【讨论】:

        • 由于这会读取整个文件,因此对于较大的文件,这将无法很好地扩展。
        • 另外,这个函数对空文件进入无限循环。
        • 为什么会循环空文件?
        • 如果没有行,或者确实少于maxLines 行,则第二个循环的条件不会终止。
        【解决方案10】:

        如果您使用RandomAccessFile,您可以使用lengthseek 到达文件末尾附近的特定点,然后从那里向前读取。

        如果您发现行数不足,请从该点备份并重试。一旦您确定了最后一行 Nth 的开始位置,您就可以找到那里并阅读并打印。

        可以根据您的数据属性做出初步的最佳猜测假设。例如,如果它是一个文本文件,则行长可能不会超过平均 132 行,因此,要获取最后五行,请在结束前 660 个字符开始。然后,如果你错了,在 1320 再试一次(你甚至可以使用你从最后 660 个字符中学到的东西来调整它 - 例如:如果这 660 个字符只是三行,下一次尝试可能是 660 / 3 * 5,再加上一些额外的以防万一)。

        【讨论】:

          【解决方案11】:

          RandomAccessFile 允许搜索 (http://download.oracle.com/javase/1.4.2/docs/api/java/io/RandomAccessFile.html)。 File.length 方法将返回文件的大小。问题是确定行数。为此,您可以查找文件末尾并向后阅读,直到找到正确的行数。

          【讨论】:

            猜你喜欢
            • 2015-09-07
            • 2016-05-06
            • 2018-12-08
            • 1970-01-01
            • 2021-05-29
            • 1970-01-01
            相关资源
            最近更新 更多