【上級プロフェッショナル向け】FSO TextStreamにおけるメモリ上限(Out of Memory)を回避するブロック境界パースアルゴリズム
レガシーシステムの深淵において、VBScriptの`Scripting.FileSystemObject`(以下、FSO)は、今なお基幹バッチ処理の生命線として稼働し続けている。しかし、現代のビッグデータや、API連携によって生成された「改行コードを無視した数GBに及ぶ単一JSON」「ログの肥大化によるモンスター級のCSV」を前に、FSOの`TextStream`は無力な子供へと変貌する。
`ReadAll`や、極端に長い行に対する`ReadLine`を実行した瞬間、プロセスは容赦なく`Out of Memory (エラー 7)`を吐き、OSの仮想メモリを食いつぶして非業の死を遂げる。
本稿では、FSOの限界を突破し、メモリフットプリントを極限まで抑制しながら超巨大ファイルを安全にブロック分割・パースするための「バッファ境界ストリーム・スキャニング技法」を、実戦投入可能なプロダクションコードと共に解き明かす。
—
1. なぜFSO TextStreamは巨大ファイルで破綻するのか
多くのプログラマは、FSOを「テキストファイルを読み込むための万能オブジェクト」と誤解している。しかし、`TextStream`の内部バッファリングメカニズムは、レガシーなCOMアーキテクチャの制約を受けており、文字列操作を行う際にBSTR(Basic String)としてヒープ領域に全データを展開しようとする。
特に致命的なのは以下の2点だ。
1. `ReadLine`の罠: 改行文字 (`vbCrLf` 等) が存在しない場合、ファイル終端(EOF)に至るまでの数GBのデータが1つの文字列としてメモリ上に構築され、即座にヒープ枯渇を引き起こす。
2. ガベージコレクションの不確実性: VBScriptの参照カウント方式(COMのAddRef/Release)は、巨大な文字列が一時変数やスタックに残留している間、メモリの解放を遅延させる。
これを回避するためには、ファイルを「行」単位ではなく、「物理的なバイト数(ブロック)」単位で厳密に切り出し、さらにマルチバイト文字(UTF-8やShift_JIS)の文字境界を破壊しないための高度なオフセット計算が必要となる。
—
2. ブロック境界パースアルゴリズムの設計思想
今回実装するアーキテクチャの核心は、「BinaryStreamによるバイト単位の切り出し」と「FSOの省メモリ性」のハイブリッドにある。
厳密には、純粋なFSOだけでバイト単位のランダムアクセスを行うことは困難(`TextStream`は前方への逐次読み込みしかサポートしない)であるため、バイナリ操作には `ADODB.Stream` を一時的に併用し、メモリ管理とストリームのライフサイクルをチーフアーキテクトの厳格な統制下に置く。
アーキテクチャの要件
- メモリ上限の固定化: ファイルサイズに関わらず、消費メモリは指定したチャンクサイズ(例: 64KB〜1MB)に完全に定数化($O(1)$ 空間複雑度)する。
- マルチバイト文字の保護: 2バイト文字(Shift_JIS)や可変長マルチバイト(UTF-8)の途中でブロックが分断され、文字化けやパースエラーが発生するのを防ぐ境界スキャンを行う。
- 確実なオブジェクト破棄: VBScript特有のメモリリークを防ぐため、エラーハンドリング(`On Error Resume Next`)と連動した確実な解放チェーンを構築する。
—
3. 実装コード:UltraSafeBlockParser.vbs
以下のコードは、改行のない超巨大テキストファイルを安全に読込・分割処理するためのプロダクションコードである。現場のバッチ処理にそのまま組み込むことができる。
Option Explicit
‘ =========================================================================
‘ 模块名: UltraSafeBlockParser.vbs
‘ 概要: FSO/ADODB.Streamを用いた超巨大ファイルのブロック境界パースエンジン
‘ 著作権: チーフアーキテクト専用パターンスニペット
‘ =========================================================================
Const adTypeBinary = 1
Const adTypeText = 2
Const adSaveCreateOverWrite = 2
‘ パフォーマンスとメモリ安全性のバランスを取るチャンクサイズ (1MB)
Const CHUNK_SIZE = 1048576
Sub ProcessHugeFile(ByVal targetFilePath)
Dim objStream
Set objStream = CreateObject(“ADODB.Stream”)
‘ エラーハンドリングの要塞化
On Error Resume Next
With objStream
.Type = adTypeBinary
.Open
.LoadFromFile targetFilePath
If Err.Number <> 0 Then
WScript.Echo “[FATAL] ファイルのロードに失敗しました: ” & targetFilePath & ” (Error: ” & Hex(Err.Number) & “)”
Exit Sub
End If
Dim totalSize, currentPosition, readBytes
totalSize = .Size
currentPosition = 0
WScript.Echo “[INFO] 処理開始: 總サイズ = ” & totalSize & ” bytes”
‘ バイナリモードでチャンクごとに安全に読み出し
Do While currentPosition < totalSize
' 残りサイズがチャンクサイズ未満の場合は調整
Dim bytesToRead
If (totalSize - currentPosition) < CHUNK_SIZE Then
bytesToRead = totalSize - currentPosition
Else
bytesToRead = CHUNK_SIZE
End If
' 現在位置を設定してブロックを抽出
.Position = currentPosition
Dim chunkData
chunkData = .Read(bytesToRead)
If Err.Number <> 0 Then
WScript.Echo “[ERROR] 読み込みエラーが発生しました. Position: ” & currentPosition
Exit Do
End If
‘ ————————————————————-
‘ 【コア処理】抽出したバイナリブロックに対するパース・解析処理
‘ ————————————————————-
Call ParseBlockData(chunkData, currentPosition)
‘ ポジションを進める
currentPosition = currentPosition + bytesToRead
‘ 強制ガベージコレクション誘発(巨大変数の早期破棄)
chunkData = Empty
Loop
.Close
End With
‘ クリーンアップ
Set objStream = Nothing
If Err.Number <> 0 Then
WScript.Echo “[ERROR] 予期せぬ例外が発生しました: ” & Err.Description
Else
WScript.Echo “[INFO] 正常終了: すべてのブロックの処理が完了しました。”
End If
On Error GoTo 0
End Sub
Sub ParseBlockData(ByRef binData, ByVal offset)
‘ —————————————————————–
‘ ここでバイナリ(またはテキスト変換されたデータ)のブロック解析を行う。
‘ 巨大な文字列として一括処理せず、正規表現やバイト配列のスキャンを行うこと。
‘ —————————————————————–
‘ 例として、ADODB.Streamを用いてテキストとして安全にデコードする処理
Dim textStream
Set textStream = CreateObject(“ADODB.Stream”)
With textStream
.Type = adTypeBinary
.Open
.Write binData
.Position = 0
.Type = adTypeText
.Charset = “UTF-8” ‘ 必要に応じて “shift_jis” 等に変更
Dim blockText
blockText = .TextStream.ReadText
‘ — 実際のパースロジック(例: 特定のキーワード検索や構造化抽出) —
‘ WScript.Echo “Offset ” & offset & ” から ” & Len(blockText) & ” 文字を処理中…”
.Close
End With
Set textStream = Nothing
End Sub
‘ 実行エントリーポイント
Dim targetFile
targetFile = “C:\Data\MonsterLog_NoNewlines.json” ‘ 処理対象の巨大ファイルパス
‘ 実行時間の計測開始
Dim startTime
startTime = Timer
Call ProcessHugeFile(targetFile)
WScript.Echo “[PERF] 処理時間: ” & (Timer – startTime) & ” 秒”
—
4. チーフアーキテクトが教える「現場の暗黙知」とチューニング
実務において、上記のコードをさらに極限まで最適化し、ミッションクリティカルな環境で破綻させないための知見を授ける。
① 変数の明示的破棄(`Empty`代入の作法)
VBScriptのスコープを抜けるまで、巨大なバイナリデータ(`chunkData`)や文字列はメモリ上に残留し続ける。ループの最後で明示的に `chunkData = Empty` を実行し、さらに `ExecuteGlobal` やサブプロシージャのスコープ分離を行うことで、VBScriptの脆弱なメモリ管理機構を強制的にハックし、メモリリークを根絶できる。
② 文字コードの境界問題(マルチバイトの呪縛)
UTF-8やShift_JIS等の可変長エンコーディングファイルを単純にバイト数(`CHUNK_SIZE`)で分割すると、漢字の途中でブロックが分断され、文字化けやパースクラッシュを引き起こす。
厳密なパーフェクト・パーシングを求める場合、ブロックの終端数バイトを確認し、それがマルチバイト文字の「先行バイト」に該当するかどうかを判定するバイトスキャンロジックを挟む、あるいは、改行コード(`0x0A`等)の直近でブロック境界をスナップする微調整コードを付加するのが、真のプロフェッショナルの仕事である。
③ タスクスケジューラ連携時の注意点
IISやWindowsタスクスケジューラ経由でこのVBScriptをバッチ実行する場合、WScriptホスト(`cscript.exe`)のメモリ制限や、COMオブジェクトのタイムアウトに直面することがある。必ず `cscript.exe //B //T:3600 script.name.vbs` のように、バッチモード(対話ダイアログの抑制)とタイムアウト設定を明示して起動すること。
—
5. 結びにかえて
「VBScriptは古いからダメだ」「メモリを食うからPythonやNode.jsに書き換えるべきだ」というのは、道具の特性を理解していない素人の言い訳に過ぎない。
Windows環境の奥深く、インフラストラクチャの根底に位置するCOMとオブジェクトのライフサイクルを完全に掌握していれば、VBScriptであっても数GB規模のモンスターファイルを秒速で、かつ極小のメモリフットプリントで調伏することが可能である。
レガシーの限界を技術力で超えていくことこそが、真のエンジニアリングの醍醐味である。本稿の知見が、あなたのシステムを絶望的な「Out of Memory」の恐怖から永遠に解放することを確信している。
