4742


テキストファイルの末尾から指定した行数のテキストを効率的に取り出したいです。  
Linux の tail
コマンドのようなものです。([こちらの質問](https://ja.stackoverflow.com/questions/4654/%e3%83%86%e3%82%ad%e3%82%b9%e3%83%88%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%e3%81%ae%e6%9c%ab%e5%b0%be%e3%81%8b%e3%82%89%ef%bd%8e%e8%a1%8c%e3%82%92%e8%aa%ad%e3%81%bf%e8%be%bc%e3%82%80%e3%81%ab%e3%81%af)
のC#版です)

簡単な実装方法として、

[code]

    var path = "test.txt";
    var n = 10;
    
    var lines = File.ReadAllLines(path);
    
    foreach (var line in lines.Skip(lines.Length - n).Take(n))
    {
        System.Console.WriteLine(line);
    }
    
[/code]

このようにファイルを先頭から全て読み込んで末尾のｎ行を取り出すということは出来るのですが、この方法だとファイルサイズが大きい時に処理時間が長くなってしまいます。ディスクI/Oやメモリ消費という面からも好ましくないと思います。

ファイルをランダムアクセスして末尾から１行ずつ取り出すにはどのような方法があるでしょうか？

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

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


CC BY-SA 3.0
2015-01-17T11:31:11.750
4740
2015-01-18T00:57:54.997
2017-04-13T12:52:38.920
-1
3925
question
c# テキストファイル file-io
テキストファイルの末尾からｎ行を読み込むには（C#）


以下の手順でどうでしょうか。

  1. `FileStream`を作成する
  2. `FileStream.Seek(-バッファーサイズ、SeekOrigin.End)`で末尾から一定の位置に移動する
  3. `FileStream.Read`で`byte[]`に値を読み込む(1回で完了しない可能性があるため繰り返し処理が必要です)
  4. バッファーの末尾からLF=`0x0A`を探す(UTF-8であればマルチバイト文字は必ず`0x80`以上です)
  5. 見つかったら`Seek`でLFの次のバイトに移動し、`StreamReader`で文字列にする。
  6. 見つからなかったら位置を変えて2からやり直す

コードを追加しました。

[code]

    static void Main(string[] args)
    {
        int BUFFER_SIZE = 32; // バッファーサイズ(あえて小さく設定)
        int lineCountToWrite = 10; // 探索行数
        var buffer = new byte[BUFFER_SIZE];
        var foundCount = 0;
        using (var fs = new FileStream(@"..\..\Program.cs", FileMode.Open))
        {
            // 検索ブロック位置の繰り返し
            for (var i = 0; ; i++)
            {
                if (fs.Length <= i * BUFFER_SIZE)
                {
                    // ファイルの先頭まで達した場合
                    Console.WriteLine("NOT FOUND");
                    Console.ReadKey();
                    return;
                }
    
                // ブロック開始位置に移動
                var offset = Math.Min((int)fs.Length, (i + 1) * BUFFER_SIZE);
                fs.Seek(-offset, SeekOrigin.End);
    
                // ブロックの読み込み
                var readLength = offset - BUFFER_SIZE * i;
                for (var j = 0; j < readLength; j += fs.Read(buffer, j, readLength - j)) ;
    
                // ブロック内の改行コードの検索
                for (var k = readLength - 1; k >= 0; k--)
                {
                    if (buffer[k] == 0x0A)
                    {
                        foundCount++;
                        if (foundCount == lineCountToWrite)
                        {
                            // 所定の行数が見つかった場合
    
                            fs.Seek(-offset + k + 1, SeekOrigin.End);
    
                            using (var sr = new StreamReader(fs, Encoding.UTF8))
                            {
                                Console.WriteLine(sr.ReadToEnd());
                            }
                            Console.ReadKey();
                            return;
                        }
                    }
                }
            }
        }
    }
    
[/code]


CC BY-SA 3.0
2015-01-17T12:08:17.117
4742
2015-01-17T12:38:28.547
2015-01-17T12:38:28.547
5750
5750
4740
answer


基本は以下のように考えれば良いかと。

  1. 先頭から1行づつ切り出す
  2. 切り出した行を最大n個、最後に切り出した行を優先して保持しておく  
→ n 個の可変長文字列変数を用意しておき、切り出した "行No%n" 番目に上書きしていけばOK  
(例えば n=3 なら 0→1→2→0→1→2… と保存先を変えていくイメージ)

  3. ファイルを最後まで読み終わったら時点で(2)で保存しておいた文字列が最後のn行に相当


CC BY-SA 3.0
2015-01-17T16:20:40.887
4757
2015-01-17T23:59:49.297
2015-01-17T23:59:49.297
4978
7504
4740
answer


tailコマンドであれば、１行は80文字程度など **ある程度**
のアタリを付けることができ、その仮定のもとpgrhoさんの回答のようにバッファーサイズを定めてアクセスすることもできます。  
しかしこれを **完全に**
一般化してしまい「１行の文字数に上限なし」としてしまうと、場合によってはファイルにｎ行含まれていない可能性も出てきますし、であればファイル先頭から読み込んでも構わない、ということもあり得ます。（tailも１行の上限はありませんがアタリをつけられるかどうか、です。）

ディスクI/Oを気にされていますが、C# /
Windowsにおいては[FileOptions.SequentialScan](http://msdn.microsoft.com/ja-
jp/library/system.io.fileoptions\(v=vs.110\).aspx)を指定することにより先頭から順次アクセスすることをOSに伝えることができ、OS側もその前提で先読みを行ってくれるため、ファイルサイズによっては十分なパフォーマンスが得られます。またメモリ消費についても一度にすべて読み込むFile.ReadAllLinesではなく、順次読み込み、読み捨てられる[File.ReadLines](http://msdn.microsoft.com/ja-
jp/library/system.io.file.readlines\(v=vs.110\).aspx)を用いることで消費と抑えることができます。

結局、条件をどこまで絞り込むことができるかで実装は変わってくると思います。またこの質問を振り返ってみると、質問者さんはJavaとC#とで質問されており、特定の言語での実装というよりはアルゴリズムを尋ねられているようにも見受けられます。その上で「tail
コマンドのようなもの」を質問されるのであれば、実際にtailコマンドのソースコードを参照されてはいかがでしょうか？


CC BY-SA 3.0
2015-01-18T00:57:54.997
4767
2015-01-18T00:57:54.997
4236
4740
answer