【上級】FSO TextStreamの限界を突破する:超巨大テキストファイルをメモリ枯渇(Out of Memory)させずにブロック境界パースする極意
開発現場において、VBScriptによるファイル処理は今なおレガシーシステムの自動化やバッチ処理の現場で現役だ。特に `Scripting.FileSystemObject (FSO)` と `TextStream` オブジェクトの組み合わせは、手軽にテキストを読み込めるため多用されている。
しかし、ここで一つ、多くのプログラマが陥る「死の罠」がある。
`ReadAll` や `ReadLine` を使ったことがあるだろうか?
改行コードが極端に少ない数ギガバイト級のログファイル、あるいは、Webからスクレイピングした改行無しの数千万文字のJSONデータを読み込ませた瞬間、VBScriptのプロセスは突如として 「メモリ不足です (Out of Memory)」 を吐き、無慈悲にクラッシュする。
なぜなら、FSOの `ReadLine` は改行文字に出会うまでメモリ上にデータを無限にバッファリングし、`ReadAll` に至ってはファイル全体を一度にBSTR(基本文字列型)としてメモリ上に展開しようとするからだ。
今回は、このVBScriptの構造的限界を完全にハックし、メモリ消費量を一定に抑えたまま超巨大ファイルを安全にブロック単位で切り出し、パースし続けるプロフェッショナル・アルゴリズムを伝授する。
—
1. なぜ従来のやり方では破綻するのか?
FSOの `TextStream.Read(Characters)` メソッドは、指定した「文字数」分だけ読み込むことができる。一見するとこれ使えば解決しそうに思えるが、致命的な問題がある。
- 文字数とバイト数の乖離: VBScriptの文字列は内部でUTF-16(1文字2バイト〜4バイト)として扱われる。Shift-JISやUTF-8のマルチバイト文字が混在するファイルを文字数ベースで制御しようとすると、文字の途中でバイナリが分断され、文字化けやパースエラーの温床となる。
- ストリームの巻き戻し不可: FSOの `TextStream` は前方向への順次読み込み(Forward-only)しかサポートしておらず、一度読み飛ばしたデータを容易に再スキャンできない。
これらを解決するためには、「固定長バイナリチャンク(ブロック)単位で安全に読み込み、マルチバイト文字の境界を計算してキャリーオーバー(持ち越し)処理を行うロジック」を自前で実装する必要がある。
—
2. ブロック境界パースアルゴリズムの設計思想
今回構築するアーキテクチャの肝は以下の3点だ。
1. バイト単位の安全な切り出し: 文字数ではなく、ストリームのバイナリ(またはマルチバイトを考慮した安全な文字列長)でブロックを区切る。
2. 不完全な末尾のキャリーオーバー: ブロックの切れ目で「単語」や「JSONのキー」が分断された場合、その端数を次のブロックの先頭に結合して処理する。
3. ガベージコレクションの明示的誘導: VBScriptのBSTRは参照カウント方式をとるため、巨大な文字列を結合・破棄する際は、明示的に変数を空 (`Empty` / `Null`) にしてメモリプレッシャーを逃がす。
—
3. 【プロダクションコード】メモリ枯渇ゼロ・ブロックパーサ
以下のコードは、そのまま実務のバッチ処理に組み込める堅牢なプロダクションコードだ。超巨大なテキストファイルを指定したチャンクサイズ(バイト/文字数ベース)で安全に読み込み、コールバック的に処理を行う。
Option Explicit
‘ ==============================================================================
‘ 処理設定
‘ ==============================================================================
Const ForReading = 1
Const BLOCK_SIZE = 8192 ‘ 一度に処理する安全なブロックサイズ(文字数ベース)
Const TARGET_FILE = “C:\Data\HugeData_No_Newlines.txt”
Sub Main()
Dim fso, ts, totalProcessed
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FileExists(TARGET_FILE) Then
WScript.Echo “エラー: 対象ファイルが存在しません。”
Exit Sub
End If
‘ TextStreamを開く (TristateFalse = ASCII/ANSI, TristateTrue = Unicode)
Set ts = fso.OpenTextFile(TARGET_FILE, ForReading, False, 0)
totalProcessed = 0
Dim carryOver : carryOver = “”
‘ メモリ枯渇を防ぐメインループ
Do While Not ts.AtEndOfStream
‘ 1. ブロック単位で読み込み、前回の余剰分(carryOver)を結合
Dim chunk
chunk = carryOver & ts.Read(BLOCK_SIZE)
‘ 2. ブロック境界の不自然な分断を防ぐため、最後の区切り文字(例: 改行やカンマ)を探す
Dim safeLength, lastDelimiterPos
lastDelimiterPos = InStrRev(chunk, vbCrLf) ‘ 改行を探す場合
If lastDelimiterPos = 0 Then
lastDelimiterPos = InStrRev(chunk, “,”) ‘ カンマ区切りの場合などのフォールバック
End If
Dim currentBlock
If lastDelimiterPos > 0 And Not ts.AtEndOfStream Then
‘ 綺麗に区切れる位置までを今回の処理対象とする
currentBlock = Left(chunk, lastDelimiterPos)
‘ 区切れなかった残りは次回のループへ持ち越す
carryOver = Mid(chunk, lastDelimiterPos + 1)
Else
‘ ファイル終端、または区切りが見つからない場合はブロック全体を処理
currentBlock = chunk
carryOver = “”
End If
‘ ———————————————————————-
‘ 3. ここでブロックごとのビジネスロジック(パース・DB登録など)を実行
‘ ———————————————————————-
Call ProcessBlock(currentBlock, totalProcessed)
‘ 4. 巨大文字列変数を明示的に解放し、メモリリーク・フラグメンテーションを防ぐ
currentBlock = Empty
chunk = Empty
Loop
‘ ループ抜け後の最後のキャリーオーバー処理
If Len(carryOver) > 0 Then
Call ProcessBlock(carryOver, totalProcessed)
carryOver = Empty
End If
ts.Close
Set ts = Nothing
Set fso = Nothing
WScript.Echo “全処理完了。総処理ブロック数/文字数: ” & totalProcessed
End Sub
‘ ==============================================================================
‘ ブロック単位のパース処理(ここに実際の業務ロジックを記述)
‘ ==============================================================================
Sub ProcessBlock(ByRef blockData, ByRef counter)
counter = counter + Len(blockData)
‘ 【実務上のヒント】
‘ ここで RegularExpression (RegExp) を使う場合も、対象が BLOCK_SIZE に
‘ 制限されているため、正規表現エンジンがCPUを食いつぶすのを防げます。
‘ サンプルとして先頭の50文字だけログ出力
Dim preview
If Len(blockData) > 50 Then
preview = Left(blockData, 50) & “…”
Else
preview = blockData
End If
‘ WScript.Echo “Processing: ” & Len(blockData) & ” chars -> ” & preview
End Sub
‘ 実行
Main()
—
4. チーフアーキテクトからの実務アドバイス:データベースやファイル連携の極意
このアルゴリズムを実際の業務システム(日次バッチやETLプロセス)に組み込む際、以下の設計原則を必ず守ってほしい。
① データベーストランザクションの分割
数ギガバイトのファイルを分割パースする際、「ファイル全体のパースに対して1つの巨大なトランザクションを張る」のは絶対法度だ。途中でデッドロックやタイムアウト、あるいはメモリ枯渇以外の要因でバッチが落ちた際、リカバリコストが跳ね上がる。
`ProcessBlock` の内部、あるいは数十ブロックごとにコミット境界(例: 10,000件ごと)を設け、冪等性(Idempotency)を担保した設計にすること。
② 文字コード(BOM・マルチバイト)の罠
VBScriptの `OpenTextFile` の第4引数(Format)の仕様には注意が必要だ。
- `-2` (TristateUseDefault): システムのデフォルト
- `-1` (TristateTrue): Unicode (UTF-16)
- `0` (TristateFalse): ASCII (ANSI / Shift-JISなど)
現代のシステムではUTF-8(BOM付き/無し)のファイルを扱うことが多いが、FSOはネイティブでUTF-8を完璧にサポートしていない(BOMなしUTF-8は文字化けする)。
もしUTF-8ノーマークの超巨大ファイルを扱う必要がある場合は、FSO単体ではなく `ADODB.Stream` オブジェクトをファサードとして採用し、文字コードを明示的に `utf-8` に指定してストリームを制御すべきだ。
—
総括
VBScriptは古い言語と嘲笑されることもあるが、その内部挙動(メモリ管理やストリームの制約)を完全に理解した上でコードを書けば、現代の巨大データ処理の現場であっても、リソースを極限まで削ぎ落とした堅牢な自動化ツールとして強烈な武器になる。
「動けばいい」のフェーズは卒業し、システムに優しく、予期せぬ巨大データすらも華麗にいなすプロフェッショナルなコードを、今日の開発から実装してほしい。
