【高速キーワードフィルタ】FileSystemObject ReadLine と正規表現を組み合わせた超巨大ログからの対象行抽出
レガシーシステムの保全、あるいはオンプレミス環境における泥臭いトラブルシューティングの現場において、数十GBに及ぶプレーンテキストのログファイルと対峙する絶望は、インフラエンジニアやシニアプログラマであれば誰もが一度は経験しているはずだ。
「数千万行のログから、特定のトランザクションエラーやFatalコードを秒速で抜き出したい」
現代であればPythonやPowerShellのパイプライン処理、あるいはripgrep(rg)などのモダンなツールを持ち出すのが定石だ。しかし、セキュリティポリシーで外部ツールの導入が厳禁とされたサンドボックス環境、あるいは「OS標準機能のみで完結させよ」という理不尽なエンタープライズの要件においては、いまだにWSH (Windows Script Host) / VBScriptが最果ての特効薬として君臨し続けている。
今回は、数GB〜数十GBの超巨大ログを対象に、メモリリーク(Out of Memory)を起こすことなく、VBScriptの限界性能を引き出して高速にキーワード抽出を行うアーキテクチャを解説する。
—
1. 致命的なアンチパターン:なぜ `OpenAsTextStream` や `ReadAll` は爆死するのか
初学者がやりがちな最大の過ちは、ファイルオブジェクト全体を一度にメモリへロードすることだ。
‘ 【絶対に行ってはならないアンチパターン】
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set file = fso.OpenTextFile(“C:\Logs\huge_system.log”, 1)
‘ 全体を一度に読み込むと、ファイルサイズ分のメモリ(+VBScriptの内部BSTR文字列バッファ)が即座に枯渇する
strData = file.ReadAll
`ReadAll` や、改行コードを無視した巨大な文字列操作は、VBScriptの背後にあるCOMコンポーネント(Automation)のメモリ管理機構を破綻させる。32bitプロセスとして動作するCScript.exeにおいて、2GBの壁はあまりにも低い。数GBのログを読み込ませた瞬間、容赦なく「メモリ不足です (Out of memory)」のエラーが吐き出され、スクリプトは無残にクラッシュする。
また、WMIや過剰なオブジェクトインスタンス化をループ内で行うコードも、COMの参照カウンタ(Reference Counting)とガベージコレクションの遅延を引き起こし、パフォーマンスを地の底まで落とす。
—
2. 極限最適化されたアーキテクチャの要件
超巨大ログを安全かつ高速に処理するためには、以下の3つの鉄則を遵守しなければならない。
1. ストリーム単位の逐次読み込み (`ReadLine`)
ファイルポインタを1行ずつ進め、メモリ上に常に「現在処理中の1行」しか存在しない状態を強制する。
2. 正規表現エンジン(VBScript.RegExp)のループ外インスタンス化
ループの内部で `New RegExp` を呼ぶと、毎回COMオブジェクトの生成・破棄が発生し、数百万回のイテレーションにおいて致命的なオーバーヘッドとなる。必ずループの外で1度だけインスタンス化し、プロパティを再利用する。
3. 適切なオブジェクトの明示的破棄 (`Set … = Nothing`)
スコープ抜ける際の暗黙の解放に頼らず、リソースを即座に開放する作法を徹底する。
—
3. 実装コード:超高速ログフィルタリングスクリプト
以下のコードは、数千万行のログからミリ秒単位の効率で目的のパターンを抽出し、別ファイルへ出力するためのプロダクションコードである。コマンドライン引数(`WScript.Arguments`)を受け取るため、バッチファイルやタスクスケジューラから即座に組み込める。
‘ ==============================================================================
‘ Script Name: FastLogFilter.vbs
‘ Description: FileSystemObject ReadLine と RegExp を用いた超巨大ログの高速抽出
‘ Usage: cscript //nologo FastLogFilter.vbs “C:\Logs\target.log” “C:\Logs\result.csv” “ERR-[0-9]{4}”
‘ ==============================================================================
Option Explicit
‘ 実行時間の計測開始
Dim dblStartTime
dblStartTime = Timer
‘ 引数のバリデーション
If WScript.Arguments.Count < 3 Then
WScript.StdErr.WriteLine "[-] Error: 引数が不足しています。"
WScript.StdErr.WriteLine "[] Usage: cscript //nologo FastLogFilter.vbs
WScript.Quit 1
End If
Dim strInputPath, strOutputPath, strPattern
strInputPath = WScript.Arguments(0)
strOutputPath = WScript.Arguments(1)
strPattern = WScript.Arguments(2)
‘ オブジェクトの宣言
Dim fso, objFileIn, objFileOut
Dim regEx
Dim strLine
Dim lngTotalLines, lngMatchedLines
lngTotalLines = 0
lngMatchedLines = 0
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 入力ファイルの存在確認
If Not fso.FileExists(strInputPath) Then
WScript.StdErr.WriteLine “[-] Error: 入力ファイルが見つかりません: ” & strInputPath
Set fso = Nothing
WScript.Quit 2
End If
‘ 【極限最適化】正規表現エンジンはループ外で1度だけ生成する
Set regEx = New RegExp
With regEx
.Pattern = strPattern
.IgnoreCase = True ‘ 大文字小文字を区別しない
.Global = True ‘ マッチ箇所をすべて検出
End With
‘ ファイルストリームのオープン
‘ 第2引数: ForReading(1), 第3引数: False(存在しない場合作成しない), 第4引数: TristateFalse(-2:システムデフォルト/ASCII)
Set objFileIn = fso.OpenTextFile(strInputPath, 1, False, 0)
‘ 出力ファイル作成 (ForWriting = 2, Overwrite = True = True)
Set objFileOut = fso.OpenTextFile(strOutputPath, 2, True, 0)
‘ ヘッダー書き込みが必要な場合はここで出力
objFileOut.WriteLine “LineNumber,LogContent”
‘ 【メインループ】1行ずつストリームを読み込む(メモリ使用量は常に一定)
Do While Not objFileIn.AtEndOfStream
lngTotalLines = lngTotalLines + 1
strLine = objFileIn.ReadLine
‘ 正規表現マッチングの高速判定
If regEx.Test(strLine) Then
lngMatchedLines = lngMatchedLines + 1
‘ CSVインジェクション対策およびダブルクォートのエスケープ処理
Dim escapedLine
escapedLine = Replace(strLine, “”””, “”””””)
‘ 出力ストリームへ書き込み
objFileOut.WriteLine lngTotalLines & “,””” & escapedLine & “”””
End If
‘ 巨大ログ処理における進捗表示(10万行毎にドットを出力してフリーズと誤認させない)
If lngTotalLines Mod 100000 = 0 Then
WScript.StdOut.Write “.”
End If
Loop
WScript.StdOut.WriteLine “”
‘ ストリームのクローズ
objFileIn.Close
objFileOut.Close
‘ オブジェクトの明示的解放(メモリリークの完全防止)
Set objFileIn = Nothing
Set objFileOut = Nothing
Set regEx = Nothing
Set fso = Nothing
‘ 処理結果の出力
Dim dblElapsedTime
dblElapsedTime = Timer – dblStartTime
WScript.StdOut.WriteLine “[+] 処理完了”
WScript.StdOut.WriteLine ” – 走査総行数: ” & FormatNumber(lngTotalLines, 0) & ” 行”
WScript.StdOut.WriteLine ” – 抽出行数: ” & FormatNumber(lngMatchedLines, 0) & ” 行”
WScript.StdOut.WriteLine ” – 処理時間: ” & FormatNumber(dblElapsedTime, 2) & ” 秒”
WScript.Quit 0
—
4. チーフアーキテクトが教える、実運用上のハックとチューニング
上記のコードはそのまま本番環境で通用する堅牢性を持っているが、極限のパフォーマンスを絞り出すための「隠しパラメータ」や知見を共有しておこう。
トライステート(文字コード)の罠
`OpenTextFile` の第4引数(Tristate)の指定を誤ると、BOM付きUTF-8やUTF-16(Unicode)の巨大ログを読み込んだ際に、文字化けを起こすか、あるいは内部での文字列変換オーバーヘッドで処理速度が数分の一に低下する。
もしターゲットログがShift-JIS(CP932)や純粋なASCIIであれば、デフォルト(`0` または `TristateFalse`)で十分だが、UTF-8の場合はWSH標準機能だけではストリームのパースが重くなることがある。その場合は、事前に `iconv` や PowerShell のパイプラインで文字コードを一度ASCII/ANSIに寄せてからVBScriptに処理させるのが、トータルでのスループット向上に繋がるケースが多い。
I/Oバッファの最適化
FileSystemObjectの `TextStream` は、内部でバッファリングを行っているが、ループ内で無駄な文字列結合(`&` 演算子による累積など)を行わないことが極めて重要である。VBScriptの文字列はイミュータブル(不変)ではないが、巨大な文字列の連結はメモリの再割り当てを頻発させる。上記のコードのように、読み込んだ行をそのまま正規表現に通し、必要な部分だけを最小限の文字列操作で出力ストリームへ流すアプローチが最も安全かつ高速である。
—
5. 総括
モダンな開発言語が全盛の今、VBScriptをディグることは一見すると時代遅れに映るかもしれない。しかし、「余計なランタイムを一切インストールできないインフラ環境」「保守フェーズに入り、既存の踏み台サーバー上で完結させなければならないシステム」において、VBScriptとWSHの知識は、システムダウンという崖っぷちからプロジェクトを救い出す「最後の剣」となる。
オブジェクトの寿命を正確に把握し、メモリの動態を脳内でトレースしながら書かれたコードは、モダンな言語の非効率な実装をも凌駕する驚異的なパフォーマンスを発揮する。
レガシーを侮るなかれ。極限まで研ぎ澄ませたVBScriptのコードは、今なお現場の最前線で静かに、そして確実に、膨大なデータを切り刻み続けている。
