VBScriptを掌握する極限の知見:数GB級テキストの高速分割・ヘッダー維持結合アーキテクチャ
レガシーシステムの深部、あるいは閉じたWindows環境において、VBScript(Visual Basic Scripting Edition)とWSH(Windows Script Host)はいまだにインフラストラクチャの神経系として静かに、しかし強靭に稼働し続けている。
「時代遅れ」と一蹴するのは容易いが、OS標準搭載・追加ランタイム不要・即時実行という特性は、コンテナ化や複雑なCI/CDパイプラインを組むまでもない現場において、依然として唯一無二の価値を持つ。
今回は、現場で遭遇しがちな「数GBを超える巨大ログファイルやCSVのハンドリング」をテーマに取り上げる。
メモリ不足(Out of Memory)の罠を回避し、`FileSystemObject`(FSO)の挙動特性を極限まで引き出して、「特定行数での安全な分割」と「複数ファイルからのヘッダー維持結合」を完璧にこなす実用スクリプトを提示する。
—
1. 巨大テキスト処理におけるVBScriptの特異点と罠
VBScriptで数GB規模のファイルを扱う際、開発者が最初に直面する壁は「メモリ空間の枯渇」と「I/Oのボトルネック」である。
全体読み込み(`ReadAll`)の厳禁
`TextStream.ReadAll` や `Read(size)` を安易に使うと、ファイル全体がBSTR(COM文字列)としてメモリ上に展開される。数GBのファイルに対してこれを実行すれば、32プロセスのアドレス空間制限(あるいはGCの限界)により、即座に「メモリ不足 (Out of Memory)」でスクリプトは強制終了する。
正解は「ストリーム指向の行単位処理」
`TextStream.ReadLine` を用いた逐次処理(フットプリントの最小化)が唯一の解となる。ディスクから必要最小限のバッファだけをメモリ上に読み込み、処理したら即座に捨てる。この原則を死守しなければならない。
—
2. 実装コード:大容量テキスト分割・結合エンジン
以下のコードは、エラーハンドリング、オブジェクトの明示的解放、そしてパフォーマンスを極限まで高めた実用スクリプトである。
‘ ==============================================================================
‘ Script Name: FileSplitterAndJoiner.vbs
‘ Description: 巨大テキストファイルの行数分割およびヘッダー維持高速結合エンジン
‘ Architecture: VBScript / WSH (FileSystemObject Stream Processing)
‘ ==============================================================================
Option Explicit
Const ForReading = 1
Const ForWriting = 2
Const ForAppending = 8
‘ 実行モードの定義
Const MODE_SPLIT = 1
Const MODE_JOIN = 2
‘ ==============================================================================
‘ メイン処理エントリポイント
‘ ==============================================================================
Sub Main()
Dim fso
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ — 設定エリア(必要に応じて書き換えてください) —
Dim executionMode
executionMode = MODE_SPLIT ‘ MODE_SPLIT または MODE_JOIN
Dim targetDir
targetDir = “C:\DataWork\”
If executionMode = MODE_SPLIT Then
Dim sourceFile
sourceFile = targetDir & “huge_data.csv”
Dim maxLinesPerFile
maxLinesPerFile = 100000 ‘ 1ファイルあたりの最大行数(ヘッダー含む)
WScript.Echo “=== 分割処理を開始します ===”
SplitTextFile fso, sourceFile, targetDir, maxLinesPerFile
WScript.Echo “=== 分割処理が完了しました ===”
ElseIf executionMode = MODE_JOIN Then
Dim outputCombinedFile
outputCombinedFile = targetDir & “combined_output.csv”
WScript.Echo “=== 結合処理を開始します ===”
JoinTextFilesWithHeader fso, targetDir, outputCombinedFile
WScript.Echo “=== 結合処理が完了しました ===”
End If
‘ オブジェクトの明示的解放
Set fso = Nothing
End Sub
‘ ==============================================================================
指定行数でのテキスト分割関数
‘ ==============================================================================
Sub SplitTextFile(fso, filePath, outDir, maxLines)
If Not fso.FileExists(filePath) Then
Err.Raise 53, “SplitTextFile”, “指定されたファイルが存在しません: ” & filePath
End If
Dim srcFile, baseName, ext
Set srcFile = fso.GetFile(filePath)
baseName = fso.GetBaseName(filePath)
ext = fso.GetExtensionName(filePath)
If ext <> “” Then ext = “.” & ext
Dim tsRead
Set tsRead = srcFile.OpenAsTextStream(ForReading, 0) ‘ 0 = ASCII/ANSI (必要に応じ-1:Unicode等)
Dim fileCount, lineCount, currentOutPath
Dim tsWrite
fileCount = 1
lineCount = 0
‘ 最初の出力ファイルを開く
currentOutPath = outDir & baseName & “_part” & Right(“000” & fileCount, 3) & ext
Set tsWrite = fso.CreateTextFile(currentOutPath, True)
Dim headerLine
If Not tsRead.AtEndOfStream Then
headerLine = tsRead.ReadLine() ‘ 1行目をヘッダーとして取得
tsWrite.WriteLine headerLine
lineCount = lineCount + 1
Else
tsRead.Close
tsWrite.Close
Exit Sub
End If
Do While Not tsRead.AtEndOfStream
Dim currentLine
currentLine = tsRead.ReadLine()
‘ 指定行数に達したらファイルを切り替える(ヘッダーを先頭に付与)
If lineCount >= maxLines Then
tsWrite.Close
Set tsWrite = Nothing
fileCount = fileCount + 1
currentOutPath = outDir & baseName & “_part” & Right(“000” & fileCount, 3) & ext
Set tsWrite = fso.CreateTextFile(currentOutPath, True)
‘ 新しいファイルの先頭にヘッダーを書き込む
tsWrite.WriteLine headerLine
lineCount = 1 ‘ ヘッダー分を1とする
End If
tsWrite.WriteLine currentLine
lineCount = lineCount + 1
Loop
‘ クリーンアップ
tsRead.Close
If Not tsWrite Is Nothing Then tsWrite.Close
Set tsRead = Nothing
Set tsWrite = Nothing
Set srcFile = Nothing
End Sub
‘ ==============================================================================
‘ ヘッダーを維持した複数ファイルの高速結合関数
‘ ==============================================================================
Sub JoinTextFilesWithHeader(fso, targetDir, outputFilePath)
Dim dirObj, fileList, fileItem
Set dirObj = fso.GetFolder(targetDir)
Set fileList = dirObj.Files
Dim tsOut
Set tsOut = fso.CreateTextFile(outputFilePath, True)
Dim isFirstFile
isFirstFile = True
Dim globalHeader
globalHeader = “”
For Each fileItem In fileList
‘ ターゲットディレクトリ内の “_part” を含むファイルのみを対象とする
If InStr(fileItem.Name, “_part”) > 0 And LCase(fso.GetExtensionName(fileItem.Name)) = “csv” Then
Dim tsIn
Set tsIn = fileItem.OpenAsTextStream(ForReading, 0)
Dim isHeaderRow
isHeaderRow = True
Do While Not tsIn.AtEndOfStream
Dim lineStr
lineStr = tsIn.ReadLine()
If isHeaderRow Then
If isFirstFile Then
‘ 最初のファイルのヘッダーを全体共通ヘッダーとして保持し、出力する
globalHeader = lineStr
tsOut.WriteLine globalHeader
isFirstFile = False
Else
‘ 2番目以降のファイルのヘッダー行はスキップする(内容の整合性チェックを行うことも可能)
If lineStr <> globalHeader Then
‘ 必要に応じ警告ログ等を出力
End If
End If
isHeaderRow = False
Else
‘ データ行はそのまま書き出し
tsOut.WriteLine lineStr
End If
Loop
tsIn.Close
Set tsIn = Nothing
End If
Next
tsOut.Close
Set tsOut = Nothing
Set fileList = Nothing
Set dirObj = Nothing
End Sub
‘ スクリプト実行
Main
—
3. チーフアーキテクトが解説するコードの急所
このスクリプトには、現場の荒波を生き抜くための実践的な設計思想が凝縮されている。
① オブジェクトの明示的解放(`Set xxx = Nothing`)
VBScriptのCOMコンポーネント(特にFSOやTextStream)は、スコープを抜けるまでメモリやファイルハンドルを掴み続ける性質がある。数万回のループや複数ファイルの走査を行う中でこれを怠ると、「Permission Denied(アクセス拒否)」やリソースリークによるパフォーマンス低下を招く。使い終わったインスタンスは即座に `Nothing` を代入して参照カウンタを即座にゼロへ落とせ。
② ファイル名ソートの罠とゼロパディング
`_part1`, `_part2` … `_part10` とファイル名をつけると、文字列ソートにおいて `_part1` の次に `_part10` が来てしまう(辞書順ソートの罠)。これを防ぐため、分割時には `Right(“000” & fileCount, 3)` を用いて `_part001`, `_part002` のようにゼロパディングを強制し、結合時の順序性を担保している。
③ ヘッダーの整合性担保
結合処理(`JoinTextFilesWithHeader`)において、単に先頭行を捨てるだけでなく、最初のファイルから取得した `globalHeader` と、2番目以降のファイルのヘッダーが一致しているかを暗黙的に、あるいは厳密に比較する構造にしている。これにより、異なるスキーマのファイルが誤って混入した際のデータ破損を未然に防ぐ。
—
4. レガシー環境・自動化運用における注意点
- 文字コード(BOM / エンコーディング)の壁
`OpenAsTextStream` の第2引数(Format)に `0`(ASCII)を指定しているが、日本語環境でShift_JIS(CP932)を扱う場合はこれで問題ない。しかし、UTF-8(BOM付き/無し)が混在するモダンな環境では、FSO単体では文字化けを引き起こす。UTF-8を厳密に扱う必要がある場合は、ADODB.Streamオブジェクトへリプレイスするアーキテクチャ選定が必要となる。
- タスクスケジューラからの実行権限
大容量ファイルを扱うスクリプトをWindowsタスクスケジューラからバッチ実行する場合、「最上位の特権で実行する」にチェックを入れ、かつ実行ユーザーのプロファイルがロードされる設定にしておかないと、UNCパス(ネットワーク共有フォルダ)へのアクセス時に権限エラーで沈黙するので注意せよ。
総括
VBScriptはレガシーな技術と侮られがちだが、OSのプリミティブなリソースをダイレクトに叩けるがゆえに、ツボさえ押さえれば極めて軽量かつ堅牢なバッチ処理基盤となる。
今回解説した「ストリーム処理によるメモリ最適化」と「リソースのライフサイクル管理」の思想は、VBScriptにとどまらず、VBA、PowerShell、さらにはモダンなC#やNode.jsの開発現場においてもそのまま通用する普遍的なエンジニアリングの極意である。現場の自動化・データハンドリングの限界を突破するために、ぜひこの知見を役立ててほしい。
