如何编写一个Java文本文件查看器以查看大型日志文件

11

我正在开发一个集成日志文件查看器的软件产品。问题是,对于非常大的文件,它会读取整个文件到内存中,导致速度缓慢且不稳定。我想编写一个新的日志文件查看器来解决这个问题。

如何为大型文本文件编写查看器的最佳实践是什么?类似于Notepad++和VIM这样的编辑器是如何实现的?我考虑使用缓冲双向文本流阅读器以及Java的TableModel。我这样想是否正确,并且是否有这样的流实现可用于Java?

编辑:是否值得运行一次文件以索引每行文本的起始位置,以便知道要寻找的位置?我可能需要知道行数,因此可能至少需要扫描一遍文件?

编辑2:我已将我的实现添加到下面的答案中。请评论或编辑它以帮助我/我们达到更好的实践,否则请提供您自己的实现。

3个回答

4
我不确定NotePad++实际上是否实现了随机访问,但我认为这是正确的方法,特别是对于日志文件查看器,这意味着它将仅被读取。
由于您的日志查看器将只读,因此可以使用一个只读随机访问内存映射文件“流”。在Java中,这是FileChannel
然后,根据需要在文件中跳转,并仅向屏幕呈现数据的滚动窗口。
FileChannel的优点之一是并发线程可以打开文件,并且读取不会影响当前文件指针。因此,如果您在另一个线程中追加日志文件,则不会受到影响。
另一个优点是可以调用FileChannel的size方法以在任何时刻获取文件大小。
直接将内存映射到随机访问文件中存在问题,一些文本编辑器(例如HxD和UltraEdit)允许这样做,但是任何直接更改都会影响文件。因此,更改是立即生效的(除了写入缓存),这通常不是用户想要的。相反,用户通常希望在点击保存之前不要进行更改。但是,由于这只是一个查看器,您没有同样的担忧。

谢谢,我还看到了RandomAccessFile和FileChannel,它们可能会很有用。 - Hannes de Jager

2

一种典型的方法是使用可寻址文件读取器,通过记录行偏移量的索引对日志进行一次遍历,然后仅在请求时呈现文件部分的窗口。

这既减少了需要快速检索的数据,也没有加载一个小部分内容当前不可见的小部件。


0

我在这里发布我的测试实现(在遵循Marcus Adams和msw的建议后),以方便大家进行进一步的评论和批评。它非常快。

我没有考虑Unicode编码的安全性。我想这将是我的下一个问题。对此有任何提示都非常欢迎。

class LogFileTableModel implements TableModel {

    private final File f;
    private final int lineCount;
    private final String errMsg;
    private final Long[] index;
    private final ByteBuffer linebuf = ByteBuffer.allocate(1024);
    private FileChannel chan;

    public LogFileTableModel(String filename) {
        f = new File(filename);
        String m;
        int l = 1;
        Long[] idx = new Long[] {};
        try {
            FileInputStream in = new FileInputStream(f);
            chan = in.getChannel();
            m = null;
            idx = buildLineIndex();
            l = idx.length;
        } catch (IOException e) {
            m = e.getMessage();
        }
        errMsg = m;
        lineCount = l;
        index = idx;
    }

    private Long[] buildLineIndex() throws IOException {
        List<Long> idx = new LinkedList<Long>();
        idx.add(0L);

        ByteBuffer buf = ByteBuffer.allocate(8 * 1024);
        long offset = 0;
        while (chan.read(buf) != -1) {
            int len = buf.position();
            buf.rewind();            
            int pos = 0;
            byte[] bufA = buf.array();
            while (pos < len) {
                byte c = bufA[pos++];
                if (c == '\n')
                    idx.add(offset + pos);
            }
            offset = chan.position();
        }
        System.out.println("Done Building index");
        return idx.toArray(new Long[] {});
    }

    @Override
    public int getColumnCount() {
        return 2;
    }

    @Override
    public int getRowCount() {
        return lineCount;
    }

    @Override
    public String getColumnName(int columnIndex) {
        switch (columnIndex) {
        case 0:
            return "#";
        case 1:
            return "Name";
        }
        return "";
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        switch (columnIndex) {
            case 0:                
                return String.format("%3d", rowIndex);
            case 1:
                if (errMsg != null)
                    return errMsg;
                try { 
                    Long pos = index[rowIndex];
                    chan.position(pos);
                    chan.read(linebuf);
                    linebuf.rewind();
                    if (rowIndex == lineCount - 1)
                        return new String(linebuf.array());
                    else    
                        return new String(linebuf.array(), 0, (int)(long)(index[rowIndex+1]-pos));
                } catch (Exception e) {
                    return "Error: "+ e.getMessage();
                }
        }            
        return "a";
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return String.class;
    }

    // ... other methods to make interface complete


}

嗯,好的,看起来我的实现是UTF-8安全的,因为UTF-8具有固有的自同步性。检查二进制00100000的'\n'在UTF-8中是唯一的。所有属于多字节序列的字节都将至少设置第8位。 - Hannes de Jager

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