【入門編】【実務中級者向け】巨大ログの末尾から特定キーワードを検索するためのバッファ制御・テキスト逆走パース技術 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

巨大ログに立ち向かう:VBScriptで「メモリを食いつぶさない」逆走パース術

こんにちは。現場の自動化を支えるエンジニアの皆さん。
これまで「マクロの記録」や、`FileSystemObject` (FSO) の `ReadAll` メソッドを使って、サクッとファイルを読み込む楽しさを味わってきたことでしょう。

しかし、実務の荒波に揉まれていると、ある日突然、牙を剥いてくる壁があります。
それは「数GBを超える巨大なログファイル」です。

「ファイル全体を読み込んで、エラー行を探す」
そんな、これまで通りの書き方をしてはいけません。実行した瞬間に `Out of Memory`(メモリ不足)のエラーが噴出し、あなたのスクリプトは力尽きてしまいます。

今日は、そんなプロフェッショナルな現場で必須となる、「メモリを最小限に抑えつつ、ファイルの末尾(最新データ)から逆方向にキーワードを検索する」という、中級者への登竜門となる技術を伝授しましょう。

ここをクリアすれば、あなたのVBScriptは「単なる便利ツール」から「堅牢なシステム基盤」へと進化します。さあ、一緒に見ていきましょう。

1. なぜ `ReadAll` ではいけないのか?(メモリの重みを知る)

まず、なぜ従来のやり方が通用しないのかを整理しておきましょう。

従来のやり方(初級編)

1. `FSO.OpenTextFile` でファイルを開く。
2. `.ReadAll` でファイルの中身をすべて変数に格納する。
3. 変数の中から `InStr` などでキーワードを探す。

【致命的な問題点】
ファイルサイズが 2GB ある場合、その 2GB を丸ごとメモリ(RAM)に載せようとします。VBScriptが動作しているプロセスに割り当てられるメモリには限界があり、OS全体のパフォーマンスを低下させるだけでなく、スクリプト自体が即座にクラッシュします。

理想的なやり方(中級編:バッファ制御)

1. ファイルの末尾へ直接ジャンプする(シーク操作)。
2. ファイルの末尾から「一定のサイズ(例:4KB)」だけをバッファとして読み込む。
3. そのバッファ内だけでキーワードを探す。
4. 見つからなければ、さらに前方のバッファへ移動して繰り返す。

これなら、ファイルが 100GB あろうとも、メモリ消費量は常に「数KB」で済みます。これが「バッファ制御」の真髄です。

2. 武器の選択:FSOではなく `ADODB.Stream` を使う理由

ここで一つ、重要な技術的判断があります。
実は、標準的な `FileSystemObject` (FSO) は、ファイルの「特定のバイト位置へ戻る」というシーク操作が非常に苦手です。

そこで、我々プロフェッショナルは `ADODB.Stream` というオブジェクトを呼び出します。これは本来データベース接続用のものですが、バイナリデータやテキストデータを「ストリーム(流れ)」として、バイト単位で精密に制御することができる、最強の武器なのです。

3. 実装:逆走パース・アルゴリズム

では、具体的なコードを見ていきましょう。このコードは、巨大なログファイルの末尾から遡って、指定したキーワードを検索するものです。

'**
' 巨大ログ逆走検索スクリプト
' 概要: ファイルの末尾から一定のバッファサイズずつ遡り、キーワードを検索する
'**
Option Explicit

Dim targetFile, keyword, bufferSize
targetFile = "C:\Logs\huge_system_log.txt" ' 調査対象の巨大ファイル
keyword    = "ERROR"                       ' 探したいキーワード
bufferSize = 4096                          ' 1回に読み込むサイズ (4KB)

' 実行
Dim foundPos
foundPos = SearchKeywordFromEnd(targetFile, keyword, bufferSize)

If foundPos <> -1 Then
    MsgBox "キーワードが見つかりました! ファイル末尾からの位置: " & foundPos & " バイト付近"
Else
    MsgBox "キーワードは見つかりませんでした。"
End If

'------------------------------------------------------------------------------
' 関数: SearchKeywordFromEnd
' 引数: strPath (ファイルパス), strKey (検索語), lngBufferSize (バッファサイズ)
' 戻り値: 見つかった位置(バイト数)または -1
'------------------------------------------------------------------------------
Function SearchKeywordFromEnd(strPath, strKey, lngBufferSize)
    Dim objStream, objFSO, lngFileSize, lngCurrentPos
    Dim strBuffer, lngMatchPos
    
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    If Not objFSO.FileExists(strPath) Then
        Err.Raise 1, , "ファイルが存在しません。"
    End If

    ' ADODB.Streamの生成
    Set objStream = CreateObject("ADODB.Stream")
    
    ' バイナリモードで開く(バイト単位の制御を行うため)
    objStream.Type = 1 ' adTypeBinary
    objStream.Open
    objStream.LoadFromFile strPath
    
    lngFileSize = objStream.Size
    lngCurrentPos = lngFileSize
    
    ' 検索ループ
    Do While lngCurrentPos > 0
        ' 読み込み開始位置を計算(ファイルの先頭を越えないように制御)
        Dim lngReadStart
        lngReadStart = lngCurrentPos - lngBufferSize
        If lngReadStart < 0 Then lngReadStart = 0
        
        ' バッファの長さを計算
        Dim lngReadLen
        lngReadLen = lngCurrentPos - lngReadStart
        
        ' 指定位置へシーク(移動)
        objStream.Position = lngReadStart
        
        ' バッファ分をバイナリとして読み込む
        Dim binData
        Set binData = objStream.Read(lngReadLen)
        
        ' バイナリをテキストに変換(ここがポイント!)
        ' ※簡易化のため、ADODB.Streamを再度テキストモードで開いて変換する手法をとります
        Dim strChunk
        strChunk = ConvertBinaryToString(binData)
        
        ' キーワードが含まれているかチェック
        lngMatchPos = InStr(strChunk, strKey)
        
        If lngMatchPos > 0 Then
            ' 見つかった場合、その位置を計算して返す
            ' (ファイルの絶対位置 = 読み込み開始位置 + 見つかった位置)
            ' ※文字コードの影響を受けるため、概算値としての返却
            SearchKeywordFromEnd = lngReadStart + lngMatchPos
            objStream.Close
            Exit Function
        End If
        
        ' 見つからなければ、読み込み開始位置をさらに前に進める
        lngCurrentPos = lngReadStart
        
        ' 無限ループ防止(ファイル先頭に到達したら終了)
        If lngReadStart = 0 Then Exit Do
    Loop

    objStream.Close
    SearchKeywordFromEnd = -1
End Function

'------------------------------------------------------------------------------
' 関数: ConvertBinaryToString
' バイナリデータをテキスト文字列に変換する補助関数
'------------------------------------------------------------------------------
Function ConvertBinaryToString(binData)
    Dim objTempStream
    Set objTempStream = CreateObject("ADODB.Stream")
    
    ' 一旦バイナリとして書き込む
    objTempStream.Type = 1 ' adTypeBinary
    objTempStream.Open
    objTempStream.Write binData
    
    ' テキストモードに切り替えて読み取る
    objTempStream.Type = 2 ' adTypeText
    objTempStream.Position = 0
    
    ' 文字コードを指定(実務に合わせて Shift-JIS や UTF-8 を選択)
    ' ここでは一般的な Shift-JIS (Windows標準) を想定
    objTempStream.Charset = "shift-jis"
    
    ConvertBinaryToString = objTempStream.ReadText
    objTempStream.Close
End Function

4. コードの解説と「プロの勘所」

このコードの心臓部は、「バイナリで位置を制御し、テキストとして解釈する」という二段構えの構造にあります。

① バイナリモードでのシーク

`objStream.Type = 1` (adTypeBinary) を指定することで、ファイルの中身を「文字」ではなく「純粋なバイトの塊」として扱います。これにより、`objStream.Position = lngReadStart` と書くだけで、ファイルのどこへでも、1バイトの狂いもなくジャンプできるのです。

② バッファの境界問題(注意点!)

中級者へのステップアップとして、一つ重要な課題を提示します。
上記のコードでは、バッファ(4KB)の区切り目で、「検索したいキーワードが泣き別れになる」可能性があります。

*例:バッファの末尾が `ERR` で、次のバッファの先頭が `OR` だった場合、`ERROR` という単語は分断されて見つかりません。*

【実務での対策】
より完璧なアルゴリズムを目指すなら、バッファを読み込む際に「前回のバッファの末尾数バイト」を結合して検索するか、あるいはバッファサイズをキーワードの最大長よりも十分に大きく設計します。

③ 文字コード(Charset)の選択

`objTempStream.Charset = “shift-jis”` の部分は、実務において最もエラーが起きやすい箇所です。
最近のログファイルは `UTF-8` が主流です。もし検索がうまくいかない場合は、ここを `”utf-8″` に変更してください。これだけで解決することも珍しくありません。

5. まとめ:この技術をマスターしたあなたへ

今回学んだのは、単なるコードの書き方ではありません。
「リソース(メモリ)をいかに節約し、巨大なデータに対して効率的にアクセスするか」という、コンピュータサイエンスの根幹を成す考え方です。

1. `ReadAll` は巨大ファイルには禁物。
2. `ADODB.Stream` を使い、バイト単位でシークする。
3. バッファ(塊)ごとに読み込み、メモリ消費を一定に保つ。

この3点を意識するだけで、あなたの書くスクリプトの信頼性は格段に向上します。
もしエラーが出ても、焦らずに「メモリの使いすぎではないか?」「文字コードは合っているか?」と自問自答してみてください。

さあ、これであなたも「巨大ログを自在に操るエンジニア」への一歩を踏み出しました。
次は、この技術を応用して、ログの監視自動化ツールを作ってみるのも面白いかもしれませんね。

応援していますよ!

タイトルとURLをコピーしました