以相反的顺序逐行读取文件



我有一个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 并在打印前加载所有文件,难道没有一种代码轻量级的方式来寻找文件的末尾并读取内容直到文件开头吗?

[编辑]

根据请求,我在此答案前面加上后面评论的情绪:如果您经常需要此行为,"更合适"的解决方案可能是使用 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) 创建一个包装RandomAccessFile实例的 InputStream 子类,以便以相反的顺序读取文件的字节:

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实例,给它"revere"日志文件。

这是一个

老问题。我也想做同样的事情,经过一些搜索发现,apache commons-io中有一个类来实现这一点:

org.apache.commons.io.input.ReversedLinesFileReader

我认为一个不错的选择是使用RandomFileAccess类。此页面上有一些用于回读此类的示例代码。以这种方式读取字节很容易,但是读取字符串可能更具挑战性。

如果您很着急并且想要最简单的解决方案而又不太担心性能,我会尝试使用外部进程来完成肮脏的工作(假设您正在 Un*x 服务器中运行您的应用程序,就像任何体面的人都会做 XD 一样)

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

一个更简单的替代方法,因为你说你正在创建一个 servlet 来做到这一点,是使用 LinkedList 来保存最后 N 行(其中 N 可能是 servlet 参数)。当列表大小超过 N 时,调用 removeFirst()

从用户体验的角度来看,这可能是最好的解决方案。如您所注意的,最新的行是最重要的。不被信息淹没也很重要。

好问题。我不知道有任何常见的实现。正确地做也不是一件小事,所以要小心你的选择。它应该处理字符集编码和不同换行方法的检测。这是我到目前为止处理 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);
        }
    }
}
可以使用

RandomAccessFile 实现这个函数,比如:

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import com.google.common.io.LineProcessor;
public class FileUtils {
/**
 * 反向读取文本文件(UTF8),文本文件分行是通过rn
 * 
 * @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
                                   .......
            });

最简单的解决方案是按正向顺序通读文件,使用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 。有一个技巧:要正确解码字节(假设是多字节编码),您需要读取与条目对应的字节,然后对其应用解码。但是,该列表将为您提供字节的开始和结束位置。

与简单地以相反顺序打印行相比,此方法的一大好处是不会损坏多行日志消息(例如异常)。

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正在花旗工作,他是优秀的程序员程序员相信我令牌[vaquar, khan, is, working, into, Citi, He, is, good, good, programmer, programmer, trust, me]

uniquWordList[信任,vaquar,程序员,是,好,进入,可汗,我,工作,花旗,他]

uniquWordList[程序员,工作,vaquar,信任,好,进入,可汗,花旗,是,我,他]

如果你想对 A 到 Z 进行排序,那就再写一个比较器

使用 Java

7 Autoclosables 和 Java 8 Streams 的简明解决方案:

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

大缺点:仅当行严格按自然顺序排列时才有效,例如以时间戳为前缀的日志文件,但没有例外

最新更新