はじめに
これはVisual Basic Advent Calendar 2017の23日の記事となります。
お仕事で外部アプリケーションが出力するログを監視し、ある条件なら警告画面を前面に表示させる要望がありました。
【.NET】外部アプリケーションの画面上すべてのタイトル(キャプション)を取得する
ログなので最終行を読み込む処理を作る必要があるなと先走って作成してみたところ、そのログは先頭に行を追加されるようになっていたため、作り損になってしまいました。
まー先頭行を取得する処理はすごく簡単なんですが、最終行を取得する処理は結構面倒なんです。
しかし、先頭に行を追加するってログって初めて見た。ログサイズが小さいからいいけど、全行をを読み込んで、先頭行に追加した上で全行を書き込む必要があるので処理コストが高いよね。何か理由があってそうしているんだろうけどさ。
Hey, Scripting Guy! テキスト ファイルの先頭に行を追加する方法はありますか
説明
Tail関数には引数として、ファイル名、読込行数(デフォルト=1)、文字コード(デフォルト=UTF-8)を指定します。
何か大きめなサイズのログがないかと検索してみたところ、以前インストールしたOracle Database 11g Express Edition(XE) のlistener.log が21MByteありましたので動かしてみました。
listener.log はUTF-8と思ったらShift-JISで作られてました。念のためサクラエディタでUTF-8に変換したのでも読み込みテストを行いました。
Dim path As String = "C:\oraclexe\app\oracle\diag\tnslsnr\LaVie_PC\listener\trace\listener.log"
Dim line As String = Tail(path, 6, "Shift-JIS")
MessageBox.Show(line)
string path = @"C:\oraclexe\app\oracle\diag\tnslsnr\LaVie_PC\listener\trace\listener.log"
string line = Tail(path, 6, "Shift-JIS");
MessageBox.Show(line);
出力結果
サクラエディタで「listener.log」を読み込み末尾を表示。
Tail関数で「listener.log」の末尾6行を読み込んだ。
ソースコード
VisualBasic版となります。
Public Shared Function Tail(ByVal path As String, ByVal Optional lines As Integer = 1, Optional ByVal encoding As String = "UTF-8") As String
Dim BUFFER_SIZE As Integer = 32
Dim offset As Integer = 0
Dim loc As Integer = 0
Dim foundCount As Integer = 0
Dim buffer = New Byte(BUFFER_SIZE - 1) {}
Dim isFirst As Boolean = True
Dim isFound As Boolean = False
Dim i As Integer = 0
Using fs = New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
While True
offset = Math.Min(CInt(fs.Length), (i + 1) * BUFFER_SIZE)
loc = 0
If fs.Length <= i * BUFFER_SIZE Then
If foundCount > 0 OrElse fs.Length > 0 Then Exit While
Throw New ArgumentOutOfRangeException("NOT FOUND DATA")
End If
fs.Seek(-offset, SeekOrigin.[End])
Dim readLength As Integer = offset - BUFFER_SIZE * i
For j As Integer = 0 To readLength - 1
j += fs.Read(buffer, j, readLength - j)
Next
For k As Integer = readLength - 1 To 0 Step -1
If buffer(k) = 10 Then
If isFirst AndAlso k = readLength - 1 Then Continue For
foundCount += 1
If foundCount = lines Then
loc = k + 1
isFound = True
Exit For
End If
End If
Next
isFirst = False
If isFound Then Exit While
i += 1
End While
fs.Seek(-offset + loc, SeekOrigin.[End])
Using sr = New StreamReader(fs, Text.Encoding.GetEncoding(encoding))
Return sr.ReadToEnd()
End Using
End Using
End Function
C#版となります。
public static string Tail(string path, int lines = 1, string encoding = "UTF-8")
{
int BUFFER_SIZE = 32; // バッファーサイズ(あえて小さく設定)
int offset = 0;
int loc = 0;
int foundCount = 0;
var buffer = new byte[BUFFER_SIZE];
bool isFirst = true;
bool isFound = false;
// ファイル共有モードで開く
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// 検索ブロック位置の繰り返し
for (int i = 0; ; i++)
{
// ブロック開始位置に移動
offset = Math.Min((int)fs.Length, (i + 1) * BUFFER_SIZE);
loc = 0;
if (fs.Length <= i * BUFFER_SIZE)
{
// ファイルの先頭まで達した場合
if (foundCount > 0 || fs.Length > 0) break;
// 行が未存在
throw new ArgumentOutOfRangeException("NOT FOUND DATA");
}
fs.Seek(-offset, SeekOrigin.End);
// ブロックの読み込み
int readLength = offset - BUFFER_SIZE * i;
for (int j = 0; j < readLength; j += fs.Read(buffer, j, readLength - j)) ;
// ブロック内の改行コードの検索
for (int k = readLength - 1; k >= 0; k--)
{
if (buffer[k] == 0x0A)
{
if (isFirst && k == readLength - 1) continue;
if (++foundCount == lines)
{
// 所定の行数が見つかった場合
loc = k + 1;
isFound = true;
break;
}
}
}
isFirst = false;
if (isFound) break;
}
// 見つかった場合
fs.Seek(-offset + loc, SeekOrigin.End);
using (var sr = new StreamReader(fs, Encoding.GetEncoding(encoding)))
{
return sr.ReadToEnd();
}
}
}
ライセンスっぽいこと
コード改変や配布は自由です。
このツールによる義務/責任を何ら負いません。
最後に
もともとstackoverflowの記事を参考にC#で作成したのを、この記事用にVisualBasicで書き直しました。
汎用っぽく末尾からn行にしたけど、最終行だけだったらシンプルに作れるような気もします。
あと、外部アプリケーション側で「別のプロセスで使用されているため、プロセスはファイル 'xxxxxx.xxx' にアクセスできません。」のエラーダイアログ画面がまれに表示されたため、別名ファイルにコピーした上で読む込むようにしました。