按照相反的顺序逐行读取文件

29

我有一个Java EE应用程序,其中我使用servlet打印使用log4j创建的日志文件。当读取日志文件时,通常会查找最后一条日志记录,因此如果servlet以相反的顺序打印日志文件,则会更有用。我的实际代码是:

    response.setContentType("text");
    PrintWriter out = response.getWriter();
    try {
        FileReader logReader = new FileReader("logfile.log");
        try {
            BufferedReader buffer = new BufferedReader(logReader);
            for (String line = buffer.readLine(); line != null; line = buffer.readLine()) {
                out.println(line);
            }
        } finally {
            logReader.close();
        }
    } finally {
        out.close();
    }

我在网上找到的实现方式都涉及使用StringBuffer,并在打印之前加载整个文件,有没有更轻量级的方法可以定位到文件末尾并读取直到文件开头的内容?


尝试使用最大整数调用skip()方法。它会告诉你它实际跳过了多远。然后从该值中减去一些量,并跳转到那里。然后读取剩余的量,并解析该缓冲区。 - Marvo
可能是重复的问题:Java:快速读取文本文件的最后一行? 特别是要看@Jon Skeet的答案,其中链接到了一个类似的C#问题。 - Stephen C
参见:https://dev59.com/Jm855IYBdhLWcg3w1oA8 - Stephen C
@Marvo: skip() 返回它跳过的距离,因为它不一定会跳过您要求的那么远。 但是,它实际上可能会尝试跳过您要求的那么远,并失败。 - ColinD
1
仅仅按照倒序打印日志文件的行并不是一个好主意。例如,如果您有一个多行日志条目(比如异常情况),会发生什么呢? - Anon
10个回答

12

[编辑]

根据请求,我在本答案前加上后来评论的观点:如果您经常需要此行为,则“更合适”的解决方案可能是使用 DBAppender(log4j 2 的一部分)将日志从文本文件移至数据库表中。然后,您可以简单地查询最新条目。

[/编辑]

我可能会以稍微不同的方式来处理这个问题。

(1) 创建一个Writer的子类,将每个字符的编码字节以相反顺序写入:

public class ReverseOutputStreamWriter extends Writer {
    private OutputStream out;
    private Charset encoding;
    public ReverseOutputStreamWriter(OutputStream out, Charset encoding) {
        this.out = out;
        this.encoding = encoding;
    }
    public void write(int ch) throws IOException {
        byte[] buffer = this.encoding.encode(String.valueOf(ch)).array();
        // write the bytes in reverse order to this.out
    }
    // other overloaded methods
}

(2) 创建一个 log4j 的子类 WriterAppender,重写其 createWriter 方法以创建一个 ReverseOutputStreamWriter 实例。

(3) 创建一个 log4j 的子类 Layout,其 format 方法以字符反向顺序返回日志字符串:

public class ReversePatternLayout extends PatternLayout {
    // constructors
    public String format(LoggingEvent event) {
        return new StringBuilder(super.format(event)).reverse().toString();
    }
}

(4) 修改我的日志配置文件,使其将日志消息发送到“普通”日志文件和“反向”日志文件。 "反向" 日志文件应包含与“普通”日志文件相同的日志消息,但每条消息都应该是反向写入的。(请注意,“反向”日志文件的编码不一定符合UTF-8甚至任何字符编码。)

(5) 创建InputStream的子类,它包装了RandomAccessFile实例,以便以相反的顺序读取文件的字节:

public class ReverseFileInputStream extends InputStream {
    private RandomAccessFile in;
    private byte[] buffer;
    // The index of the next byte to read.
    private int bufferIndex;
    public ReverseFileInputStream(File file) {
        this.in = new RandomAccessFile(File, "r");
        this.buffer = new byte[4096];
        this.bufferIndex = this.buffer.length;
        this.in.seek(file.length());
    }
    public void populateBuffer() throws IOException {
        // record the old position
        // seek to a new, previous position
        // read from the new position to the old position into the buffer
        // reverse the buffer
    }
    public int read() throws IOException {
        if (this.bufferIndex == this.buffer.length) {
            populateBuffer();
            if (this.bufferIndex == this.buffer.length) {
                return -1;
            }
        }
        return this.buffer[this.bufferIndex++];
    }
    // other overridden methods
}
现在,如果我想反向读取“正常”日志文件的条目,我只需要创建一个 ReverseFileInputStream 实例,并将其提供给“反向”日志文件。

这绝对是个有趣的答案,但我认为对于我的方法来说太过于代码繁重,而且我不喜欢有两个日志文件的想法。 - eliocs
4
如果你经常需要这种行为,一个"更合适"的解决方案可能是将你的日志从文本文件转移到数据库表中,并使用DBAppender(log4j2的一部分)。然后你只需要查询最新的条目即可,这样既简单又不会重复记录数据。 - Nathan Ryan
1
我将使用DBAppender,这样我可以轻松地添加一个定期清除日志的过程,并在以后需要搜索日志时受益。 - eliocs

11

这是一个旧问题。我也想做同样的事情,经过一些搜索后发现在 apache commons-io 中有一个类可以实现这个目的:

org.apache.commons.io.input.ReversedLinesFileReader


1
即使链接指向类,我也会在您的答案中添加类的名称,因为链接可能在将来失效。顺便说一句,这是个好发现! - eliocs
1
这是相关的Maven构件:http://mvnrepository.com/artifact/commons-io/commons-io/2.4 - Renato

4

我认为使用RandomFileAccess类是一个不错的选择。这个页面上有一些使用这个类进行反向读取的示例代码on this page。以这种方式读取字节很容易,但读取字符串可能会有点挑战。


第一个链接页面上没有任何信息。 - But I'm Not A Wrapper Class

3

如果您急于解决问题,不想过多关注性能,我建议尝试使用外部进程来完成此任务(前提是您在运行应用程序的Un * x服务器上,就像任何体面的人一样XD)。

new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("tail yourlogfile.txt -n 50 | rev").getProcess().getInputStream()))

依赖于外部命令行进程似乎不太具有可移植性 ;) - eliocs

2

如果您正在创建一个servlet来实现这个功能,那么一个更简单的替代方法是使用LinkedList来保存最后的N行(其中N可能是一个servlet参数)。当列表大小超过N时,调用removeFirst()

从用户体验的角度来看,这可能是最好的解决方案。正如您所指出的,最近的行是最重要的。不被信息淹没也非常重要。


1
不被信息淹没确实是一个非常好的点。 - eliocs

1

好问题。我不知道有任何常见的实现方式。这也不是一件容易做好的事情,所以要小心选择。它应该处理字符集编码和检测不同的换行方法。这是我目前拥有的实现,可以处理ASCII和UTF-8编码的文件,包括一个UTF-8的测试用例。它不能处理UTF-16LE或UTF-16BE编码的文件。

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import junit.framework.TestCase;

public class ReverseLineReader {
    private static final int BUFFER_SIZE = 8192;

    private final FileChannel channel;
    private final String encoding;
    private long filePos;
    private ByteBuffer buf;
    private int bufPos;
    private byte lastLineBreak = '\n';
    private ByteArrayOutputStream baos = new ByteArrayOutputStream();

    public ReverseLineReader(File file, String encoding) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        channel = raf.getChannel();
        filePos = raf.length();
        this.encoding = encoding;
    }

    public String readLine() throws IOException {
        while (true) {
            if (bufPos < 0) {
                if (filePos == 0) {
                    if (baos == null) {
                        return null;
                    }
                    String line = bufToString();
                    baos = null;
                    return line;
                }

                long start = Math.max(filePos - BUFFER_SIZE, 0);
                long end = filePos;
                long len = end - start;

                buf = channel.map(FileChannel.MapMode.READ_ONLY, start, len);
                bufPos = (int) len;
                filePos = start;
            }

            while (bufPos-- > 0) {
                byte c = buf.get(bufPos);
                if (c == '\r' || c == '\n') {
                    if (c != lastLineBreak) {
                        lastLineBreak = c;
                        continue;
                    }
                    lastLineBreak = c;
                    return bufToString();
                }
                baos.write(c);
            }
        }
    }

    private String bufToString() throws UnsupportedEncodingException {
        if (baos.size() == 0) {
            return "";
        }

        byte[] bytes = baos.toByteArray();
        for (int i = 0; i < bytes.length / 2; i++) {
            byte t = bytes[i];
            bytes[i] = bytes[bytes.length - i - 1];
            bytes[bytes.length - i - 1] = t;
        }

        baos.reset();

        return new String(bytes, encoding);
    }

    public static void main(String[] args) throws IOException {
        File file = new File("my.log");
        ReverseLineReader reader = new ReverseLineReader(file, "UTF-8");
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }

    public static class ReverseLineReaderTest extends TestCase {
        public void test() throws IOException {
            File file = new File("utf8test.log");
            String encoding = "UTF-8";

            FileInputStream fileIn = new FileInputStream(file);
            Reader fileReader = new InputStreamReader(fileIn, encoding);
            BufferedReader bufReader = new BufferedReader(fileReader);
            List<String> lines = new ArrayList<String>();
            String line;
            while ((line = bufReader.readLine()) != null) {
                lines.add(line);
            }
            Collections.reverse(lines);

            ReverseLineReader reader = new ReverseLineReader(file, encoding);
            int pos = 0;
            while ((line = reader.readLine()) != null) {
                assertEquals(lines.get(pos++), line);
            }

            assertEquals(lines.size(), pos);
        }
    }
}

这段代码在处理编码时存在问题,可能会导致数据损坏。从任意字节流中读取并正确转换为字符(特别是变长多字节编码)非常难以做到正确。更不用说,在搜索 '\r' 和 '\n' 时直接将字节转换为字符,这也是错误的。 - jtahlborn
@jtahlborn:我不确定你是如何断定它完全无法正确处理编码的。我测试过使用UTF-8编码的文件,其中包含各种多字节字符和换行符的变化。我从未说过它是完美的,可能存在处理格式错误文件的问题。但我相信它在大多数情况下都能正常工作,并且我很想看到一个它无法处理的正确编码文件的例子。至于将换行符识别为字节的问题,请参考https://dev59.com/YnRB5IYBdhLWcg3wN1AQ,了解为什么这样做是安全的。 - WhiteFang34
你在UTF-16LE文件上进行了测试吗? - jtahlborn
2
@jtalborn:它确实无法处理UTF-16LE或UTF-16BE文件。我已经更新了答案来表明这一点。虽然我怀疑OP和大多数人都不使用这些编码来处理log4j文件。毫无疑问,正确地处理这些编码是困难的。我的答案以此免责声明为开头,并包含了我的解决方案 :) - WhiteFang34

1

您可以使用RandomAccessFile实现此功能,例如:

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

import com.google.common.io.LineProcessor;
public class FileUtils {
/**
 * 反向读取文本文件(UTF8),文本文件分行是通过\r\n
 * 
 * @param <T>
 * @param file
 * @param step 反向寻找的步长
 * @param lineprocessor
 * @throws IOException
 */
public static <T> T backWardsRead(File file, int step,
        LineProcessor<T> lineprocessor) throws IOException {
    RandomAccessFile rf = new RandomAccessFile(file, "r");
    long fileLen = rf.length();
    long pos = fileLen - step;
    // 寻找倒序的第一行:\r
    while (true) {
        if (pos < 0) {
            // 处理第一行
            rf.seek(0);
            lineprocessor.processLine(rf.readLine());
            return lineprocessor.getResult();
        }
        rf.seek(pos);
        char c = (char) rf.readByte();
        while (c != '\r') {
            c = (char) rf.readByte();
        }
        rf.readByte();//read '\n'
        pos = rf.getFilePointer();
        if (!lineprocessor.processLine(rf.readLine())) {
            return lineprocessor.getResult();
        }
        pos -= step;
    }

  }

使用:

       FileUtils.backWardsRead(new File("H:/usersfavs.csv"), 40,
            new LineProcessor<Void>() {
                                   //TODO  implements method
                                   .......
            });

0
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
 * Inside of C:\\temp\\vaquar.txt we have following content
 * vaquar khan is working into Citi He is good good programmer programmer trust me
 * @author vaquar.khan@gmail.com
 *
 */

public class ReadFileAndDisplayResultsinReverse {
    public static void main(String[] args) {
        try {
            // read data from file
            Object[] wordList = ReadFile();
            System.out.println("File data=" + wordList);
            //
            Set<String> uniquWordList = null;
            for (Object text : wordList) {
                System.out.println((String) text);
                List<String> tokens = Arrays.asList(text.toString().split("\\s+"));
                System.out.println("tokens" + tokens);
                uniquWordList = new HashSet<String>(tokens);
                // If multiple line then code into same loop
            }
            System.out.println("uniquWordList" + uniquWordList);

            Comparator<String> wordComp= new Comparator<String>() {

                @Override
                public int compare(String o1, String o2) {
                    if(o1==null && o2 ==null) return 0;
                    if(o1==null ) return o2.length()-0;
                    if(o2 ==null) return o1.length()-0;
                    //
                    return o2.length()-o1.length();
                }
            };
            List<String> fs=new ArrayList<String>(uniquWordList);
            Collections.sort(fs,wordComp);

            System.out.println("uniquWordList" + fs);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    static Object[] ReadFile() throws IOException {
        List<String> list = Files.readAllLines(new File("C:\\temp\\vaquar.txt").toPath(), Charset.defaultCharset());
        return list.toArray();
    }


}

输出:

[Vaquar Khan正在Citi工作,他是一个优秀的程序员,相信我 tokens [vaquar,khan,is,working,into,Citi,He,is,good,good,programmer,programmer,trust,me]

uniquWordList [trust,vaquar,programmer,is,good,into,khan,me,working,Citi,He]

uniquWordList [programmer,working,vaquar,trust,good,into,khan,Citi,is,me,He]

如果你想按照字母顺序排序,那么再写一个比较器


0
最简单的解决方案是按顺序读取文件,使用一个ArrayList<Long>来保存每个日志记录的字节偏移量。您需要使用类似于Jakarta Commons CountingInputStream的工具来检索每个记录的位置,并需要仔细组织缓冲区以确保它返回正确的值:

FileInputStream fis = // .. logfile
BufferedInputStream bis = new BufferedInputStream(fis);
CountingInputStream cis = new CountingInputSteam(bis);
InputStreamReader isr = new InputStreamReader(cis, "UTF-8");

你可能无法使用BufferedReader,因为它会尝试预读并扰乱计数(但一次读取一个字符不会影响性能,因为你在堆栈中缓冲较低)。

要写入文件,您需要反向迭代列表并使用RandomAccessFile。有一个小技巧:为了正确解码字节(假设是多字节编码),您需要读取与条目对应的字节,然后对其进行解码。然而,列表将为您提供字节的起始和结束位置。

与仅按相反顺序打印行相比,这种方法的一个重大优点是,您不会损坏多行日志消息(例如异常)。


0

使用Java 7的自动关闭和Java 8的流的简洁解决方案:

try (Stream<String> logStream = Files.lines(Paths.get("C:\\logfile.log"))) {
   logStream
      .sorted(Comparator.reverseOrder())
      .limit(10) // last 10 lines
      .forEach(System.out::println);
}

一个很大的缺点:只有在行严格按照自然顺序排列时才能工作,比如带有时间戳前缀但没有异常情况的日志文件。


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