【実務・中級編】【実務中級者向け】単一の巨大CSVを特定キー列(支社コード・日付等)ごとに動的分割出力するデータ整理バッチ – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

VBScript極限性能:数十万行の巨大CSVを即座に動的分割するストリーム制御アーキテクチャ

数十万行、数GBクラスの巨大CSVファイルを前にして、VBScriptがメモリ不足(Out of Memory)でダウンする、あるいは処理完了までに何時間もかかると嘆いてはいませんか?

現場の浅い知識で書かれたスクリプトによくある致命的な設計ミスは、「全データを配列やメモリ上に一括保持しようとする」こと、そして「一行書くごとにファイルを開いて閉じるディスクI/Oの乱発」です。

本記事では、VBScriptのプロセスメモリ消費量を常に数MBレベルの定数($O(1)$)に抑えつつ、指定した特定キー(支社コードや日付など)ごとにファイルを動的に切り替えて高速出力する「ストリーム・ハンドラ・キャッシュパターン」の完全設計を伝授します。

—

1. なぜあなたのCSV分割バッチは遅いのか?(アンチパターンの解剖)

コードを書く前に、VBScript(`cscript.exe` / `wscript.exe`)の内部挙動とリソース制限を正しく理解しましょう。初心者が犯しがちな2つの致命的なアンチパターンを排除しなければ、真の実用性は手に入りません。

アンチパターン1:`ReadAll` や巨大配列へのデータ一括読み込み

‘ 【絶対 NG】メモリ爆発を招くアンチパターン
Dim allContent, lines
allContent = tsInput.ReadAll() ‘ 巨大ファイルを一度にヒープ領域へ読み込む
lines = Split(allContent, vbCrLf) ‘ 文字列配列の大量生成によりGC負荷が限界突破

FSO(`Scripting.FileSystemObject`)の `ReadAll` メソッドや、巨大な文字列に対する `Split` 関数は、VBScriptの32bitプロセスメモリ上限(約2GB)をあっという間に圧迫します。メモリの割り当てとガベージコレクション(GC)のオーバーヘッドにより、処理速度は指数関数的に低下します。

アンチパターン2:1行ごとのファイル「Open-Write-Close」ループ

‘ 【絶対 NG】ディスクI/Oボトルネックの極み
Do Until tsInput.AtEndOfStream
line = tsInput.ReadLine()
key = ExtractKey(line)

‘ 行ごとにファイルを開いて閉じる(カーネルオブジェクトの生成・破棄が超高コスト)
Set tsOut = fso.OpenTextFile(OUTPUT_DIR & key & “.csv”, 8, True) ‘ 8 = ForAppending
tsOut.WriteLine line
tsOut.Close
Loop

OSにとってファイルハンドルのオープン/クローズは非常に重いシステムコールです。数十万行のループ内でこれを繰り返すと、ディスクI/Oがボトルネックとなり、処理時間は何倍にも跳ね上がります。

—

2. 実務で勝つための堅牢なアーキテクチャ設計

本ソリューションで採用する設計パターンは以下の通りです。

[入力CSV Stream]
│ (ReadLine: 1行ずつ逐次読み込み)
▼
[キー抽出 & バリデーション]
│
├─► [ファイルハンドル・キャッシュ (Dictionary)]
│ │
│ ├─► 初出キー: 新規ファイル作成 + ヘッダー書き込み + ハンドル保持
│ └─► 既出キー: 保持しているハンドルの WriteLine を呼び出し
▼
[全行処理完了後に一括 Close & メモリ解放]

アーキテクチャの要点:

1. 完全ストリーム処理(`ReadLine`): メモリ上には常に「現在処理中の1行」しか存在させない。
2. 動的書き込みハンドルの保持(`Scripting.Dictionary`): 出力先ファイルオブジェクト(`TextStream`)を辞書型オブジェクトに保持し、オープン状態を維持したまま `WriteLine` を叩く。
3. ヘッダーの自動伝搬: 分割生成されたすべての出力ファイルに対し、オリジナルCSVの1行目(ヘッダー)を自動的に付与する。
4. ファイル名サニタイズ: キー文字列に含まれる可能性があるパス禁止文字(`\ / : ? ” < > |`)を安全に置換する。

—

3. プロダクション適用可能:完全版VBScriptソースコード

以下のコードは、そのまま本番環境に投入可能な品質で記述されています。設定値を環境に合わせて変更し、`cscript.exe` で実行してください。

‘ ==============================================================================
‘ 【目的】巨大CSVファイルを特定キー列(例: 支社コード)ごとに動的分割出力する
‘ 【特徴】ストリーム処理による低メモリ運用、ファイルハンドルキャッシュによる高速化
‘ 【動作環境】Windows Standard Script Host (cscript.exe 推進)
‘ ==============================================================================
Option Explicit

‘ — 定数定義 —
Const ForReading = 1
Const ForWriting = 2
Const ForAppending = 8

‘ FSOの文字コード指定 (-2: システムデフォルト/Shift-JIS, -1: Unicode, 0: ASCII)
Const TristateUseDefault = -2

‘ — 設定パラメータ —
Const INPUT_FILE_PATH = “C:\BatchProcess\Data\LargeSourceData.csv” ‘ 分割対象CSV
Const OUTPUT_DIR = “C:\BatchProcess\Output\” ‘ 出力先フォルダ
Const KEY_COLUMN_NAME = “支社コード” ‘ キーとする列名(ヘッダー名)

‘ — メイン処理呼び出し —
Call Main()

Sub Main()
Dim startTime: startTime = Timer
WScript.Echo “=== CSV動的分割処理を開始します ===”

Dim fso, tsInput
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ 1. 入力ファイルの存在チェック
If Not fso.FileExists(INPUT_FILE_PATH) Then
WScript.Echo “[ERROR] 入力ファイルが存在しません: ” & INPUT_FILE_PATH
WScript.Quit 1
End If

‘ 2. 出力フォルダの自動生成
If Not fso.FolderExists(OUTPUT_DIR) Then
fso.CreateFolder(OUTPUT_DIR)
End If

‘ 3. 入力ファイルのオープン(ストリーム処理)
Set tsInput = fso.OpenTextFile(INPUT_FILE_PATH, ForReading, False, TristateUseDefault)

If tsInput.AtEndOfStream Then
WScript.Echo “[WARN] 入力ファイルが空です。”
tsInput.Close
WScript.Quit 0
End If

‘ 4. ヘッダー行の解析とキー列インデックスの特定
Dim headerLine: headerLine = tsInput.ReadLine()
Dim keyColumnIndex: keyColumnIndex = GetColumnIndex(headerLine, KEY_COLUMN_NAME)

If keyColumnIndex = -1 Then
WScript.Echo “[ERROR] 指定されたキー列名が見つかりません: ” & KEY_COLUMN_NAME
tsInput.Close
WScript.Quit 1
End If

‘ 5. 動的分割処理のループ実行
Dim dictStreams: Set dictStreams = CreateObject(“Scripting.Dictionary”)
Dim line, keyVal, safeKeyVal, tsOutput
Dim totalRows: totalRows = 0
Dim createdFilesCount: createdFilesCount = 0

Do Until tsInput.AtEndOfStream
line = tsInput.ReadLine()
totalRows = totalRows + 1

‘ 空行スキップ
If Trim(line) <> “” Then
‘ 行からキー値を取得
keyVal = ExtractColumnValue(line, keyColumnIndex)

‘ ファイル名に使用できない文字のサニタイズ
safeKeyVal = SanitizeFileName(keyVal)
If safeKeyVal = “” Then safeKeyVal = “_UNKNOWN_”

‘ ハンドルの参照または動的生成
If Not dictStreams.Exists(safeKeyVal) Then
‘ 新規出力ファイルの生成とハンドルのキャッシュ
Dim outPath: outPath = fso.BuildPath(OUTPUT_DIR, safeKeyVal & “.csv”)
Set tsOutput = fso.CreateTextFile(outPath, True, False) ‘ Shift-JIS出力

‘ 新規ファイルには必ずヘッダーを書き込む
tsOutput.WriteLine headerLine

‘ 辞書にハンドルを保持
dictStreams.Add safeKeyVal, tsOutput
createdFilesCount = createdFilesCount + 1
Else
Set tsOutput = dictStreams.Item(safeKeyVal)
End If

‘ データ行の出力
tsOutput.WriteLine line
End If

‘ 10万行ごとの進捗表示(バッチの稼働状態を可視化)
If totalRows Mod 100000 = 0 Then
WScript.Echo FormatNumber(totalRows, 0) & ” 行処理完了…”
End If
Loop

‘ 6. クリーンアップ処理(オープンしている全ハンドルの明示的クローズ)
tsInput.Close
Set tsInput = Nothing

Dim k
For Each k In dictStreams.Keys
dictStreams.Item(k).Close
Set dictStreams.Item(k) = Nothing
Next
dictStreams.RemoveAll
Set dictStreams = Nothing
Set fso = Nothing

Dim duration: duration = Round(Timer – startTime, 2)
WScript.Echo “=== 処理が正常終了しました ===”
WScript.Echo “総処理行数: ” & FormatNumber(totalRows, 0) & ” 行”
WScript.Echo “生成ファイル数: ” & createdFilesCount & ” ファイル”
WScript.Echo “所要時間: ” & duration & ” 秒”
End Sub

‘ ==============================================================================
‘ 【補助関数群】保守性と堅牢性を担保するモジュール群
‘ ==============================================================================

‘ CSVのヘッダー行から特定列のインデックス(0始まり)を取得する
Function GetColumnIndex(headerLine, targetColumnName)
Dim cols, i
cols = Split(headerLine, “,”)
For i = 0 To UBound(cols)
‘ 前後のダブルクォーテーションや空白を除去して比較
If Trim(Replace(cols(i), “”””, “”)) = targetColumnName Then
GetColumnIndex = i
Exit Function
End If
Next
GetColumnIndex = -1
End Function

‘ CSVの1行から指定インデックスの値を抽出する(簡易カンマ区切り解析)
Function ExtractColumnValue(line, colIndex)
Dim cols
cols = Split(line, “,”)
If colIndex <= UBound(cols) Then ExtractColumnValue = Trim(Replace(cols(colIndex), """", "")) Else ExtractColumnValue = "" End If End Function ' ファイル名に使用できない特殊文字をアンダースコアに置換する Function SanitizeFileName(fileName) Dim regEx Set regEx = CreateObject("VBScript.RegExp") regEx.Global = True regEx.IgnoreCase = True ' Windowsファイル名禁止文字: \ / : ? " < > |
regEx.Pattern = “[\\/:?””<>|]”
SanitizeFileName = regEx.Replace(fileName, “_”)
End Function

—

4. 現場運用で差がつくアーキテクトの深掘り知見

コードをコピー&ペーストして満足してはいけません。実務の現場でトラブルを起こさないための「3つの技術的境界条件」を解説します。

① OSのファイルハンドル制限とメモリ対策(キーの種類が膨大な場合)

もし分割キー(支社コード等)の種類が数千~数万種類に及ぶ場合、全ハンドルの開きっぱなし(キャッシュ)はOSの利用可能ハンドル数上限やメモリを圧迫します。

  • キーの種類が数百程度: 上記コードのキャッシュ方式で最高速度が出ます。
  • キーの種類が数千以上(未知): キー値ごとに毎回ファイルを開閉する(ただし `ForAppending` モード)か、一度キー一覧を割り出す「2パス(2回読み込み)方式」への切り替えを検討してください。

② 文字コード(Encoding)の罠とADODB.Streamの使い分け

FSO(`FileSystemObject`)は、Shift-JIS(ANSI)または UTF-16 しかネイティブで扱えません。現代のWeb系システムから出力される BOMなし UTF-8 のCSVを直接分解する場合、FSOでは文字化けが発生します。
UTF-8を厳密に扱う必要がある場合は、`ADODB.Stream` オブジェクトを使用してバイナリストリームとして読み込む設計に変更してください。

③ 実行環境の選択:`wscript.exe` ではなく `cscript.exe` を使用する

このバッチを実行する際、ダブルクリック(`wscript.exe`)で起動してはいけません。ダイアログが連続で表示されたり、コンソールへのプロ

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