Java:读取一个巨大文件的最后n行

41

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

我查看了JDK API和Apache Commons I/O,并没有找到适合这个目的的方法。

我考虑使用UNIX中tail或less所使用的方法。我认为它们不会加载整个文件,然后显示文件的最后几行。在Java中应该也有类似的方法可以实现。


15个回答

34

我发现使用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++;
}

6
注意:每次调用readLine()时,光标会向前移动。因此,这段代码实际上会漏掉每隔一行的内容,因为在while语句中readLine()的输出没有被捕获。 - aapierce
2
这段代码有点错误,因为readLine()被调用了两次,正如aapierce所提到的。但是ReversedLinesFileReader得到了满分。 - vinksharma
4
@aapierce 和 @vinksharma 的评论已经过时了,对吧?Mise 的编辑解决了这个问题,我猜.. 当评论与当前版本的帖子本身不符时,会有点令人困惑。 - Daniel Eisenreich
@DanielEisenreich 是的,看起来自从我3年前添加了我的评论以来,答案已经被编辑过了。现在我不知道如何编辑我的评论了。抱歉! - aapierce

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

如果发现行数不够,可以从该点回退并重试。一旦找出第N个最后一行的起始位置,就可以定位到那里并只读取和打印。

可以根据数据属性进行初步猜测。例如,如果是文本文件,则可能行长度不会超过平均值132,因此要获取最后五行,请在末尾前660个字符处开始。如果猜错了,再在1320处尝试(你甚至可以利用上一次的错误来调整-例如:如果那660个字符只是三行,下一次尝试可能是660 / 3 * 5,加上一些额外的预防措施)。


这里提到了RandomAccessFile的所有答案在处理100,000行文件时所需时间明显比Apache库更长。而且所有示例都返回字符串,而不是数组或列表。请参阅下面我的答案,它返回一个数组,而且没有使用Apache库。 - Adrian Bartyczak

22

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

如果您的文件没有使用每个字符一个字节的编码方式进行编码,则readLine()方法将无法工作。而readUTF()在任何情况下都不会工作。(它读取一个由字符计数前导的字符串...)

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

在UTF-8中,一个字符的第一个字节将是 0xxxxxxx 110xxxxx 1110xxxx 11110xxx 。 其他任何内容都是第二个/第三个字节或非法的UTF-8序列。请参见Unicode标准,版本5.2,第3.9章,表3-7。这意味着,正如评论讨论所指出的那样,在正确编码的UTF-8流中,任何0x0A和0x0D字节都将表示LF或CR字符。因此,如果我们可以假设不使用其他种类的Unicode行分隔符(0x2028、0x2029和0x0085),则简单地计算0x0A和0x0D字节是一种有效的实现策略(对于UTF-8)。如果无法假设,则代码会更加复杂。

在识别出适当的字符边界后,您可以只需调用new String(...) ,传递字节数组、偏移量、计数和编码,然后重复调用String.lastIndexOf(...) 以计算行尾。


1
+1 提到了警告。我认为对于UTF-8,通过扫描“\n”可以简化问题...至少这是Jon Skeet在他回答一个相关问题中所暗示的...似乎'\n'只能作为UTF-8中的有效字符出现,而不会出现在“额外字节”中... - Stijn de Witt
对于UTF-8来说,很简单。UTF-8将字符编码为单个字节(所有ASCII字符)或多个字节(所有其他Unicode字符)。幸运的是,换行符是一个ASCII字符,在UTF-8中,没有多字节字符包含也是有效的ASCII字符的字节。也就是说,如果你扫描一个字节数组以查找ASCII换行符并找到它,你就知道它是一个换行符而不是某个其他多字节字符的一部分。我写了一篇博客文章,其中有一个漂亮的表格说明这一点。 - Stijn de Witt
问题在于:1)字符编码中字节 0x0a 不是换行符(例如 UTF-16);2)还有其他 Unicode 行分隔符代码点,比如 0x20280x20290x0085 - Stephen C
是的,简单的情况只适用于UTF-8编码,并且换行符被编码为CRLF或LF...然而我认为在实践中,这涵盖了大多数真实世界的情况。当涉及到文本文件编码时,UTF-16非常罕见(它经常用于内存中,但不太常用于文件中),我也不知道有多少编辑器会插入其他Unicode行分隔符... - Stijn de Witt

8
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
@Wisienkas 因为没有关于类ReversedLinesFileReader的信息。这个类不是标准jdk的一部分。 - RakeshS
嗨@RakeshS,没错。这是Apache Commons IO的一部分。 - Wisienkas
请注意,result 变量按相反顺序保存了这些行。 - Markus Pscheidt

4

我发现RandomAccessFile和其他缓冲读取器类对我来说太慢了。没有什么比tail -<#lines>更快的了。所以这对我来说是最好的解决方案。

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

6
根据内存使用情况,执行到tail可能本身就是一项非常昂贵的操作。而且这也只适用于Unix系统。 - Gray
不是通用解决方案。类似于tail,可能有多个可用的实用程序。这不是问题所要求的。 - shaILU

2

我查看了你提到的类,虽然它确实可以用来跟踪文件中的最后5行,但我认为这里的挑战不是跟踪这些行,而是找到在文件中开始阅读的点,以及如何到达该点。 - Stijn de Witt

2
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());
        }

    }
}

我喜欢这种方法,但是存在问题。最重要的是:你不能假设randomAccessFile.read();返回的内容可以转换为有效的字符。例如,在UTF-8编码中,欧元符号将被编码为三个字节,即0xe2, 0x82, 0xac。这意味着你需要读取字节,反转它们,然后再进行编码。我会尝试在下面发布一个重新编写的版本。 - g00se

2
这是一个没有Apache依赖的方法,以下是我从一个有100,000行的文件中读取最后90,000行时得到的结果:
这种方法:50毫秒 Apache的ReversedLinesFileReader:900毫秒 RandomAccessFile(反向读取):1,200毫秒
原始来源:Original source
public static String[] getLastNLinesFromFile(String filePath, int numLines) throws IOException {
    try (Stream<String> stream = Files.lines(Paths.get(filePath))) {
        AtomicInteger offset = new AtomicInteger();
        String[] lines = new String[numLines];
        stream.forEach(line -> {
            lines[offset.getAndIncrement() % numLines] = line;
        });
        List<String> list = IntStream.range(offset.get() < numLines ? 0 : offset.get() - numLines, offset.get())
                .mapToObj(idx -> lines[idx % numLines]).collect(Collectors.toList());
        return list.toArray(new String[0]);
    }
}

这对我来说实际上看起来是最好的答案。它简单而且不依赖第三方库,也不需要处理RandomAccessFile - dzim
转念一想,看起来你仍然从上到下逐行阅读... 当你只需要最后几行时,这真的是个好主意吗? - dzim
1
这是一个很好的观点。而且它让我感到好奇。所以我测试了所有以逆序读取文件的RandomAccessFile解决方案,发现对于一个有10万行的文件,在读取最后的9万行时,它们大约需要1200毫秒。而当我测试我的解决方案时,只需要大约50毫秒。所以我会说这比RandomAccessFile更好,但如果你真的想要以逆序读取文件,你最好使用Apache的ReversedLinesFileReader。 - Adrian Bartyczak
1
这是一个很好的观点。它让我感到好奇。所以我测试了所有以相反顺序读取文件的RandomAccessFile解决方案,并发现对于一个有10万行的文件,当读取最后的9万行时,它们大约需要1200毫秒。而当我测试我的解决方案时,只需要大约50毫秒。所以我可以说这比RandomAccessFile更好,但如果你真的想要以相反顺序读取文件,你最好使用Apache的ReversedLinesFileReader。 - undefined
是在另一台电脑上进行的吗?你也可以使用并行流。也许这样可以加快速度一点。 我最关心的是内存效率:你不断地复制东西。虽然垃圾回收可能没有问题,但似乎在某个时候将所有行至少存储一次,最后只保留其中的一小部分,这似乎有些过度。 - dzim
显示剩余4条评论

1
我有类似的问题,但我不理解其他解决方案。
我用了这个。希望这是简单的代码。
// 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) {
    //
}

1
这是关于此事的工作。
    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();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接