[BlueLeaf1336]> PROBLEMS> tail>

tail > ログファイルの末尾を監視したり

historyTOP

2005/02/10:作成
2005/02/11:試した読み込み方法をHTML化

2005/02/10TOP

tailというコマンドがあります。

自分の使ったことがある方法だけで限定的に紹介すると、大量に出力されるログファイルの末尾を指定した行数だけ表示しつづけるツールです。そして、ここでは、この異常に限定された機能だけをプログラムしてみようというわけです。

「あるテキストファイルの末尾10行(仮)を常に監視する」ためには、そのファイルが変更を受けたことを感知しなければなりません。また、テキストファイルを頭から読まずにいきなり最後の数行だけを読むことは(おそらく)できません。すると数メガバイトもあるようなファイルを監視しようとした時に、(変更が感知できたとして)毎回読み込むのか? という疑問も沸いてきます。

とりあえず、一番単純なのは、次のような感じで処理することだと思います。

  1. ファイルを指定する
  2. タイマーをセットして、変更されていようがいまいが、一定時間ごとに常に全部を読み込む
  3. 指定された行数だけを画面に表示する

「ファイル指定」「タイマーセット」については、TOpenDialog / TTimer でよいわけなので、とりあえず指定されたファイルをできるだけ早く読み込む方法について、少し調べます。実験はしません。検索するだけです。

指定サイズのテスト用ファイルを作成するTOP

まずその前に、読み込むためのファイルを作成することから。

//  指定したサイズのファイルを作成する
procedure TForm1.Button1Click(Sender: TObject);
var
    Target: TextFile;
    FileName: string;
    KiloText: array[0..1021] of char;   //  0..1021 で 1022 バイト
    i, j, MegaCount: integer;
begin
    //  作成するファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  指定されたサイズを変数に代入
    MegaCount := UpDown1.Position;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  変数にテキストファイルを割り当てる
    AssignFile(Target, FileName);

    try
        //  あろうがなかろうが作り直します
        Rewrite(Target);

        //  1 メガバイト = 1024 キロバイト = 1024 * 1024 バイト
        //  1 キロバイト分のテキストを作成
        FillChar(KiloText, SizeOf(KiloText), '*');

        //  指定された回数だけ、1メガバイトをくり返す
        for i := 0 to MegaCount - 1 do
        begin
            //  1キロバイトを1024回書き出したら1メガバイト
            for j := 0 to 1023 do
            begin
                //  CRLF分で1024 バイトになると思う
                //  String でキャストしないと末尾が気持悪いことになる(原因知らず)
                Writeln(Target, String(KiloText));
            end;
        end;
    finally
        //  割り当てを解放します
        CloseFile(Target);
    end;

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

テキストファイルの読み込みTOP

それから、ほぼそのままの読み込み処理です。時間計測したりサイズを表示したり余分なことをしてますが、ほぼそのままです。後で利用し易そうな気がするので、文字列リストに読み込むようにしています。

//  読み込む
//  http://130.158.124.192/~takeuchi/delphi/article/061/061078.html
procedure TForm1.Button2Click(Sender: TObject);
var
    FileName: string;
    FileStream: TFileStream;
    TickCount: Cardinal;
begin
    //  別のところで確保してます
    StringList.Clear;

    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  なければやめる
    if (not FileExists(FileName)) then Exit;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  時間計測開始
    LblTickCount.Caption := '---';
    LblFileSize.Caption  := '---';
    TickCount := GetTickCount();

    try
        //  ストリームにファイルを割り当てます(他からの全アクセスを許可)
        FileStream := TFileStream.Create(FileName, fmShareDenyNone);
        try
            //  文字列リストに読み込みます(とりあえずコレがゴール)
            StringList.LoadFromStream(FileStream);

            //  時間計測終了
            LblTickCount.Caption := FormatFloat('#,##0 msec', GetTickCount() - TickCount);
            LblFileSize.Caption := FormatFloat('#,##0 kbyte', FileStream.Size / 1024);
        finally
            FileStream.Free;
        end;
    except
        ;
    end;

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

try..finally..end / try..except..end をどこまで書くべきなのかを余りしっかり理解していないので、ネストが深くすごく深くなってますが、やってることは参考にしたサイトのとおりです。

比較などやってみるもよくわからずTOP

実行結果はこんな感じでした。50メガバイトを1.7秒弱。16メガバイトで0.5秒弱。あれ? 16メガバイトで0.1秒って...。

ということで、その他の何個かのやり方を試してみました。意外とLoadFromFileが早くてなんかこう、それでええやん、みたいな感じになったりして。

想像ですが、最終的に TStringList に読み込みたいわけで、これは最後の5行といった行アクセスをやりたいからなんですが、そうであるなら、直接クラスが読むのが早いわなぁ、という感じがします。

20050210tail_test.zip(199,356bytes)

とりあえず、ここで書いたやり方が記述が単純な気がします。ただ、1行ずつ読み込んで解釈しながらというのであれば、一括読み込みはないと思います。ただ、読んだ後で1行ずつアクセスという手も可能です。早い話、わかりやすいほうが吉、というところでしょうか。

というわけで、読み込みについては、毎回読み込んでも大丈夫そう、ということにします。

おまけ。while not Eof / ReadLn を使用して読み込むTOP

//  別の方法
procedure TForm1.Button3Click(Sender: TObject);
var
    FileName, Line: string;
    Target: TextFile;
    TickCount: Cardinal;
begin
    //  別のところで確保してます
    StringList.Clear;

    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  なければやめる
    if (not FileExists(FileName)) then Exit;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  時間計測開始
    LblTickCount2.Caption := '---';
    TickCount := GetTickCount();

    try
        //  変数にテキストファイルを割り当てる
        AssignFile(Target, FileName);
        try
            //  ファイルを開きます
            Reset(Target);

            //  文字列リストに読み込みます(とりあえずコレがゴール)
            while not Eof(Target) do
            begin
                Readln(Target, Line);
                StringList.Add(Line);
            end;

            //  時間計測終了
            LblTickCount2.Caption := FormatFloat('#,##0 msec', GetTickCount() - TickCount);
        finally
            CloseFile(Target);
        end;
    except
        ;
    end;

    //  確認出力
    //StringList.SaveToFile(ChangeFileExt(FileName, '.2'));

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

おまけ。TStringList.LoadFromFile を使用して読み込むTOP

//  もっと別の方法
procedure TForm1.Button4Click(Sender: TObject);
var
    FileName: string;
    TickCount: Cardinal;
begin
    //  別のところで確保してます
    StringList.Clear;

    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  なければやめる
    if (not FileExists(FileName)) then Exit;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  時間計測開始
    LblTickCount3.Caption := '---';
    TickCount := GetTickCount();

    try
        StringList.LoadFromFile(FileName);

        //  時間計測終了
        LblTickCount3.Caption := FormatFloat('#,##0 msec', GetTickCount() - TickCount);
    except
        ;
    end;

    //  確認出力
    //StringList.SaveToFile(ChangeFileExt(FileName, '.3'));

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

おまけ。API を使用して読み込むTOP

//  すごく適当
//  http://web.archive.org/web/20040220034606/http://www.bekkoame.ne.jp/~ilgg/VBMain/VB/Folder/ReadFile.html
procedure TForm1.Button5Click(Sender: TObject);
var
    FileName: string;
    TickCount: Cardinal;
    Handle: THandle;
    FileSize, ReadSize: Cardinal;
    SearchRec: TSearchRec;
    Buf: string;
begin
    //  別のところで確保してます
    StringList.Clear;

    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  なければやめる
    if (not FileExists(FileName)) then Exit;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  時間計測開始
    LblTickCount4.Caption := '---';
    TickCount := GetTickCount();

    //  ファイルサイズ取得
    FileSize := 0;
    if FindFirst(FileName, faAnyFile, SearchRec) = 0 then
    begin
        FileSize := SearchRec.Size;
        FindClose(SearchRec);
    end;

    if (FileSize > 0) then
    begin
        try
            Handle := CreateFile(PChar(FileName), GENERIC_READ,
                        FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
                        nil, OPEN_EXISTING, 0, 0);

            SetLength(Buf, FileSize);

            if (ReadFile(Handle, Buf[1], FileSize, ReadSize, nil)) then
            begin
                StringList.Text := Buf;
                CloseHandle(Handle);
            end;

            //  時間計測終了
            LblTickCount4.Caption := FormatFloat('#,##0 msec', GetTickCount() - TickCount);
        except
            ;
        end;
    end;

    //  確認出力
    //StringList.SaveToFile(ChangeFileExt(FileName, '.4'));

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

おまけ。BlockRead を使用して読み込むTOP

//  http://homepage1.nifty.com/MADIA/delphi/delphi_bbs/200207_02070035.html
procedure TForm1.Button6Click(Sender: TObject);
var
    FileName: string;
    TickCount: Cardinal;
    F: File;
    FileSize, ReadSize: Cardinal;
    SearchRec: TSearchRec;
    Buf: string;
begin
    //  別のところで確保してます
    StringList.Clear;

    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');

    //  なければやめる
    if (not FileExists(FileName)) then Exit;

    //  カーソルを変えておきます
    Screen.Cursor := crHourGlass;

    //  時間計測開始
    LblTickCount5.Caption := '---';
    TickCount := GetTickCount();

    //  ファイルサイズ取得
    FileSize := 0;
    if FindFirst(FileName, faAnyFile, SearchRec) = 0 then
    begin
        FileSize := SearchRec.Size;
        FindClose(SearchRec);
    end;

    if (FileSize > 0) then
    begin
        try
            AssignFile(F, FileName);
            try
                SetLength(Buf, FileSize);
                Reset(F, 1);
                BlockRead(F, PChar(Buf)^, FileSize, ReadSize);
                StringList.Text := Buf;
            finally
                CloseFile(F);
            end;

            //  時間計測終了
            LblTickCount5.Caption := FormatFloat('#,##0 msec', GetTickCount() - TickCount);
        except
            ;
        end;
    end;

    //  確認出力
    //StringList.SaveToFile(ChangeFileExt(FileName, '.5'));

    //  カーソルを元に戻す
    Screen.Cursor := crDefault;
end;

EOFTOP