はじめに:なぜ今、枯れたVBScriptでテンプレートエンジンを構築するのか
エンタープライズの現場、とりわけ金融、インフラ、レガシー産業の基幹系現場には、最新のランタイム(.NET Core、Node.js、Python等)の導入すらセキュリティポリシーで弾かれる閉域網サーバが今なお厳然として存在する。
そうした極限環境において、外部依存ゼロ(ゼロフットプリント)で確実に動作し、OS標準のコンポーネントだけで設定ファイルを量産できる技術――それがWindows標準のWindows Script Host(WSH)上で駆動するVBScriptだ。
しかし、雑に書かれたVBScriptは遅く、メモリを貪り、エンコーディングの不一致で文字化けを引き起こす。
本稿では、`Scripting.FileSystemObject`(以下FSO)のファイル・ディレクトリ走査能力と、高速なハッシュルックアップを提供する`Scripting.Dictionary`、そしてUTF-8を確実にハンドリングする`ADODB.Stream`を融合させ、数千ファイル規模の設定ファイル(`.conf`, `.json`, `.xml`, `.env`等)を瞬時に自動生成する「極限のテンプレートエンジン」を設計・実装する。
オブジェクトのライフサイクル、BSTR(文字列)のメモリ割り当て特性、正規表現による単一パストークン解決など、シニアエンジニアとして知っておくべきアーキテクチャの神髄を提示しよう。
—
1. アーキテクチャ設計:O(N×M)からO(N)への計算量削減
ナイーブな実装の罠
初心者が書きがちなテンプレート置換は、以下のような構造だ。
For Each Key In Dictionary.Keys
TemplateContent = Replace(TemplateContent, “${” & Key & “}”, Dictionary(Key))
Next
この実装は、テンプレートサイズを $L$、キー数を $K$、生成ファイル数を $N$ と置いた場合、置換処理だけで $O(N \times K \times L)$ のスキャンコストを要求する。さらに、VBScriptの文字列型(BSTR)はイミュータブルであるため、`Replace`を実行するたびにヒープ領域上で新たな文字列メモリの確保・コピー・破棄が連続して発生し、メモリの断片化と著しいパフォーマンス低下を招く。
採用すべき設計思想:正規表現ワンパス・ディスパッチ
本設計では、`VBScript.RegExp`を用いてテンプレート内の `${TokenName}` を一発でスキャンし、マッチしたトークンのみを `Scripting.Dictionary` から $O(1)$ で定数時間参照して差し替える。
[テンプレートファイル]
│
▼ (ADODB.Stream / UTF-8読込)
[BSTRメモリバッファ]
│
▼ (VBScript.RegExp: 全トークンを一括マッチ抽出)
[トークンリスト] ──(キー照合: Dictionary O(1))──> [置換テーブル]
│
▼ (一括マッピング置換)
[生成バッファ]
│
▼ (ADODB.Stream / UTF-8書込)
[個別設定ファイル群]
このアプローチにより、走査回数はテンプレート内のプレースホルダ出現回数にのみ依存するようになり、計算量は $O(N \times L)$ へと劇的に低減される。
—
2. 避けて通れない「文字コードの壁」の突破
FSOの `TextStream` は、歴史的経緯から「ASCII」または「UTF-16(Unicode)」しかネイティブ対応していない。現代のWebシステムやコンテナ環境向け設定ファイル(JSON, YAML等)はほぼ確実にUTF-8(BOMあり/なし)である。FSOでUTF-8を無理に開くと、マルチバイト文字が軒並み破壊される。
この制約を突破するため、本アーキテクチャでは以下のハイブリッド構成を採用する。
1. ファイルシステム統括・存在確認・パス解決: `Scripting.FileSystemObject`
2. 高速キー・バリュー保持: `Scripting.Dictionary`
3. バイトレベルUTF-8入出力: `ADODB.Stream`
—
3. 実装:商用利用に耐えうるテンプレートエンジン
以下に、実務でそのまま運用可能な完成されたスクリプトを提示する。
外部のCSV定義ファイルからパラメータを読み込み、テンプレートに流し込んで大量のターゲットファイルを生成する堅牢な構成だ。
Option Explicit
‘ ==============================================================================
‘ Script Name : UltraTemplateEngine.vbs
‘ Description : FSO x Dictionary x ADODB.Stream による高速UTF-8テンプレート置換エンジン
‘ Architecture: High-Performance COM Automation Architecture
‘ ==============================================================================
‘ メインルーチン起動
Call Main()
Sub Main()
Dim oEngine
Set oEngine = New TemplateEngine
‘ 初期化: テンプレートパス、出力ディレクトリ
oEngine.TemplatePath = “.\templates\server_config.template.json”
oEngine.OutputDir = “.\dist”
‘ パラメータソース(CSV等の定義)を元に複数ファイルをバッチ生成
‘ 第1引数: 出力ファイル名, 第2引数: 置換パラメータディクショナリ
‘ テストデータ1: WEB-SRV-01
Dim p1
Set p1 = CreateObject(“Scripting.Dictionary”)
p1.CompareMode = vbBinaryCompare
p1.Add “SERVER_ID”, “WEB-001”
p1.Add “IP_ADDRESS”, “192.168.10.11”
p1.Add “ENVIRONMENT”, “Production”
p1.Add “MAX_CLIENTS”, “2048”
p1.Add “LOG_LEVEL”, “INFO”
oEngine.Generate “web_srv_01.json”, p1
Set p1 = Nothing
‘ テストデータ2: DB-SRV-01
Dim p2
Set p2 = CreateObject(“Scripting.Dictionary”)
p2.CompareMode = vbBinaryCompare
p2.Add “SERVER_ID”, “DB-001”
p2.Add “IP_ADDRESS”, “192.168.10.21”
p2.Add “ENVIRONMENT”, “Production”
p2.Add “MAX_CLIENTS”, “512”
p2.Add “LOG_LEVEL”, “DEBUG”
oEngine.Generate “db_srv_01.json”, p2
Set p2 = Nothing
‘ オブジェクトの完全破棄
Set oEngine = Nothing
WScript.Echo “All configurations have been successfully generated.”
End Sub
‘ ==============================================================================
‘ Class: TemplateEngine
‘ ==============================================================================
Class TemplateEngine
Private m_fso
Private m_regExp
Private m_templatePath
Private m_outputDir
Private m_templateCache
‘ クラス初期化
Private Sub Class_Initialize()
Set m_fso = CreateObject(“Scripting.FileSystemObject”)
‘ 正規表現エンジンのセットアップ: ${TOKEN_NAME} を抽出
Set m_regExp = CreateObject(“VBScript.RegExp”)
With m_regExp
.Pattern = “\$\{([a-zA-Z0-9_]+)\}”
.IgnoreCase = False
.Global = True
End With
m_templateCache = “”
End Sub
‘ クラス終了処理(メモリの完全開放)
Private Sub Class_Terminate()
Set m_regExp = Nothing
Set m_fso = Nothing
End Sub
Public Property Let TemplatePath(ByVal strPath)
m_templatePath = m_fso.GetAbsolutePathName(strPath)
End Property
Public Property Let OutputDir(ByVal strPath)
m_outputDir = m_fso.GetAbsolutePathName(strPath)
If Not m_fso.FolderExists(m_outputDir) Then
Call CreateFolderRecursive(m_outputDir)
End If
End Property
‘ ————————————————————————–
‘ 生成コアメソッド
‘ ————————————————————————–
Public Sub Generate(ByVal strOutFileName, ByRef oDictParams)
If m_templateCache = “” Then
‘ 初回のみディスクからテンプレートを読み込みメモリキャッシュ
m_templateCache = ReadAllTextUTF8(m_templatePath)
End If
Dim targetContent
targetContent = m_templateCache
‘ 正規表現によるマッチングコレクションの取得
Dim oMatches, oMatch
Set oMatches = m_regExp.Execute(targetContent)
‘ マッチしたトークンを一意に収集し、置換を実施
‘ (同一トークンが複数回出現した場合の重複処理を避けるための作業用Dictionary)
Dim oProcessedTokens
Set oProcessedTokens = CreateObject(“Scripting.Dictionary”)
For Each oMatch In oMatches
Dim fullToken, tokenKey
fullToken = oMatch.Value ‘ 例: “${SERVER_ID}”
tokenKey = oMatch.SubMatches(0) ‘ 例: “SERVER_ID”
If Not oProcessedTokens.Exists(fullToken) Then
If oDictParams.Exists(tokenKey) Then
Dim val
val = oDictParams.Item(tokenKey)
targetContent = Replace(targetContent, fullToken, val)
Else
‘ 定義が存在しない場合は空文字にするかエラーハンドリング(設計方針による)
targetContent = Replace(targetContent, fullToken, “”)
End If
oProcessedTokens.Add fullToken, True
End If
Next
‘ ファイル出力
Dim finalPath
finalPath = m_fso.BuildPath(m_outputDir, strOutFileName)
Call WriteAllTextUTF8(finalPath, targetContent)
‘ リソース解放
Set oProcessedTokens = Nothing
Set oMatches = Nothing
End Sub
‘ ————————————————————————–
‘ ADODB.Streamを用いた堅牢なUTF-8ファイル読み込み
‘ ————————————————————————–
Private Function ReadAllTextUTF8(ByVal strFilePath)
If Not m_fso.FileExists(strFilePath) Then
Err.Raise vbObjectError + 512, “TemplateEngine”, “Template file not found: ” & strFilePath
End If
Dim oStream
Set oStream = CreateObject(“ADODB.Stream”)
With oStream
.Type = 2 ‘ adTypeText
.Charset = “UTF-8”
.Open
.LoadFromFile strFilePath
ReadAllTextUTF8 = .ReadText(-1) ‘ adReadAll
.Close
End With
Set oStream = Nothing
End Function
‘ ————————————————————————–
‘ ADODB.Streamを用いたBOMなしUTF-8書き込み
‘ ————————————————————————–
Private Sub WriteAllTextUTF8(ByVal strFilePath, ByVal strContent)
Dim oSourceStream, oTargetStream
Set oSourceStream = CreateObject(“ADODB.Stream”)
Set oTargetStream = CreateObject(“ADODB.Stream”)
‘ UTF-8として文字列を流し込む(ADODB.StreamはデフォルトでBOMを付加する)
With oSourceStream
.Type = 2 ‘ adTypeText
.Charset = “UTF-8”
.Open
.WriteText strContent
‘ ポジションをバイナリ先頭に戻す
.Position = 0
.Type = 1 ‘ adTypeBinary
.Position = 3 ‘ BOM(先頭3バイト: EF BB BF)をスキップ
‘ バイナリとしてBOM以降を別ストリームに複製
With oTargetStream
.Type = 1 ‘ adTypeBinary
.Open
oSourceStream.CopyTo oTargetStream
.SaveToFile strFilePath, 2 ‘ adSaveCreateOverWrite
.Close
End With
.Close
End With
Set oTargetStream = Nothing
Set oSourceStream = Nothing
End Sub
‘ ————————————————————————–
‘ 再帰的ディレクトリ作成(多重階層パスにも対応)
‘ ————————————————————————–
Private Sub CreateFolderRecursive(ByVal strPath)
Dim parentFolder
parentFolder = m_fso.GetParentFolderName(strPath)
If Not m_fso.FolderExists(parentFolder) Then
CreateFolderRecursive parentFolder
End If
If Not m_fso.FolderExists(strPath) Then
m_fso.CreateFolder strPath
End If
End Sub
End Class
—
4. チーフアーキテクトが教える「極限チューニングの急所」
① BOMの存在を許容しない最新エコシステムへの適合
`ADODB.Stream`でUTF-8出力を行うと、既定で先頭にBOM(Byte Order Mark: `0xEF, 0xBB, 0xBF`)が付与される。
最新のLinux系デーモンやJSONパーサー、Dockerコンテナにこのファイルをマウントすると、パーサがクラッシュする事象が後を絶たない。
上記コードで実装した「Type=1(バイナリ)に切り替えてPosition=3からコピーするテクニック」は、VBScriptで完全なBOMなしUTF-8を吐き出すための最も信頼性の高いバイナリハックである。
② BSTRのキャッシュとメモリライフサイクル
ループ処理の中で毎回ファイルをディスクから読み込んではならない。
本実装では、テンプレート実体を `m_templateCache`(プライベート変数)に保持している。
VBScriptのランタイムエンジンにおいて、巨大な文字列を変数に代入し続けると「メモリコンパクション」が発生せず、OSから確保したワーキングセットがスクリプト終了まで肥大化し続ける現象が発生する。
テンプレートの読み込みは必ず1度にとどめ、メモリ内のバッファを複製して処理に回すのが原則である。
③ COMオブジェクトの明示的破棄(`Set = Nothing`)の真意
「スクリプトが終了すればガベージコレクションが走るから`Set = Nothing`は不要」という甘い言説を信じてはならない。
大量のファイルをループで生成する際、ループ内部でインスタンス化されたCOMオブジェクト(ストリームやディクショナリ)を明示的に解放しないと、参照カウント(Reference Count)がゼロにならず、Windows内部のCoTaskMemFreeが遅延する。結果、数千件の処理中に`Out of Memory`(800A0007)例外を叩き出すことになる。
クラスの `Class_Terminate` を活用し、スコープを抜けたリソースは自らの手で確実にゼロクリアせよ。
—
結び:枯れた技術を「研ぎ澄まされた刃」にするということ
技術の進歩は速いが、Windowsという巨大な基盤の深部には、20年前に設計されたCOMオートメーションとVBScriptが今もなお揺るぎない精度で鎮座している。
外部ランタイムを一切持ち込めない要塞のようなミッションクリティカル環境において、FSOとDictionary、そしてADODB.Streamの挙動を極限まで掌握したスクリプトは、数百行のフットプリントで巨大な自動化ラインを築き上げる。
「何を使うか」ではなく、「その特性をどこまで知覚し、制御できているか」。
本アーキテクチャを現場のレガシー保守・システム自動化の切り札として役立ててほしい。
