4680


テキストファイルの末尾から指定した行数のテキストを効率的に取り出したいです。  
Linux の tail コマンドのようなものです。

[code]

    File file = new File("c:¥¥tmp¥¥test.txt");
    BufferedReader br = new BufferedReader(new FileReader(file));
    
    String str;
    while((str = br.readLine()) != null){
      ・・・
    }
    
    br.close();
    
[/code]

このようなファイルの先頭から順番に１行ずつ取得する方法を応用して最後のｎ行を取り出すことは出来るのですが、この方法だとファイルサイズが大きい時に処理時間が長くなってしまいます。  
ファイルの末尾から１行ずつ取り出す方法はあるでしょうか？

対象のテキストファイルは以下の想定です。

  * 文字コードは UTF-8
  * 改行はCRLF、LFが混在
  * １行の文字数に上限なし


CC BY-SA 3.0
2015-01-16T03:45:31.497
4654
2015-01-16T09:03:02.210
3925
question
java テキストファイル file-io
テキストファイルの末尾からｎ行を読み込むには


RandomAccessFileを使って末尾から読んでいく方法はどうでしょうか？  
<http://docs.oracle.com/javase/jp/7/api/java/io/RandomAccessFile.html>

// コードは後ほど時間がとれたら追加します


CC BY-SA 3.0
2015-01-16T03:50:44.060
4655
2015-01-16T03:50:44.060
845
4654
answer


ファイルをシーケンシャルに読んでいくのではなく、ランダムアクセスする必要があります。  
`SeekableByteChannel(Java7)`を使用し、ファイル位置を指定しながら必要なデータを末尾から取得します。

[code]

    // 読み込みモード
    Path path = Paths.get("./", "test.dat");
    SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ);
    // ファイルサイズ取得
    long fileSize = channnel.size();
    
    // 取得のためのバッファを用意
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    // 末尾(fileSize-1)から1024byteを取得
    channel.position(fileSize - 1 - 1024);
    channel.read(buffer);
    
[/code]

あとはアクセスしている位置を保持し、繰り返し取得しながら改行コード(データを逆から辿るならば`CRLF`と`LF`が混在していたとしても`LF='\n'`の個数のみをカウントすれば良い)を見つけて必要な行数分データを取っていくロジックにすれば良いです(このロジックの場合、ファイルサイズが1024byteより小さい場合の考慮が必要となります)。

取得したバイトデータを指定された文字コードで変換するには以下のようにします。

[code]

    new String(buffer.array(), "UTF-8");
    
[/code]

この変換処理は欲しいデータ分たまったあと一気にすれば良いです(指定行数分の取得ロジック部では改行コードのバイトデータをチェックしていれば良い)。


CC BY-SA 3.0
2015-01-16T05:18:22.140
4665
2015-01-16T09:03:02.210
2015-01-16T09:03:02.210
7214
7214
4654
answer


Java8ならFiles.linesが良いかと思います。  
<http://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#lines-
java.nio.file.Path->

それ以前ならjava.io.LineNumberReaderクラスを使うのはどうでしょう。  
<http://docs.oracle.com/javase/7/docs/api/java/io/LineNumberReader.html>


CC BY-SA 3.0
2015-01-16T06:05:44.690
4668
2015-01-16T06:05:44.690
7456
4654
answer


[Apache Commons-IO](https://commons.apache.org/proper/commons-io/) ライブラリの
[`org.apache.commons.io.input`](https://commons.apache.org/proper/commons-
io/javadocs/api-release/index.html?org/apache/commons/io/input/package-
summary.html) パッケージに参考になりそうなものがありました。

  * [`ReversedLinesFileReader`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/ReversedLinesFileReader.html): テキストファイルを末尾から読むクラス
  * [`Tailer`](https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/Tailer.html) : tail のようにファイル監視機能付の末尾出力の仕組み

仕様は、[`FileReader`](https://docs.oracle.com/javase/8/docs/api/java/io/FileReader.html)
に似ているので、使い方が直観的です。

ReadFromTail.java:

[code]

    import org.apache.commons.io.input.ReversedLinesFileReader;
    import java.io.*;
    
    public class ReadFromTail {
      public static void main(String[] args) throws IOException {
        try (ReversedLinesFileReader r = new ReversedLinesFileReader( new File(args[0]))) {
            System.out.println(r.readLine());
        }
      }
    }
    
[/code]

[code]

    $ javac -cp commons-io-2.4.jar ReadFromTail.java
    $ java -cp commons-io-2.4.jar ReadFromTail c:¥tmp¥test.txt
    I'm the Last Line!!!
    
[/code]

`ReversedLinesFileReader` のソースを見ると、デフォルト 4kb
でファイル全体をブロックに分割されているとしてから、末尾ブロックから読みだしていました。 読みだし位置の指定は、
[`RandomAccessFile.seek()`](http://docs.oracle.com/javase/8/docs/api/java/io/RandomAccessFile.html#seek-
long-) です。他に、UTF-8 や Shift-JIS エンコーディングに対応する部分が興味を引く感じ。

ReversedLinesFileReader.java (抜粋):

[code]

    ...
    
    randomAccessFile = new RandomAccessFile(file, "r");
    totalByteLength = randomAccessFile.length();
    int lastBlockLength = (int) (totalByteLength % blockSize);
    if (lastBlockLength > 0) {
        totalBlockCount = totalByteLength / blockSize + 1;
    } else {
        totalBlockCount = totalByteLength / blockSize;
        if (totalByteLength > 0) {
            lastBlockLength = blockSize;
        }
    }
    currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
    
    ...
    
    int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
    this.data = new byte[dataLength];
    final long off = (no - 1) * blockSize;
    
    // read data
    if (no > 0 /* file not empty */) {
        randomAccessFile.seek(off);
        final int countRead = randomAccessFile.read(data, 0, length);
    
    ...
    
[/code]


CC BY-SA 3.0
2015-01-16T08:11:06.443
4680
2015-01-16T08:11:06.443
4978
4654
answer