【テクニカル・上級編】【メモリ削減】AtEndOfStreamを活用した大容量テキストファイルの1行ずつ高速読み込み処理 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【メモリ削減】AtEndOfStreamを活用した大容量テキストファイルの1行ずつ高速読み込み処理

レガシーシステムの深部、あるいは現代のWindows環境においても、VBScriptはタスクスケジューラから呼び出される軽量なバッチ処理の要として静かに稼働し続けている。
しかし、この枯れた言語を扱うエンジニアがしばしば直面するのが、数ギガバイトに及ぶ巨大ログファイルやトランザクションデータのハンドリングにおける「メモリ不足(Out of Memory)」の壁だ。

今回は、FSO(FileSystemObject)の `ReadAll` が引き起こすメモリ崩壊のメカニズムを解剖し、`AtEndOfStream` を駆使した真のストリーミング処理による省メモリ・高速読み込みの極意を授ける。

—

1. なぜ `ReadAll` は巨大ファイルで死ぬのか?

多くの開発者は、テキストファイルを読む際に以下のようなコードを書く。

‘ 【アンチパターン】絶対に真似してはならない実装
Dim fso, filePath, fileContent
Set fso = CreateObject(“Scripting.FileSystemObject”)
filePath = “C:\Logs\huge_transaction.log”

‘ ファイル全体をメモリ上に一気に展開する
Set fileContent = fso.OpenTextFile(filePath, 1)
Dim allText
allText = fileContent.ReadAll ‘ ← ここで数GBのメモリが吹き飛ぶ
fileContent.Close

Set fileContent = Nothing
Set fso = Nothing

VBScriptのメモリ管理の闇

VBScriptの内部エンジン(`oleaut32.dll`)における文字列型(BSTR)は、長さに応じて動的にヒープ領域を消費する。さらに、スクリプト実行環境である `wscript.exe` / `cscript.exe` は標準で32bitプロセスとして動作することが多く(※環境依存はあるものの)、仮想アドレス空間の限界は約2GBである。

そこに数GBのテキストファイルを `ReadAll` で放り込めば、ヒープ領域の断片化(Heap Fragmentation)を引き起こし、例外コード `0x800A0007(メモリ不足です)` と共にスクリプトは非業の死を遂げる。
さらに、ガベージコレクタ(GC)の挙動が極めて緩慢なVBScriptにおいて、巨大なVariant変数が解放された瞬間のメモリ解放処理は非効率極まりない。

—

2. 解決策:`AtEndOfStream` によるストリーミング処理

この絶望的な制約を突破する唯一の現実解が、ファイルを細切れのストリームとして扱い、1行ずつ逐次処理するアーキテクチャへの移行である。

FSOの `TextStream` オブジェクトが持つ `AtEndOfStream` プロパティと `ReadLine` メソッドを組み合わせることで、メモリ消費量を常に「1行分のサイズ+数バイト」に固定することが可能となる。

実践:極限まで最適化されたストリーミング読込スクリプト

以下に、実業務でそのまま使える堅牢かつ高速な実装パターンを示す。

Option Explicit

Const ForReading = 1
Const TristateUseDefault = -2 ‘ システムデフォルトエンコーディング

Sub ProcessHugeLogFile(ByVal strFilePath)
Dim objFSO, objFile
Set objFSO = CreateObject(“Scripting.FileSystemObject”)

‘ ファイルが存在するかチェック
If Not objFSO.FileExists(strFilePath) Then
WScript.Echo “Error: ファイルが見つかりません -> ” & strFilePath
Exit Sub
End If

‘ TextStreamオブジェクトを開く(共有モード指定: ForReading)
‘ ※ 巨大ファイルへのアクセス時はロック競合に注意
Set objFile = objFSO.OpenTextFile(strFilePath, ForReading, False, TristateUseDefault)

Dim lngLineCount
lngLineCount = 0

Dim strLine
‘ 【核心】EOFに到達するまで1行ずつメモリ上に読み込む
Do While Not objFile.AtEndOfStream
strLine = objFile.ReadLine
lngLineCount = lngLineCount + 1

‘ — ここにビジネスロジック(パース・DB書き込み・フィルタリングなど)を記述 —
‘ 例: 特定のキーワードが含まれる行だけを処理する
If InStr(strLine, “ERROR”) > 0 Then
‘ 処理本体(パフォーマンスのため、ここでの過剰なオブジェクト生成は避ける)
‘ Call WriteToDatabase(strLine)
End If
‘ ————————————————————————–

‘ 10万行ごとにガベージコレクションを意識した進捗出力(必要に応じて)
If lngLineCount Mod 100000 = 0 Then
WScript.Echo lngLineCount & ” 行処理完了…”
End If
Loop

‘ クリーンアップ(リソースの明示的解放はシニアの義務)
objFile.Close
Set objFile = Nothing
Set objFSO = Nothing

WScript.Echo “完了: 総行数 ” & lngLineCount & ” 行”
End Sub

‘ 実行エントリーポイント
Call ProcessHugeLogFile(“C:\Logs\huge_transaction.log”)

—

3. チーフアーキテクトが教えるパフォーマンスチューニングの極意

上記のコードを単に動かすだけでは、真のプロフェッショナルとは言えない。以下のチューニング要件を押さえてこそ、レガシー環境を極限まで搾り取ることができる。

① ループ内のオブジェクト生成を排除せよ

`Do While` ループの中で `CreateObject` や新たなコンポーネントのインスタンス化を行ってはならない。VBScriptのインタプリタはループごとのオブジェクト生成・破棄のオーバーヘッドに非常に弱い。
外部への書き込みが必要な場合(例えばADOを使ったDBインサートなど)は、バルクインサート(トランザクションの一括コミット)の設計を採用し、I/Oの回数自体を激減させなければならない。

② 文字コード(Encoding)の罠に留意せよ

`OpenTextFile` の第4引数(Tristate)の扱いは慎重になるべきだ。

  • `TristateFalse` (0): ASCII (ANSI)
  • `TristateTrue` (-1): Unicode (UTF-16LE)
  • `TristateUseDefault` (-2): システムのデフォルト

現代のシステムから出力されるログは UTF-8(BOM付き/BOMなし) が主流である。残念ながら、純粋なFSOの `OpenTextFile` は BOMなしUTF-8をネイティブで正しくデコードできない。
もし対象ファイルがUTF-8(BOMなし)である場合、FSOではなく `ADODB.Stream` オブジェクトをストリーミングのベースとして採用する必要がある。参考までに、その場合のコアスニペットを提示する。

Dim objStream
Set objStream = CreateObject(“ADODB.Stream”)
objStream.Type = 2 ‘ adTypeText
objStream.Charset = “UTF-8”
objStream.Open
objStream.LoadFromFile “C:\Logs\utf8_huge.log”

Do While Not objStream.EOS
strLine = objStream.TextLine ‘ ※正確にはReadTextを使用
‘ ReadText(-2)で1行読み込み
‘ strLine = objStream.ReadText(-2)
‘ ※厳密な行単位制御には改行コードの制御が必要になるため、FSOとの使い分けが重要
Loop

objStream.Close
Set objStream = Nothing

(※UTF-8の巨大ファイルを扱う極限の現場では、ADODB.Streamの `ReadText` と行区切りの制御を組み合わせるか、あるいはPowerShellへ処理を移譲する判断もシニアアーキテクトとしての重要な決断である。)

③ オブジェクトの明示的解放(`Nothing`代入)の徹底

バッチ処理が長時間稼働する場合、スコープを抜けるまでの間、COMオブジェクトがメモリ上に居座り続ける。
処理の最後には必ず `Set objFile = Nothing` および `Set objFSO = Nothing` を実行し、参照カウンタを即座にデクリメントさせよ。これを怠ると、ゾンビのようなメモリリークが蓄積し、タスクスケジューラの2回目以降の実行で必ず痛い目を見る。

—

総括

VBScriptは「古い言語」として片付けられがちだが、OSの深部に直結した極めて強力な自動化ツールである。
メモリ構造とストリームの概念を正しく理解していれば、数ギガバイトのデータ処理であっても、現代のモダンな言語群に引けを取らない省メモリかつ安定した稼働を実現できる。

「メモリ不足エラー」に怯える日々に終止符を打ち、`AtEndOfStream` を手懐けた真のストリーミング処理を、あなたのシステムにも導入してほしい。

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