Быстро прочитать последнюю строку текстового файла?

Какой самый быстрый и эффективный способ чтения последней строки текста из [очень, очень большого] файла в Java?

11 ответов

Решение

Посмотрите на мой ответ на аналогичный вопрос для C#. Код был бы очень похож, хотя поддержка кодирования в Java несколько иная.

В общем, это не очень легко сделать в целом. Как указывает MSalter, UTF-8 позволяет легко обнаружить \r или же \n поскольку представление этих символов в UTF-8 точно такое же, как в ASCII, и эти байты не будут иметь многобайтовый символ.

Таким образом, в основном, возьмите буфер (скажем) 2 КБ и постепенно читайте назад (перейдите к 2 КБ, прежде чем вы были раньше, прочитайте следующие 2 КБ), проверяя завершение строки. Затем перейдите в нужное место в потоке, создайте InputStreamReader на вершине, и BufferedReader более того. Тогда просто позвоните BufferedReader.readLine(),

Ниже приведены две функции, одна из которых возвращает последнюю непустую строку файла без загрузки или пошагового выполнения по всему файлу, а другая возвращает последние N строк файла без пошагового выполнения по всему файлу:

Хвост выполняет масштабирование до последнего символа файла, затем шаг за шагом, символ за символом, записывает то, что видит, пока не обнаружит разрыв строки. Как только он находит разрыв строки, он выходит из цикла. Переворачивает то, что было записано, бросает его в строку и возвращает. 0xA - это новая строка, а 0xD - возврат каретки.

Если ваши окончания строки \r\n или же crlf или какой-то другой "двойной перевод новой строки в стиле новой строки", тогда вам нужно будет указать n*2 строки, чтобы получить последние n строк, потому что он считает 2 строки для каждой строки.

public String tail( File file ) {
    RandomAccessFile fileHandler = null;
    try {
        fileHandler = new RandomAccessFile( file, "r" );
        long fileLength = fileHandler.length() - 1;
        StringBuilder sb = new StringBuilder();

        for(long filePointer = fileLength; filePointer != -1; filePointer--){
            fileHandler.seek( filePointer );
            int readByte = fileHandler.readByte();

            if( readByte == 0xA ) {
                if( filePointer == fileLength ) {
                    continue;
                }
                break;

            } else if( readByte == 0xD ) {
                if( filePointer == fileLength - 1 ) {
                    continue;
                }
                break;
            }

            sb.append( ( char ) readByte );
        }

        String lastLine = sb.reverse().toString();
        return lastLine;
    } catch( java.io.FileNotFoundException e ) {
        e.printStackTrace();
        return null;
    } catch( java.io.IOException e ) {
        e.printStackTrace();
        return null;
    } finally {
        if (fileHandler != null )
            try {
                fileHandler.close();
            } catch (IOException e) {
                /* ignore */
            }
    }
}

Но вам, вероятно, не нужна последняя строка, вам нужны последние N строк, поэтому используйте это вместо:

public String tail2( File file, int lines) {
    java.io.RandomAccessFile fileHandler = null;
    try {
        fileHandler = 
            new java.io.RandomAccessFile( file, "r" );
        long fileLength = fileHandler.length() - 1;
        StringBuilder sb = new StringBuilder();
        int line = 0;

        for(long filePointer = fileLength; filePointer != -1; filePointer--){
            fileHandler.seek( filePointer );
            int readByte = fileHandler.readByte();

             if( readByte == 0xA ) {
                if (filePointer < fileLength) {
                    line = line + 1;
                }
            } else if( readByte == 0xD ) {
                if (filePointer < fileLength-1) {
                    line = line + 1;
                }
            }
            if (line >= lines) {
                break;
            }
            sb.append( ( char ) readByte );
        }

        String lastLine = sb.reverse().toString();
        return lastLine;
    } catch( java.io.FileNotFoundException e ) {
        e.printStackTrace();
        return null;
    } catch( java.io.IOException e ) {
        e.printStackTrace();
        return null;
    }
    finally {
        if (fileHandler != null )
            try {
                fileHandler.close();
            } catch (IOException e) {
            }
    }
}

Вызовите вышеупомянутые методы как это:

File file = new File("D:\\stuff\\huge.log");
System.out.println(tail(file));
System.out.println(tail2(file, 10));

Предупреждение На диком западе юникода этот код может привести к неправильному выводу этой функции. Например, "Мэри?" Вместо "Мэри". Символы с шляпами, акцентами, китайскими и т. Д. Могут привести к неправильному выводу, потому что акценты добавляются в качестве модификаторов после символа. Реверсирование составных символов меняет характер личности персонажа при обращении. Вам нужно будет выполнить полный набор тестов на всех языках, с которыми вы планируете его использовать.

Для получения дополнительной информации об этой проблеме обращения Unicode прочитайте это: http://msmvps.com/blogs/jon_skeet/archive/2009/11/02/omg-ponies-aka-humanity-epic-fail.aspx

Apache Commons имеет реализацию, использующую RandomAccessFile.

Это называется ReversedLinesFileReader.

Использование FileReader или FileInputStream не будет работать - вам придется использовать либо FileChannel, либо RandomAccessFile, чтобы перебирать файл в обратном направлении от конца. Хотя кодирование будет проблемой, как сказал Джон.

Вы можете легко изменить приведенный ниже код для печати последней строки.

MemoryMappedFile для печати последних 5 строк:

private static void printByMemoryMappedFile(File file) throws FileNotFoundException, IOException{
        FileInputStream fileInputStream=new FileInputStream(file);
        FileChannel channel=fileInputStream.getChannel();
        ByteBuffer buffer=channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        buffer.position((int)channel.size());
        int count=0;
        StringBuilder builder=new StringBuilder();
        for(long i=channel.size()-1;i>=0;i--){
            char c=(char)buffer.get((int)i);
            builder.append(c);
            if(c=='\n'){
                if(count==5)break;
                count++;
                builder.reverse();
                System.out.println(builder.toString());
                builder=null;
                builder=new StringBuilder();
            }
        }
        channel.close();
    }

RandomAccessFile для печати последних 5 строк:

private static void printByRandomAcessFile(File file) throws FileNotFoundException, IOException{
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        int lines = 0;
        StringBuilder builder = new StringBuilder();
        long length = file.length();
        length--;
        randomAccessFile.seek(length);
        for(long seek = length; seek >= 0; --seek){
            randomAccessFile.seek(seek);
            char c = (char)randomAccessFile.read();
            builder.append(c);
            if(c == '\n'){
                builder = builder.reverse();
                System.out.println(builder.toString());
                lines++;
                builder = null;
                builder = new StringBuilder();
                if (lines == 5){
                    break;
                }
            }

        }
    }

Насколько я знаю, самый быстрый способ прочитать последнюю строку текстового файла - это использовать класс Apache FileUtils, который находится в "org.apache.commons.io". У меня есть файл с двумя миллионами строк, и с помощью этого класса мне понадобилось менее одной секунды, чтобы найти последнюю строку. Вот мой код:

LineIterator lineIterator = FileUtils.lineIterator(newFile(filePath),"UTF-8");
String lastLine="";
while (lineIterator.hasNext()){
 lastLine=  lineIterator.nextLine();
}
Path path = Paths.get(pathString);
      List<String> allLines = Files.readAllLines(path);
      return allLines.get(allLines.size()-1);

Код всего 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>
try(BufferedReader reader = new BufferedReader(new FileReader(reqFile))) {

    String line = null;

    System.out.println("======================================");

    line = reader.readLine();       //Read Line ONE
    line = reader.readLine();       //Read Line TWO
    System.out.println("first line : " + line);

    //Length of one line if lines are of even length
    int len = line.length();       

    //skip to the end - 3 lines
    reader.skip((reqFile.length() - (len*3)));

    //Searched to the last line for the date I was looking for.

    while((line = reader.readLine()) != null){

        System.out.println("FROM LINE : " + line);
        String date = line.substring(0,line.indexOf(","));

        System.out.println("DATE : " + date);      //BAM!!!!!!!!!!!!!!
    }

    System.out.println(reqFile.getName() + " Read(" + reqFile.length()/(1000) + "KB)");
    System.out.println("======================================");
} catch (IOException x) {
    x.printStackTrace();
}

В C# вы должны иметь возможность установить позицию потока:

От: http://bytes.com/groups/net-c/269090-streamreader-read-last-line-text-file

using(FileStream fs = File.OpenRead("c:\\file.dat"))
{
    using(StreamReader sr = new StreamReader(fs))
    {
        sr.BaseStream.Position = fs.Length - 4;
        if(sr.ReadToEnd() == "DONE")
            // match
    }
}

Чтобы избежать проблем с Unicode, связанных с возвратом строки (или StringBuilder), как обсуждалось в отличном ответе , можно прочитать список байтов с конца файла, вернуть его в массив байтов, а затем создать строку из байтового массива.

Ниже приведены изменения в коде ответа Эрика ЛещинскиЭрика Лещинского , чтобы сделать это с помощью байтового массива. Изменения кода приведены ниже прокомментированных строк кода:

      static public String tail2(File file, int lines) {
    java.io.RandomAccessFile fileHandler = null;
    try {
        fileHandler = new java.io.RandomAccessFile( file, "r" );
        long fileLength = fileHandler.length() - 1;
        //StringBuilder sb = new StringBuilder();
        List<Byte> sb = new ArrayList<>();
        int line = 0;

        for(long filePointer = fileLength; filePointer != -1; filePointer--){
            fileHandler.seek( filePointer );
            int readByte = fileHandler.readByte();

            if( readByte == 0xA ) {
                if (filePointer < fileLength) {
                    line = line + 1;
                }
            } else if( readByte == 0xD ) {
                if (filePointer < fileLength-1) {
                    line = line + 1;
                }
            }
            if (line >= lines) {
                break;
            }
            //sb.add( (char) readByte );
            sb.add( (byte) readByte );
        }

        //String lastLine = sb.reverse().toString();
        //Revert byte array and create String
        byte[] bytes = new byte[sb.size()];
        for (int i=0; i<sb.size(); i++) bytes[sb.size()-1-i] = sb.get(i);
        String lastLine = new String(bytes);
        return lastLine;
    } catch( java.io.FileNotFoundException e ) {
        e.printStackTrace();
        return null;
    } catch( java.io.IOException e ) {
        e.printStackTrace();
        return null;
    }
    finally {
        if (fileHandler != null )
            try {
                fileHandler.close();
            } catch (IOException e) {
            }
    }
}
Другие вопросы по тегам