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

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

散乱するログを秩序へと変える:FSOによる大規模テキスト解析とメモリ管理の極意

業務自動化の現場において、VBScriptは「古臭いスクリプト」と軽視されることが少なくない。しかし、Windows標準環境において追加のランタイムを要求せず、FileSystemObject (FSO) を通じてOSのファイルシステムに直接介入できるその特性は、レガシーなエンタープライズ環境においては今なお最強の武器である。

単に「ファイルを読み込む」だけなら、稚拙なエンジニアでもできる。しかし、数千、数万のログファイルを対象に、特定のキーワード出現頻度をクロス集計し、メモリ枯渇を回避しながら高速にCSVへ出力する……。ここから先は、オブジェクトのライフサイクルと計算量を制御できる「アーキテクト」の領域だ。

本稿では、実務における「複数ファイル×複数キーワード」の集計タスクを題材に、極限の効率を追求した実装手法を解説する。

1. アーキテクチャ設計:多次元集計のデータ構造

クロス集計(ファイル名 × キーワード)を実装する際、最も避けるべきは「二重ループ内での動的な配列拡張」である。VBScriptの配列は動的なリサイズに弱く、要素が増えるたびにメモリの再確保が発生し、パフォーマンスが指数関数的に悪化する。

我々が採用すべきは、`Scripting.Dictionary` オブジェクトによるネスト構造だ。

  • 外側(Master Dictionary): キーを「ファイル名」、値を「内側のDictionary」とする。
  • 内側(Sub Dictionary): キーを「検索キーワード」、値を「出現回数(Integer)」とする。

この構造により、未知のファイルやキーワードに遭遇した際も、`Exists` メソッドによる定数時間に近い速度での検索と、動的なキー追加が可能となる。

2. 徹底したメモリ管理:オブジェクトの生存期間

VBScriptにおける最大の罠は、ループ内でのオブジェクト生成と、その解放漏れによるメモリリークである。

FSOの `TextStream` オブジェクトをループ内で生成し、`Set obj = Nothing` を怠れば、ファイルハンドルが開放されず、OSのリソースを食いつぶし、最終的にはスクリプトの異常終了を招く。特に大量のファイルをスキャンする場合、「生成したものは、そのスコープを抜ける前に確実に破棄する」という鉄則を、コードの至る所に刻み込む必要がある。

3. 実装:高効率クロス集計エンジン

以下に、実戦投入レベルのコードを示す。このコードは、再帰的なフォルダ探索、キーワードスキャン、そしてメモリ効率を最大化したCSV出力機能を備えている。

' ==============================================================================
' Script Name: KeywordCrossTabAnalyzer.vbs
' Description: 複数ファイル内のキーワード出現頻度をクロス集計しCSV出力する
' Author: Legendary Chief Architect
' ==============================================================================
Option Explicit

' --- 設定セクション ---
Const TARGET_DIR = "C:\Logs\Target"      ' 解析対象ディレクトリ
Const OUTPUT_CSV = "C:\Logs\Result.csv"  ' 出力先CSV
Dim KEYWORDS : KEYWORDS = Array("ERROR", "WARNING", "TIMEOUT", "CRITICAL")

' --- メインプロセス ---
Dim fso, dictMaster, keywordArray, i
Set fso = CreateObject("Scripting.FileSystemObject")
Set dictMaster = CreateObject("Scripting.Dictionary")
keywordArray = KEYWORDS

On Error Resume Next

' 1. 再帰的スキャン開始
Call ScanFolder(fso.GetFolder(TARGET_DIR))

' 2. 集計結果のCSV出力
Call ExportToCSV(dictMaster, keywordArray)

' 3. 後始末
Set dictMaster = Nothing
Set fso = Nothing

WScript.Echo "Analysis Complete. Output: " & OUTPUT_CSV

' ==============================================================================
' 関数: ScanFolder (再帰的フォルダ走査)
' ==============================================================================
Sub ScanFolder(objFolder)
    Dim objFile, objSubFolder, stream, line, kw, subDict
    
    ' フォルダ内のファイルを処理
    For Each objFile In objFolder.Files
        ' テキストファイルのみを対象 (拡張子チェックは環境に合わせて調整)
        If LCase(fso.GetExtensionName(objFile.Name)) = "log" Or _
           LCase(fso.GetExtensionName(objFile.Name)) = "txt" Then
            
            ' ファイルごとの集計用Dictionaryを生成
            Set subDict = CreateObject("Scripting.Dictionary")
            ' 初期値としてキーワードを0でセットしておく(クロス集計の精度向上)
            For Each kw In keywordArray
                subDict.Add kw, 0
            Next
            
            ' ファイルのストリームを開く (読み取り専用)
            Set stream = objFile.OpenAsTextStream(1) ' 1 = ForReading
            
            If Err.Number = 0 Then
                ' 行単位での読み込み(メモリ節約のためReadAllは避ける)
                Do Until stream.AtEndOfStream
                    line = stream.ReadLine
                    For Each kw In keywordArray
                        ' 大文字小文字を区別しない検索
                        If InStr(1, line, kw, vbTextCompare) > 0 Then
                            subDict(kw) = subDict(kw) + 1
                        End If
                    Next
                Loop
                stream.Close
                Set stream = Nothing ' 明示的な解放
                
                ' マスターDictionaryに格納
                dictMaster.Add objFile.Name, subDict
            Else
                WScript.Echo "Error opening file: " & objFile.Name & " (" & Err.Description & ")"
                Err.Clear
            End If
            Set stream = Nothing
        End If
    Next

    ' サブフォルダを再帰的に探索
    For Each objSubFolder In objFolder.SubFolders
        Call ScanFolder(objSubFolder)
    Next
End Sub

' ==============================================================================
' 関数: ExportToCSV (クロス集計結果の出力)
' ==============================================================================
Sub ExportToCSV(dict, keywords)
    Dim objFile, stream, fileName, kw
    
    Set objFile = fso.CreateTextFile(OUTPUT_CSV, True)
    
    ' ヘッダー行の作成 (ファイル名, KW1, KW2, ...)
    Dim header : header = "FileName"
    For Each kw In keywords
        header = header & "," & kw
    Next
    objFile.WriteLine header
    
    ' データ行の作成
    Dim fileKey, rowData
    For Each fileKey In dict.Keys
        rowData = """" & fileKey & """" ' ファイル名をダブルクォートで囲む
        For Each kw In keywords
            ' ファイル内にキーワードが存在しない場合も0として出力
            rowData = rowData & "," & dict(fileKey)(kw)
        Next
        objFile.WriteLine rowData
    Next
    
    objFile.Close
    Set objFile = Nothing
End Sub

4. アーキテクトの視点:なぜこのコードは「強い」のか

行単位のストリーム処理(Memory Footprint Optimization)

初心者は `stream.ReadAll` を使いがちだが、これは致命的なミスだ。数GBのログファイルに対してこれを行うと、即座に `Out of Memory` エラーでプロセスが死ぬ。我々は `Do Until stream.AtEndOfStream` を使い、一行ずつメモリに乗せては捨てる、ストリーミング方式を採用している。これにより、ファイルサイズに関わらずメモリ消費量を一定に保つことができる。

キーワードの事前初期化(Data Integrity)

クロス集計において、あるファイルに特定のキーワードが一度も出現しない場合、Dictionaryにはそのキー自体が存在しない状態になる。そのままCSV出力しようとすると、列のズレ(カラムの不一致)が発生する。
コード内の `subDict.Add kw, 0` という処理は、集計の「型」をあらかじめ定義することで、出力データの整合性を担保するための極めて重要なステップだ。

エラーハンドリングの局所化

`On Error Resume Next` をスクリプト全体に漫然と適用するのは素人のすることだ。本実装では、ファイルオープンという「外部要因による失敗」が予測される箇所にのみ限定的に適用し、エラー発生時にはその内容を報告しつつ、次のファイル処理へ確実に移行できるように設計している。

結論

VBScriptによる自動化の本質は、単に「作業を楽にする」ことではない。「不確実な環境(レガシーOS、巨大なログ、不安定なファイルアクセス)において、いかに決定論的な結果を、最小限のリソースで導き出すか」にある。

オブジェクトのライフサイクルを支配し、計算量を制御する。その意識こそが、スクリプトエンジニアを単なる作業者から、システムアーキテクトへと昇華させるのである。

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