【実務・中級編】【差分ログ抽出】2つのテキストファイル(新旧マスター)をFSOで比較し追加・変更行のみを抽出するDiffツール – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【差分ログ抽出】VBScriptとFSOだけで実現する、実務直結の高速テキストDiffツール設計論

業務自動化の現場において、設定ファイルの世代管理や、日次で出力されるマスターデータの突き合わせは避けて通れないタスクだ。
「たった2つのファイルの差分を見るために、わざわざPython環境を構築したり、重いサードパーティ製ツールを導入したりするのか?」
――不要だ。Windows環境さえあれば、どの端末にも標準で備わっている VBScript (Visual Basic Scripting Edition) と FileSystemObject (FSO) さえあれば十分である。

今回は、外部ライブラリ一切なしで、巨大なテキストファイルをも高速かつ安全に比較し、追加・変更行のみを抽出する「プロダクション品質のDiffツール」の設計思想と実装を、チーフアーキテクトの視点から伝授する。

—

1. なぜ素朴なFSOの `ReadAll` は現場で破綻するのか?

多くの初学者は、FSOを使ったファイル読み込みで次のようなコードを書く。

‘ 【アンチパターン】絶対にやってはいけない実装
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set file = fso.OpenTextFile(“old_master.txt”, 1)
content = file.ReadAll ‘ ファイル全体をメモリに一気にロード
file.Close
‘ ここから文字列処理…

このアプローチは、管理対象のファイルが数キロバイト程度であれば動く。しかし、数メガバイト、あるいは数万行に及ぶ実務のログやマスターデータにおいて、このコードはメモリ枯渇(Out of Memory)を引き起こすか、ガベージコレクションの嵐による深刻なパフォーマンス低下を招く。

堅牢な設計の要件

1. ストリーム処理(逐次読み込み): ファイル全体をメモリに抱えず、1行ずつ(あるいはチャンク単位で)処理する。
2. 文字コードの明示的制御: VBScriptのデフォルトであるANSI(Shift-JIS)と、昨今の標準であるUTF-8(BOM付き/無し)の差異を考慮する。
3. メモリ効率の良いデータ構造: 変更行の特定において、旧マスターの全行をメモリ上の連想配列(Dictionary)にインデックス化し、$O(1)$ のオーダーで存在確認を行う。

—

2. アーキテクチャ設計:Dictionaryを用いた高速差分検知

今回のDiffツールの核心は、「旧マスターの全行を `Scripting.Dictionary` にハッシュとして取り込み、新マスターを1行ずつ走査して、辞書に存在しない行(=追加・変更された行)を抽出する」というアルゴリズムだ。

これにより、単純な二重ループによる $O(N \times M)$ の爆発的な処理遅延を防ぎ、リニアな $O(N + M)$ の高速処理を実現する。

—

3. プロダクションコード:実用Diffツール(`MakeDiffLog.vbs`)

以下のコードは、エラーハンドリング、文字コードへの配慮、そして実務に耐えうるログ出力を備えた完全版のVBScriptである。そのまま `.vbs` ファイルとして保存して実行可能である。

‘ ==============================================================================
‘ Script Name : MakeDiffLog.vbs
‘ Description : 旧マスターと新マスターを比較し、差分(追加・変更行)を抽出する
‘ Author : チーフアーキテクト
‘ ==============================================================================

Option Explicit

‘ 定数定義
Const ForReading = 1
Const ForWriting = 2
Const TriStateUseDefault = -2 ‘ システムのデフォルトエンコーディング

Call Main()

Sub Main()
Dim fso, oldFilepath, newFilepath, diffFilepath
Dim startTime

startTime = Timer
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ パスの設定(実務ではコマンドライン引数やINIファイルから取得することを推奨)
oldFilepath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “old_master.txt”)
newFilepath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “new_master.txt”)
diffFilepath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “diff_result.log”)

‘ 前提条件チェック
If Not fso.FileExists(oldFilepath) Then
WScript.Echo “エラー: 旧マスターファイルが見つかりません: ” & oldFilepath
Exit Sub
End If
If Not fso.FileExists(newFilepath) Then
WScript.Echo “エラー: 新マスターファイルが見つかりません: ” & newFilepath
Exit Sub
End If

WScript.Echo “処理を開始します…”

‘ 差分抽出ロジックの実行
Call ExtractDiff(fso, oldFilepath, newFilepath, diffFilepath)

WScript.Echo “処理が完了しました。実行時間: ” & FormatNumber(Timer – startTime, 2) & ” 秒”
WScript.Echo “出力先: ” & diffFilepath
End Sub

Sub ExtractDiff(fso, oldPath, newPath, diffPath)
Dim dicOld, objFile, line, diffCount
Set dicOld = CreateObject(“Scripting.Dictionary”)

‘ 大文字小文字を区別するかどうか(必要に応じて True/False を変更)
dicOld.CompareMode = TextCompare

‘ 1. 旧マスターをDictionaryにロード(高速ルックアップのため)
‘ ※注意: ADODB.Streamを使うことでUTF-8など多様な文字コードに対応可能だが、
‘ 今回は標準FSOでシフトJISを前提とした高速読み込みを実装
Set objFile = fso.OpenTextFile(oldPath, ForReading, False, TriStateUseDefault)
Do Until objFile.AtEndOfStream
line = Trim(objFile.ReadLine)
‘ 空行をスキップしたい場合はここで条件分岐を入れる
If line <> “” And Not dicOld.Exists(line) Then
dicOld.Add line, True
End If
Loop
objFile.Close

‘ 2. 新マスターを読み込み、旧マスターに存在しない行を差分として書き出す
Set objFile = fso.OpenTextFile(newPath, ForReading, False, TriStateUseDefault)
Dim objOut
Set objOut = fso.OpenTextFile(diffPath, ForWriting, True, TriStateUseDefault)

‘ ログヘッダーの書き込み
objOut.WriteLine “=== Diff Extraction Log ===”
objOut.WriteLine “Execution Time: ” & Now()
objOut.WriteLine “——————————————-”

diffCount = 0
Do Until objFile.AtEndOfStream
line = Trim(objFile.ReadLine)
If line <> “” Then
‘ 旧マスターに存在しない = 追加または変更された行
If Not dicOld.Exists(line) Then
objOut.WriteLine line
diffCount = diffCount + 1
End If
End If
Loop

objFile.Close
objOut.Close

WScript.Echo “差分検出数: ” & diffCount & ” 行”

Set dicOld = Nothing
End Sub

—

4. プロジェクトで運用する際の重要注意点(アーキテクトの知見)

このスクリプトを実際の業務システムやバッチ処理に組み込む際、以下のポイントに留意してほしい。

① 文字コード(Encoding)の罠

VBScriptの標準 `FSO.OpenTextFile` の第4引数(TriState)は、環境のANSIコードページに依存する。もし比較対象のファイルが UTF-8 (BOMなし) や UTF-16 である場合、文字化けを起こすか、正しく比較できない。
厳密な文字コード制御が必要な現場では、FSOではなく `ADODB.Stream` オブジェクト を用いてストリームをバイナリ/テキスト変換して読み込む設計にリプレイスすること。

② メモリとディスクI/Oのトレードオフ

今回の実装では `Scripting.Dictionary` に旧マスターの全行を格納している。数百万行レベルの巨大ファイルを扱う場合、Dictionary自体のメモリ消費量が問題になることがある。その場合は、事前に両方のファイルをSortコマンドなどでソート済みにしておき、ポインタをインクリメントしながら比較する「マージアルゴリズム(Line-by-Line Sequential Scan)」を採用すべきだ。

③ エラーハンドリングの徹底

ファイルが別のプロセス(Excelやテキストエディタなど)で排他ロックされている場合、`OpenTextFile` は実行時エラー(エラー番号 70: 書き込み権限がありません / アクセスが拒否されました)を吐いて即座にクラッシュする。
プロダクションコードでは必ず `On Error Resume Next` と `Err.Number` を用いたリトライ機構、あるいはログ出力によるフェイルセーフを組み込むべきである。

—

総括

VBScriptは「古い言語」と揶揄されることもあるが、OS標準で動作し、インフラストラクチャに依存しないという圧倒的な機動力を持っている。
FSOの本質を理解し、メモリとアルゴリズムを最適化したコードを書けば、RPAツールや大げさなミドルウェアを導入せずとも、現場の業務効率化要求に秒速で応えることが可能だ。

あなたの手元の自動化タスクにも、この堅牢なDiffロジックを組み込み、真のエンジニアリングの効率をもたらしてほしい。

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