繁体   English   中英

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

[英]Java : Read last n lines of a HUGE file

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

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

我在想 UNIX 中 tail or less 的方式。我认为他们不会加载整个文件,然后显示文件的最后几行。 在 Java 中也应该有类似的方法来做同样的事情。

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

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

如果您发现行数不足,请从该点备份并重试。 一旦你弄清楚最后N行从哪里开始,你就可以找到那里并阅读和打印。

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

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

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

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

在 UTF-8 的情况下,字符的第一个字节将是0xxxxxxx110xxxxx1110xxxx11110xxx 其他任何内容要么是第二个/第三个字节,要么是非法的 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(...)来计算行尾数。

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;

我发现RandomAccessFile和其他 Buffer Reader 类对我来说太慢了。 没有什么比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();
}

来自 apache commons 的CircularFifoBuffer 如何将 .txt 文件的最后 5 行读入 java 中的类似问题的答案

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

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

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

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

// 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) {
    //
}

这是为此工作。

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

(见表扬)

public String readFromLast(File file, int howMany) throws IOException {
    int numLinesRead = 0;
    StringBuilder builder = new StringBuilder();
    try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            long fileLength = file.length() - 1;
            /*
             * Set the pointer at the end of the file. If the file is empty, an IOException
             * will be thrown
             */
            randomAccessFile.seek(fileLength);

            for (long pointer = fileLength; pointer >= 0; pointer--) {
                randomAccessFile.seek(pointer);
                byte b = (byte) randomAccessFile.read();
                if (b == '\n') {
                    numLinesRead++;
                    // (Last line often terminated with a line separator)
                    if (numLinesRead == (howMany + 1))
                        break;
                }
                baos.write(b);
                fileLength = fileLength - pointer;
            }
            /*
             * Since line is read from the last so it is in reverse order. Use reverse
             * method to make it ordered correctly
             */
            byte[] a = baos.toByteArray();
            int start = 0;
            int mid = a.length / 2;
            int end = a.length - 1;

            while (start < mid) {
                byte temp = a[end];
                a[end] = a[start];
                a[start] = temp;
                start++;
                end--;
            }// End while
            return new String(a).trim();
        } // End inner try-with-resources
    } // End outer try-with-resources

} // End method

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

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

我首先尝试了 RandomAccessFile,但向后读取文件很乏味,每次读取操作时都重新定位文件指针。 因此,我尝试了@Luca 解决方案,并在几分钟内将文件的最后几行作为字符串仅用了两行。

    InputStream inputStream = Runtime.getRuntime().exec("tail " + path.toFile()).getInputStream();
    String tail = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining(System.lineSeparator()));

代码只有2行

     // Please specify correct Charset
     ReversedLinesFileReader rlf = new ReversedLinesFileReader(file, StandardCharsets.UTF_8);

     // read last 2 lines
     System.out.println(rlf.toString(2));

Gradle:

implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'

Maven:

   <dependency>
        <groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version>
   </dependency>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM