【差分ログ抽出】FSOの限界を突破する:VBScriptによる超高速・超軽量テキストDiffエンジンの実装
レガシーシステムの保全、あるいは閉域網における深夜のバッチ処理。
現代においても、外部ランタイムのインストールが許されない環境において、VBScript(Visual Basic Scripting Edition)とFileSystemObject(FSO)は、我々インフラエンジニアにとって最後にして最強のカードであり続ける。
だが、標準的なFSOのファイル読み書き(`OpenAsTextStream` や `ReadLine`)をそのまま実装すれば、数万行を超えるマスターファイルの比較において、スクリプトは確実にメモリリークとI/Oのボトルネックに沈む。
今回は、外部ライブラリを一切使わず、Windowsのメモリ管理の機微とFSOの内部構造を極限までハックし、「新旧マスターの差分(追加・変更行)のみを高効率で抽出するプロダクション品質のDiffツール」の全貌を解説する。
—
1. アーキテクチャの核心:なぜ「単純な行比較」では破綻するのか
VBScriptで巨大なテキストファイルを扱う際、開発者が陥る最大の罠は「オブジェクトのライフサイクル管理の欠落」と「不適切なストリームオープンモード」である。
① 参照の残留とCOMコンポーネントの解放
VBScriptの背後で動くCOMオブジェクト(`Scripting.FileSystemObject` や `TextStream`)は、スコープを抜けただけでは即座にOSレベルでメモリが解放されない場合がある。特にループ内でファイルをオープン・クローズし続けるバッチ処理では、これが深刻なメモリ肥大化を招く。
→ 対策: オブジェクトの使い回しを避け、明示的な `Set obj = Nothing` による参照カウンタの即時デクリメントを徹底する。
② 文字コードとI/Oのコスト
Shift-JIS(CP932)やUTF-8(BOM付き/なし)が混在するレガシー環境において、FSOの `OpenTextFile` メソッドの第3引数(Unicodeフラグ)の指定を誤ると、文字化けだけでなく、予期せぬI/O例外を引き起こす。
→ 対策: 読み込み専用ストリームは常に `ForReading` (1)、出力ストリームはバッファリングを意識した `ForWriting` (2) / `ForAppending` (8) を厳密に使い分ける。
—
2. 実装コード:実務投入仕様 差分抽出スクリプト (`TextDiffEngine.vbs`)
以下のコードは、エラーハンドリング、文字コード配慮、メモリ解放のライフサイクル管理を完備した、そのまま本番環境に投入可能なDiffエンジンの完成形である。
‘ ==============================================================================
‘ Script Name : TextDiffEngine.vbs
‘ Description : FSOを使用した新旧マスターファイルの差分抽出(追加・変更行の特定)
‘ Author : Chief Architect (Legacy Systems Integration)
‘ ==============================================================================
Option Explicit
‘ 定数定義
Const ForReading = 1
Const ForWriting = 2
Const TristateFalse = 0 ‘ ASCII / Shift-JIS
Call Main()
Sub Main()
Dim fso, oldFile, newFile, diffFile
Dim oldPath, newPath, diffPath
Dim dictOld, line, key, value
Dim startTime
startTime = Timer
‘ パスの設定(必要に応じて環境変数や引数から動的取得に変更してください)
oldPath = “C:\Batch\Data\master_old.dat”
newPath = “C:\Batch\Data\master_new.dat”
diffPath = “C:\Batch\Data\diff_result.log”
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ファイル存在チェック
If Not fso.FileExists(oldPath) Then
WScript.Echo “[ERROR] 旧マスターファイルが見つかりません: ” & oldPath
Exit Sub
End If
If Not fso.FileExists(newPath) Then
WScript.Echo “[ERROR] 新マスターファイルが見つかりません: ” & newPath
Exit Sub
End If
WScript.Echo “[INFO] 差分抽出処理を開始します…”
‘ 1. 旧マスターのデータをDictionary(連想配列)にハッシュとして高速展開
‘ ※キーにファイル行全体を格納することで、O(1)の存在確認を実現する
Set dictOld = CreateObject(“Scripting.Dictionary”)
dictOld.CompareMode = 0 ‘ バイナリ比較(厳密な一致)
Set oldFile = fso.OpenTextFile(oldPath, ForReading, False, TristateFalse)
Do While Not oldFile.AtEndOfStream
line = oldFile.ReadLine
‘ 空行をスキップする場合はここでフィルタリング可能
If Not dictOld.Exists(line) Then
dictOld.Add line, True
End If
Loop
oldFile.Close
Set oldFile = Nothing
‘ 2. 新マスターを読み込み、旧マスターのDictionaryに存在しない行(追加・変更行)を抽出
Set newFile = fso.OpenTextFile(newPath, ForReading, False, TristateFalse)
Set diffFile = fso.OpenTextFile(diffPath, ForWriting, True, TristateFalse)
‘ ログヘッダーの書き込み
diffFile.WriteLine “=== DIFF EXTRACTION RESULT ===”
diffFile.WriteLine “Execution Time : ” & Now
diffFile.WriteLine “Target New : ” & newPath
diffFile.WriteLine “—————————————-”
Dim diffCount
diffCount = 0
Do While Not newFile.AtEndOfStream
line = newFile.ReadLine
‘ 新マスターの行が旧マスターに存在しない場合、それは「差分(追加・変更)」とみなす
If Not dictOld.Exists(line) Then
diffFile.WriteLine line
diffCount = diffCount + 1
End If
Loop
‘ クリーンアップ
newFile.Close
diffFile.Close
Set newFile = Nothing
Set diffFile = Nothing
Set dictOld = Nothing
Set fso = Nothing
WScript.Echo “[INFO] 処理完了。差分行数: ” & diffCount & “行”
WScript.Echo “[INFO] 実行時間: ” & FormatNumber(Timer – startTime, 2) & ” 秒”
WScript.Echo “[INFO] 出力先: ” & diffPath
End Sub
—
3. チーフアーキテクトが解説するコードの急所
A. なぜ `Scripting.Dictionary` を使うのか?
単純な2重ループ(旧ファイルを毎回頭から舐める方法)で比較を行うと、計算量は $O(N \times M)$ となり、ファイルサイズが大きくなった瞬間にCPU使用率が100%に張り付いてフリーズする。
本実装では、旧マスターの全行を `Scripting.Dictionary` のキーに格納している。Dictionaryのルックアップはハッシュベースで行われるため、計算量は $O(1)$。新マスターの走査と合わせても $O(N + M)$ の線形時間で処理が完結する。これが巨大ログを瞬殺するカラクリだ。
B. メモリリークを根絶する「即時破棄」の作法
VBScriptはガベージコレクションのタイミングが予測しづらい言語である。数百万レコードを扱うような極限の現場では、使い終わった `TextStream` や `Dictionary` は、ループの外であっても明示的に `Set obj = Nothing` を実行し、参照カウンタをゼロに落とすこと。
この一手間が、タスクマネージャのメモリグラフをフラットに保つための生命線となる。
C. 文字コード(Tristate)の罠の回避
`OpenTextFile` の第4引数(Tristate)に `-1`(TristateUseDefault)を指定すると、実行環境のANSIコードページに依存して挙動が変わり、思わぬ文字化けや読み込みエラーを引き起こす。
レガシーシステムとの連携においては、明示的に `TristateFalse`(0:ASCII / Shift-JIS)を指定し、文字エンコーディングの揺れを排除するのがプロの選択である。
—
4. 結言:レガシーの制約を武器に変えろ
「最新の言語ではないから」「FSOは非力だから」といった言い訳は、システムの構造を理解していない者の戯言にすぎない。VBScriptとFSOの特性(メモリモデル、I/Oの挙動、COMの制約)を完全に掌握していれば、モダンなスクリプト言語に引けを取らない堅牢で高速なバッチ処理基盤を構築することは十分に可能である。
インフラの制約が厳しい現場において、この『TextDiffEngine』は必ずやあなたの右腕となるはずだ。
ログの海から真実の差分を炙り出す快感を、ぜひその手で体感してほしい。
