Java: чтение последних n строк ОГРОМНОГО файла
Я хочу прочитать последние n строк очень большого файла, не читая весь файл в любой области буфера / памяти, используя Java.
Я осмотрел API-интерфейсы JDK и ввод-вывод Apache Commons и не могу найти тот, который подходит для этой цели.
Я думал о том, как tail или менее делает это в UNIX. Я не думаю, что они загружают весь файл и затем показывают последние несколько строк файла. Должен быть похожий способ сделать то же самое в Java.
15 ответов
Если вы используете RandomAccessFile
, ты можешь использовать length
а также seek
чтобы добраться до определенной точки в конце файла, а затем продолжить чтение оттуда.
Если вы обнаружите, что строк недостаточно, вернитесь назад и попробуйте снова. Как только вы выяснили, где N
Последняя строка начинается, вы можете искать там и просто читать и печатать.
Исходное предположение о наилучшем предположении может быть сделано на основе ваших свойств данных. Например, если это текстовый файл, возможно, длина строк не будет превышать в среднем 132, поэтому, чтобы получить последние пять строк, начните с 660 символов до конца. Затем, если вы ошиблись, попробуйте еще раз на 1320 (вы можете даже использовать то, что вы узнали из последних 660 символов, чтобы отрегулировать это - пример: если эти 660 символов были всего лишь тремя строками, следующая попытка может быть 660 / 3 * 5, плюс может быть немного больше на всякий случай).
Я нашел это самый простой способ сделать с помощью ReversedLinesFileReader
от apache commons-io api. Этот метод даст вам строку снизу вверх файла, и вы можете указать 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 - хорошее место для начала, как описано в других ответах. Здесь есть одно важное предостережение.
Если ваш файл не закодирован с кодированием по одному байту на символ, readLine()
метод не будет работать для вас. А также readUTF()
не будет работать ни при каких обстоятельствах. (Это читает строку, которой предшествует число символов...)
Вместо этого вам нужно убедиться, что вы ищете маркеры конца строки таким образом, чтобы соблюдать границы символов кодировки. Для кодировок фиксированной длины (например, разновидности UTF-16 или UTF-32) необходимо извлечь символы, начиная с позиций байтов, которые делятся на размер символа в байтах. Для кодировок переменной длины (например, UTF-8) вам необходимо найти байт, который должен быть первым байтом символа.
В случае UTF-8 первый байт символа будет 0xxxxxxx
или же 110xxxxx
или же 1110xxxx
или же 11110xxx
, Все остальное является либо вторым / третьим байтом, либо недопустимой последовательностью UTF-8. См . Стандарт Unicode, Версия 5.2, Глава 3.9, Таблица 3-7. Это означает, что, как отмечается в комментариях, любые байты 0x0A и 0x0D в правильно закодированном потоке UTF-8 будут представлять символ LF или CR. Таким образом, простой подсчет байтов 0x0A и 0x0D является допустимой стратегией реализации (для UTF-8), если мы можем предположить, что другие виды разделителя строк Unicode (0x2028, 0x2029 и 0x0085) не используются. Вы не можете предположить это, тогда код был бы более сложным.
Определив правильную границу персонажа, вы можете просто позвонить new String(...)
передача байтового массива, смещения, количества и кодировки, а затем повторно вызвать String.lastIndexOf(...)
посчитать конец строки.
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();
}
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());
}
}
}
Вот пример без зависимости от Apache и результаты, которые я получил при чтении последних 90 000 строк из файла со 100 000 строк:
Этот метод: 50 мс
Apache ReversedLinesFileReader: 900 мс
RandomAccessFile (чтение в обратном направлении): 1200 мс
public static String[] getLastNLinesFromFile(String filePath, int numLines) throws IOException {
try (Stream<String> stream = Files.lines(Paths.get(filePath))) {
AtomicInteger offset = new AtomicInteger();
String[] lines = new String[numLines];
stream.forEach(line -> {
lines[offset.getAndIncrement() % numLines] = line;
});
List<String> list = IntStream.range(offset.get() < numLines ? 0 : offset.get() - numLines, offset.get())
.mapToObj(idx -> lines[idx % numLines]).collect(Collectors.toList());
return list.toArray(new String[0]);
}
}
CircularFifoBuffer из Apache Commons. ответ на аналогичный вопрос в Как прочитать последние 5 строк.txt файла в Java
Обратите внимание, что в Apache Commons Collections 4 этот класс, кажется, был переименован в CircularFifoQueue
Вот работа для этого.
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();
}
}
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) {
//
}
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();
}
Код всего 2 строки
// Please specify correct Charset
ReversedLinesFileReader rlf = new ReversedLinesFileReader(file, StandardCharsets.UTF_8);
// read last 2 lines
System.out.println(rlf.toString(2));
Грейдл:
implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'
Мейвен:
<dependency>
<groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version>
</dependency>
Сначала я попробовал RandomAccessFile, и было утомительно читать файл в обратном направлении, перемещая указатель файла при каждой операции чтения. Итак, я попробовал решение @Luca и получил последние несколько строк файла в виде строки всего за две строки за несколько минут.
InputStream inputStream = Runtime.getRuntime().exec("tail " + path.toFile()).getInputStream();
String tail = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining(System.lineSeparator()));