我正在开发一个带有集成日志文件查看器的软件产品。问题是,它对于非常大的文件来说是缓慢且不稳定的,因为它在您查看日志文件时将整个文件读入内存。我想写一个新的日志文件查看器来解决这个问题。
为大型文本文件编写查看器的最佳做法是什么?像notepad ++和VIM这样的编辑器如何实现这一目标?我正在考虑使用缓冲的双向文本流阅读器和Java的TableModel。我是否按照正确的思路思考,是否有适用于Java的流实现?
编辑:是否值得运行一次文件来索引每行文本开头的位置,以便知道在哪里寻找?我可能需要大量的行,所以可能需要至少扫描一次文件?
Edit2:我已将我的实现添加到下面的答案中。请对其进行评论或编辑,以帮助我/我们达到更好的实践实施或以其他方式提供您自己的实施。
答案 0 :(得分:4)
我不确定NotePad ++实际上是否实现了随机访问,但我认为这是可行的方法,特别是对于日志文件查看器,这意味着它将是只读的。
由于您的日志查看器是只读的,因此您可以使用只读random access内存映射文件“stream”。在Java中,这是FileChannel。
然后根据需要在文件中跳转,并在屏幕上呈现数据的滚动窗口。
FileChannel的一个优点是并发线程可以打开文件,并且读取不会影响当前文件指针。因此,如果您在另一个线程中附加到日志文件,它将不会受到影响。
另一个优点是你可以随时调用FileChannel的size方法来获取文件大小。
将内存直接映射到随机访问文件(一些文本编辑器允许的文件(例如HxD和UltraEdit))的问题是任何更改都会直接影响文件。因此,更改是立即的(写入缓存除外),这是用户通常不想要的。相反,用户通常不希望在他们单击“保存”之前进行更改。但是,由于这只是一个观众,因此您没有同样的顾虑。
答案 1 :(得分:2)
一种典型的方法是使用可搜索文件阅读器,通过日志记录一行线偏移索引,然后根据要求在文件的一部分上只显示一个窗口。
这样可以减少快速调用所需的数据,也不会加载其中99%的内容当前不可见的小部件。
答案 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
}