【実務・中級編】【実務中級者向け】テキストファイル内のYAML/JSON風インデント構造をFSOと再帰処理で解析しDictionary化するパース術 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【VBScript極限活用】FSOと再帰処理で挑む!インデント構造設定ファイルの自前パース術

業務自動化の現場において、外部ライブラリのインストールが制限されている環境は少なくない。COMコンポーネントのレジストリ登録権限がない、あるいはセキュリティポリシーによりサードパーティ製のJSON/YAMLパーサーを持ち込めない……。そんな「絶望的な制限環境」でも、Windows標準のVBScriptとFileSystemObject(FSO)があれば、複雑な階層構造を持つ設定ファイルを完全に掌握できる。

今回は、実務中級者に向けて、インデントで表現されたYAML/JSON風の設定ファイルをFSOで1行ずつ読み込み、再帰処理と`Scripting.Dictionary`を駆使してネストされたデータ構造としてメモリ上に構築する「堅牢なパース術」を伝授する。

—

なぜ「単純なReadLine」では実務に耐えないのか?

多くの初級プログラマは、テキストファイルを上から順に`ReadLine`で読み込み、適当な文字列置換や`Split`関数で設定値を取り出そうとする。しかし、このアプローチは以下の理由から実務では破綻する。

1. 階層の深さに対応できない: ハードコーディングされたループでは、3階層、4階層とネストが深くなった途端にロジックが崩壊する。
2. インデント(空白の数)の揺れ: 半角スペース2つ、4階層、あるいはタブ混入など、人間の手による編集ミスでパースエラーや意図しない上書きが発生する。
3. データ構造の欠落: 単なるキーと値のペアではなく、「どの親に属する子要素なのか」というコンテキスト(文脈)を保持できない。

これを解決するには、「現在のインデントレベルを監視し、階層構造のツリーを再帰的に構築するアーキテクチャ」が必要となる。

—

アーキテクチャの設計思想

今回構築するパーサーのコアコンセプトは以下の通りである。

  • FSOによるストリーム読み込み: 巨大な設定ファイルであってもメモリを圧迫しないよう、行単位で効率的に処理する。
  • インデント幅の動的検出: スペースの数を数えて「深さ(Depth)」を算出する。
  • Dictionaryの多重ネスト(ツリー構造): 親ノードの中に子ノードとしての`Dictionary`を格納し、無限の階層を表現する。
  • ステートの維持: 再帰呼び出しの間で「現在何行目を読んでいるか」のポインタを共有する。

—

プロダクションコード例

以下のコードは、エラーハンドリングを完備し、そのまま実務の自動化スクリプトに組み込めるプロダクション品質のVBScriptである。

適当な場所に `config.txt` という名前でインデント構造のファイルを作成し、スクリプトを実行して動作を確認してほしい。

1. サンプルの設定ファイル (`config.txt`)

server:
host: “192.168.1.10”
port: 8080
auth:
enabled: true
user: “admin”
token: “xyz123abc”
database:
name: “ProductionDB”
timeout: 30

2. パース実行スクリプト (`parser.vbs`)

Option Explicit

‘ ==============================================================================
‘ スクリーンプレイ: インデント構造設定ファイルの再帰パースとDictionary化
‘ ==============================================================================
Dim fso, targetPath, parsedConfig
Set fso = CreateObject(“Scripting.FileSystemObject”)
targetPath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), “config.txt”)

If Not fso.FileExists(targetPath) Then
WScript.Echo “エラー: 設定ファイルが見つかりません -> ” & targetPath
WScript.Quit 1
End If

‘ 設定ファイルをパースしてネストされたDictionaryを取得
Set parsedConfig = ParseIndentConfig(targetPath)

‘ 取得したデータを検証(再帰的にダンプ出力)
WScript.Echo “=== パース結果の検証 ===”
Call DumpDictionary(parsedConfig, “”)

‘ オブジェクトの解放
Set parsedConfig = Nothing
Set fso = Nothing

‘ ==============================================================================
‘ メイン・パーサー関数(ファイルを開き、行リーダーを再帰関数に渡す)
‘ ==============================================================================
Function ParseIndentConfig(filePath)
Dim fileStream, lines, i
Set fileStream = CreateObject(“Scripting.FileSystemObject”).OpenTextFile(filePath, 1, False)

Dim rawLines()
i = 0
‘ コメント行や空行を除外しながら配列に格納
Do While Not fileStream.AtEndOfStream
Dim lineText: lineText = fileStream.ReadLine
‘ 空行やコメント行(#で始まる行)をスキップ
If Trim(lineText) <> “” And Left(Trim(lineText), 1) <> “#” Then
ReDim Preserve rawLines(i)
rawLines(i) = lineText
i = i + 1
End If
Loop
fileStream.Close
Set fileStream = Nothing

Dim currentIndex: currentIndex = 0
‘ ルートとなるDictionaryを作成
Dim rootDict: Set rootDict = CreateObject(“Scripting.Dictionary”)

‘ 再帰処理の開始(インデントレベル 0 からスタート)
If i > 0 Then
Set rootDict = ParseBlock(rawLines, currentIndex, 0)
End If

Set ParseIndentConfig = rootDict
End Function

‘ ==============================================================================
‘ 再帰的にブロックを解析するコアロジック
‘ ==============================================================================
Function ParseBlock(ByRef lines, ByRef index, targetIndent)
Dim currentDict
Set currentDict = CreateObject(“Scripting.Dictionary”)

Do While index <= UBound(lines) Dim currentLine: currentLine = lines(index) Dim indent: indent = GetIndentWidth(currentLine) ' 現在のブロックのインデントより浅くなったら、親のコンテキストに戻る If indent < targetIndent Then Exit Do End If ' インデントが一致する場合、キーと値を抽出 If indent = targetIndent Then Dim trimmed: trimmed = Trim(currentLine) Dim colonPos: colonPos = InStr(trimmed, ":") If colonPos > 0 Then
Dim key: key = Trim(Left(trimmed, colonPos – 1))
Dim val: val = Trim(Mid(trimmed, colonPos + 1))

‘ 値が空の場合、それは子ブロック(ネスト)の始まりを意味する
If val = “” Then
index = index + 1 ‘ 次の行へ
If index <= UBound(lines) Then Dim nextIndent: nextIndent = GetIndentWidth(lines(index)) If nextIndent > targetIndent Then
‘ 子ブロックを再帰的にパースし、Dictionaryを値として格納
Set currentDict(key) = ParseBlock(lines, index, nextIndent)
Else
‘ 子がない場合は空のDictionaryを格納
Set currentDict(key) = CreateObject(“Scripting.Dictionary”)
End If
Else
Set currentDict(key) = CreateObject(“Scripting.Dictionary”)
End If
Else
‘ 通常の値(文字列、数値、真偽値等)を格納
‘ 必要に応じてクォーテーションの除去や型変換を行う
currentDict(key) = CleanValue(val)
index = index + 1
End If
Else
‘ コロンがない不正な行はスキップまたはエラー処理
index = index + 1
End If
Else
‘ インデントが深すぎる場合は、予期せぬ挙動を防ぐためインクリメントして継続
index = index + 1
End If
Loop

Set ParseBlock = currentDict
End Function

‘ ==============================================================================
‘ ヘルパー関数: 行頭のインデント幅(スペース数)を算出する
‘ ==============================================================================
Function GetIndentWidth(strLine)
Dim i, count
count = 0
For i = 1 To Len(strLine)
Dim c: c = Mid(strLine, i, 1)
If c = ” ” Then
count = count + 1
ElseIf c = vbTab Then
‘ タブはスペース4つ分として換算(環境に合わせて調整)
count = count + 4
Else
Exit For
End If
Next
GetIndentWidth = count
End Function

‘ ==============================================================================
‘ ヘルパー関数: 値から不要なダブルクォーテーションを除去し型を推測する
‘ ==============================================================================
Function CleanValue(strVal)
strVal = Trim(strVal)
‘ 文字列囲みのダブルクォーテーション除去
If Left(strVal, 1) = “””” And Right(strVal, 1) = “””” Then
strVal = Mid(strVal, 2, Len(strVal) – 2)
ElseIf LCase(strVal) = “true” Then
strVal = True
ElseIf LCase(strVal) = “false” Then
strVal = False
ElseIf IsNumeric(strVal) Then
strVal = CDbl(strVal)
End If
CleanValue = strVal
End Function

‘ ==============================================================================
‘ デバッグ用: Dictionaryの内容を再帰的にダンプ出力する
‘ ==============================================================================
Sub DumpDictionary(dict, indentSpace)
Dim k
For Each k In dict.Keys
If IsObject(dict(k)) Then
WScript.Echo indentSpace & “[” & k & “]”
Call DumpDictionary(dict(k), indentSpace & ” “)
Else
WScript.Echo indentSpace & k & ” = ” & dict(k) & ” (” & TypeName(dict(k)) & “)”
End If
Next
End Sub

—

現場で役立つ!実装上の重要ポイントと注意点

1. 参照渡し(`ByRef`)によるポインタの共有

再帰処理を実装する際、最も重要なのが「現在何行目を読んでいるか」というインデックスの状態管理である。
`ParseBlock` 関数内で `index` を `ByRef`(参照渡し)で受け渡している点に注目してほしい。これにより、下位の再帰ブロックが読み進めた行数を、上位のループでも正確に共有・同期させることができる。これを値渡し(`ByVal`)にしてしまうと、無限ループに陥るか、行の読み飛ばしが発生する。

2. メモリ管理とオブジェクトのライフサイクル

VBScriptの `Scripting.Dictionary` は非常に強力だが、COMオブジェクトであるため、不要になったら適切にメモリを解放する意識が求められる。今回のスクリプトでは、再帰的に生成されたオブジェクトツリーがそのままメモリ上に保持される。
実務でこのパーサーを長時間の常駐プロセスや、数千回に及ぶループ内で使用する場合、メモリリークの温床になり得るため、使い終わったルートDictionaryは速やかに `Set parsedConfig = Nothing` で解放することを徹底してほしい。

3. データベースや外部連携への応用

こうしてメモリ上に構築したネストされた `Dictionary` は、次のような実務シナリオで絶大な威力を発揮する。

  • 動的なSQLクエリの構築: 設定ファイルの階層構造をそのまま読み取り、トランザクション設定や接続先(Oracle, SQL Server, SQLite)を切り替える。
  • APIペイロードの動的生成: JSONライブラリが使えない環境でも、Dictionaryの構造を走査して自前でJSON文字列を組み立て、`MSXML2.ServerXMLHTTP` 経由でREST APIに投げる。

—

チーフアーキテクトからの総括

「VBScriptだからできない」というのは、単なるエンジニアの勉強不足か、思考の放棄にすぎない。FSOのストリーム操作と再帰アルゴリズムの基礎さえ抑えておけば、外部ライブラリに依存しない、極めてポータブルで強靭な自動化ツールを組み上げることができる。

今回紹介したコードは、そのままあなたの現場の資産としてコピー&ペーストして活用して構わない。古い技術と侮るなかれ。VBScriptを極めた者こそが、Windows環境における真の自動化マスターなのだ。

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