【実務・中級編】【実務中級者向け】複数テキストファイル内の特定検索キーワード出現頻度をFSOでカウントしクロス集計CSVを出力するツール – VBScript (Visual Basic Scripting Edition)解析バイブル

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

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`)時にも運用監視が可能になる。

プロのエンジニアは、コードを書く前に「データがどう流れ、どこで止まり、どう死ぬか」を設計する。この知見を、君の武器にしてほしい。

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