VBScriptの限界を超える設計思想:FSOによる高速キーワード・クロス集計エンジンの構築
「とりあえずFSO(FileSystemObject)でファイルを回して、中身を検索して、結果を書き出す」
もし君がそんな設計を考えているなら、今すぐその手を止めてほしい。ログファイルの数が増え、ファイルサイズが数MBから数百MBへと膨れ上がった瞬間、君の書いたスクリプトは「動かない」のではない。「実用に耐えないゴミ」へと成り下がる。
実務における自動化エンジニアに求められるのは、単にコードが動くことではない。計算量を意識したデータ構造の選択、オブジェクトのライフサイクル管理、そして例外発生時の堅牢な挙動だ。
本稿では、複数ファイル内の特定キーワード出現頻度をスキャンし、ファイル名とキーワードを軸とした「クロス集計CSV」を出力する、プロフェッショナル仕様のツールを構築する。
1. 凡庸な設計と、プロフェッショナルの設計
多くの初級者が陥る罠は、「多重ループによる総当たり攻撃」だ。
非効率な設計(アンチパターン)
1. ファイルを一つずつ開く
2. 行を一つずつ読み込む
3. 各行に対して、キーワードリストを一つずつループして `InStr` で検索する
4. 結果を配列に格納する(配列のサイズ変更が困難)
この設計の計算量は、ファイル数 $N \times$ 行数 $M \times$ キーワード数 $K$ となり、指数関数的にパフォーマンスが悪化する。また、VBScriptの配列は動的な拡張に弱く、メモリ管理を誤れば即座にクラッシュを招く。
堅牢な設計(アーキテクチャ)
我々が採用すべきは、「ハッシュマップ(Scripting.Dictionary)による多次元集計」だ。
- データ構造: `Dictionary` オブジェクトの中に、さらに `Dictionary` をネストさせる。
- `MasterDict(ファイル名) = SubDict(キーワード = 出現回数)`
- アルゴリズム: ファイルを一度だけスキャンし、その行に含まれるキーワードをカウントする。
- メモリ管理: 各ファイル処理後にストリームを確実に閉じ、オブジェクトの参照を解放する。
2. プロダクション・コード:キーワード・クロス集計エンジン
以下に、そのまま実戦投入可能なレベルのコードを示す。単なるコピペではなく、コメントに記した「なぜこう書くのか」という意図を読み取ってほしい。
'**
' Tool: Keyword Cross-Tabulation Engine
' Author: Chief Architect
' Description: 複数ファイル内のキーワード出現頻度を解析し、クロス集計CSVを出力する
'**
Option Explicit
' --- 設定領域 ---
Const TARGET_DIR = "C:\Logs\Input" ' 解析対象フォルダ
Const OUTPUT_FILE = "C:\Logs\Analysis_Report.csv" ' 出力先CSV
Dim KEYWORDS : KEYWORDS = Array("ERROR", "WARNING", "TIMEOUT", "RETRY", "CRITICAL")
' --- メインプロセス ---
Main
Sub Main()
Dim fso, targetFolder, file, masterDict
Set fso = CreateObject("Scripting.FileSystemObject")
Set masterDict = CreateObject("Scripting.Dictionary")
' 1. 入力ディレクトリの存在確認
If Not fso.FolderExists(TARGET_DIR) Then
MsgBox "Error: Target directory not found: " & TARGET_DIR, vbCritical
Exit Sub
End If
Set targetFolder = fso.GetFolder(TARGET_DIR)
' 2. ファイルスキャンの実行
For Each file In targetFolder.Files
' テキストファイルのみを対象とする(拡張子チェックは実務上必須)
If LCase(fso.GetExtensionName(file.Name)) = "log" Or _
LCase(fso.GetExtensionName(file.Name)) = "txt" Then
ProcessFile file.Path, masterDict, KEYWORDS
End If
Next
' 3. クロス集計CSVの出力
If masterDict.Count > 0 Then
WriteCrossTabCSV OUTPUT_FILE, masterDict, KEYWORDS
MsgBox "Analysis Complete. Output: " & OUTPUT_FILE, vbInformation
Else
MsgBox "No valid files processed.", vbExclamation
End If
' 4. オブジェクトのクリーンアップ(メモリリーク防止)
Set masterDict = Nothing
Set fso = Nothing
End Sub
' --- ファイル解析ロジック ---
Sub ProcessFile(filePath, ByRef masterDict, ByRef keywords)
Dim fso, ts, line, kw, subDict
Dim fileName : fileName = CreateObject("Scripting.FileSystemObject").GetFileName(filePath)
Set fso = CreateObject("Scripting.FileSystemObject")
' ファイル読み込み用Dictionaryの初期化
Set subDict = CreateObject("Scripting.Dictionary")
For Each kw In keywords
subDict(kw) = 0
Next
On Error Resume Next
Set ts = fso.OpenTextFile(filePath, 1) ' 1 = ForReading
If Err.Number <> 0 Then
' ファイルが開けない場合(使用中など)はスキップ
WScript.Echo "Skip file (Error " & Err.Number & "): " & filePath
Err.Clear
Exit Sub
End If
' 行ごとのスキャン
Do Until ts.AtEndOfStream
line = ts.ReadLine
For Each kw In keywords
' InStrは高速だが、大文字小文字を区別しない場合は vbTextCompareを指定
If InStr(1, line, kw, vbTextCompare) > 0 Then
subDict(kw) = subDict(kw) + 1
End If
Next
Loop
ts.Close
' マスター辞書へ格納
masterDict.Add fileName, subDict
' オブジェクト解放
Set ts = Nothing
Set subDict = Nothing
Set fso = Nothing
On Error GoTo 0
End Sub
' --- CSV出力ロジック ---
Sub WriteCrossTabCSV(outputPath, masterDict, keywords)
Dim fso, ts, fileName, kw, i
Set fso = CreateObject("Scripting.FileSystemObject")
' 2 = ForWriting, True = Create if not exists
Set ts = fso.CreateTextFile(outputPath, True)
' ヘッダー行の書き出し (FileName, KW1, KW2, ...)
ts.Write "File Name"
For Each kw In keywords
ts.Write "," & kw
Next
ts.WriteLine
' データ行の書き出し
Dim fileKeys : fileKeys = masterDict.Keys
For i = 0 To masterDict.Count - 1
fileName = fileKeys(i)
ts.Write fileName
' 各キーワードのカウントを列挙
Dim subDict : Set subDict = masterDict(fileName)
For Each kw In keywords
ts.Write "," & subDict(kw)
Next
ts.WriteLine
Next
ts.Close
Set ts = Nothing
Set fso = Nothing
End Sub
3. 技術的深掘り:なぜこのコードは「強い」のか
① `Scripting.Dictionary` による $O(1)$ のアクセス
配列で「どのファイルにどのキーワードがいくつあるか」を管理しようとすると、インデックス管理が極めて複雑になる。`Dictionary` を使用することで、ファイル名をキーとして即座にデータ構造へアクセスできる。これは、データ量が増大した際の検索速度において決定的な差を生む。
② 堅牢なエラーハンドリング
実務のファイル操作において、`On Error Resume Next` を無計画に使うのは自殺行為だ。本コードでは、「ファイルが開けない(他プロセスがロックしている)」という具体的なエラーに対してのみ、エラーを検知してスキップする設計にしている。これにより、1つのファイルエラーで全プロセスが停止するのを防ぎつつ、致命的なエラーは見逃さない。
③ メモリリークの徹底排除
VBScriptはCOM(Component Object Model)を利用している。`Set obj = Nothing` を怠ると、スクリプトが終了してもプロセス内にオブジェクトの残骸が残り、メモリを圧迫し続ける。特に `Dictionary` をネストさせる場合、内側のオブジェクトも明示的に解放する意識が、長時間稼働する自動化ツールには不可欠だ。
④ 計算量の最適化
キーワードの検索には `InStr` を使用している。これはVBScriptにおいて極めて最適化されたネイティブ関数だ。正規表現(RegExp)を使用すればより複雑なパターンマッチングが可能だが、単なるキーワード出現頻度であれば、`InStr` を使うのがパフォーマンスの観点から正解である。
4. 次のステップへのアドバイス
このツールは「基本形」だ。現場の要件に応じて、以下の拡張を検討せよ。
1. エンコーディングへの対応: `FileSystemObject` は基本的にANSI/UTF-16を扱う。もし対象ファイルが UTF-8 (BOMなし) の場合、このスクリプトは文字化けを起こす。その場合は `ADODB.Stream` オブジェクトへの切り替えが必要だ。
2. 正規表現の導入: 「`ERROR[0-9]+`」のようなパターンでカウントしたい場合は、`VBScript.RegExp` を組み込め。ただし、その分だけ処理速度は低下することを覚悟せよ。
3. ログ出力: `WScript.Echo` ではなく、実行ログを別途ファイルに出力するように設計変更すれば、バックグラウンド実行(`wscript.exe`)時にも運用監視が可能になる。
プロのエンジニアは、コードを書く前に「データがどう流れ、どこで止まり、どう死ぬか」を設計する。この知見を、君の武器にしてほしい。
