【実務・中級編】【大容量ファイル分割・結合】FileSystemObject を駆使したテキストの特定行数分割とヘッダー維持結合 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【大容量ファイル分割・結合】FileSystemObject を駆使したテキストの特定行数分割とヘッダー維持結合

開発現場で突如として突きつけられる「数GBに及ぶ巨大CSV・ログファイルの処理」という悪夢。
Excelで開こうとしてメモリー不足で強制終了した経験はないだろうか。あるいは、他のシステムへのインポート制限に引っかかり、頭を抱えたことはないだろうか。

世の中には様々な高機能エディタやPythonなどのスクリプト言語があふれているが、インフラの制約が厳しい閉域網のWindows環境において、追加のランタイムを一切必要とせず、OS標準の機能だけで完結するVBScript(WSH)は、今なお業務自動化の最強のカードであり続ける。

今回は、FileSystemObject(FSO)を極限までチューニングし、数GBクラスの巨大テキストを指定行数で安全に分割し、さらに「ヘッダー行を維持したまま」完璧に再結合するプロダクションレベルのコードを伝授する。

1. なぜ素朴な実装では数GBのファイルで破綻するのか?

多くのプログラマが最初に書くコードは、次のようなものだ。

‘ 【アンチパターン】メモリを枯渇させる危険な実装
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set ts = fso.OpenTextFile(“huge.csv”, 1)
allText = ts.ReadAll ‘ ← ここでファイル全体をメモリ上に読み込む
ts.Close
‘ 巨大ファイルを一気に配列にブチ込む
lines = Split(allText, vbCrLf)

論外である。 `ReadAll` や `Split` を巨大ファイルに対して実行した瞬間、VBScriptの背後にあるCOMコンポーネントはメモリを爆食いし、OSのスワップが発生、最終的に「メモリ不足(Out of memory)」でスクリプトは非業の死を遂げる。

堅牢な設計の鉄則

1. ストリーム処理の徹底: ファイル全体を一度にメモリへ読み込まず、`ReadLine` を使って「1行ずつ」読み込み、書き出す。
2. オブジェクトの即時解放とI/Oの最小化: ループ内での無駄なオブジェクト生成を避け、バッファリングの概念を持ってI/Oコストを抑える。
3. 文字コード(BOM)の制御: Windows環境特有のShift-JIS(CP932)やUTF-8(BOM付き/なし)の文字化け・ヘッダー破壊を防ぐ。

2. プロダクションコード:大容量ファイル分割・結合スクリプト

以下のコードを `FileProcessor.vbs` として保存してほしい。エラーハンドリング、動的なファイル名生成、ヘッダーの確実な継承を実装した、そのまま現場で使える実用コードだ。

Option Explicit

‘ ==============================================================================
‘ 処理設定エリア
‘ ==============================================================================
Dim fso, mode
Set fso = CreateObject(“Scripting.FileSystemObject”)

WScript.Echo “=== 巨大テキストファイル 処理ツール ===” & vbCrLf & _
“1: 指定行数でファイルを分割” & vbCrLf & _
“2: ヘッダーを維持してファイルを結合”
mode = Trim(WScript.StdIn.ReadLine())

Select Case mode
Case “1”
Call SplitFile(fso)
Case “2”
Call MergeFiles(fso)
Case Else
WScript.Echo “無効な選択肢です。終了します。”
End Select

Set fso = Nothing
WScript.Echo “処理が完了しました。”
WScript.Quit

‘ ==============================================================================
‘ 機能1: 指定行数ファイル分割(ヘッダー自動継承)
‘ ==============================================================================
Sub SplitFile(fso)
Dim targetPath, maxLines, headerLine, fileCounter, lineCounter, outTs, inTs
Dim lineData

WScript.Echo “分割するファイルのパスを入力してください:”
targetPath = Trim(WScript.StdIn.ReadLine())
If Not fso.FileExists(targetPath) Then
WScript.Echo “エラー: ファイルが存在しません。”
Exit Sub
End If

WScript.Echo “1ファイルあたりの最大行数(ヘッダー除く)を入力してください(例: 50000):”
maxLines = CLng(Trim(WScript.StdIn.ReadLine()))

Set inTs = fso.OpenTextFile(targetPath, 1, False) ‘ ForReading

‘ ヘッダー行の取得
If inTs.AtEndOfStream Then
WScript.Echo “エラー: ファイルが空です。”
inTs.Close
Exit Sub
End If
headerLine = inTs.ReadLine()

fileCounter = 1
lineCounter = 0
Set outTs = CreateNewSplitFile(fso, targetPath, fileCounter)
outTs.WriteLine headerLine ‘ 最初のファイルのヘッダー書き込み

Do While Not inTs.AtEndOfStream
lineData = inTs.ReadLine()

If lineCounter >= maxLines Then
outTs.Close
Set outTs = Nothing
fileCounter = fileCounter + 1
Set outTs = CreateNewSplitFile(fso, targetPath, fileCounter)
outTs.WriteLine headerLine ‘ 分割ファイルにもヘッダーを継承
lineCounter = 0
End If

outTs.WriteLine lineData
lineCounter = lineCounter + 1
Loop

inTs.Close
If Not outTs Is Nothing Then outTs.Close

WScript.Echo “分割完了: 合計 ” & fileCounter & ” 個のファイルに分割されました。”
End Sub

‘ 分割ファイル生成ヘルパー
Function CreateNewSplitFile(fso, originalPath, index)
Dim parentDir, baseName, ext, newPath
parentDir = fso.GetParentFolderName(originalPath)
baseName = fso.GetBaseName(originalPath)
ext = fso.GetExtensionName(originalPath)

newPath = parentDir & “\” & baseName & “_part” & Right(“000” & index, 3) & “.” & ext
‘ 第3引数 True でファイル新規作成・上書き許可
Set CreateNewSplitFile = fso.CreateTextFile(newPath, True)
End Function

‘ ==============================================================================
‘ 機能2: ヘッダー維持結合
‘ ==============================================================================
Sub MergeFiles(fso)
Dim targetDir, outputPath, folder, file, headerLine, isFirstFile, inTs, outTs
Dim searchExt, targetFiles, fileCount

WScript.Echo “結合するファイルが格納されているフォルダパスを入力してください:”
targetDir = Trim(WScript.StdIn.ReadLine())
If Not fso.FolderExists(targetDir) Then
WScript.Echo “エラー: フォルダが存在しません。”
Exit Sub
End If

WScript.Echo “出力先のファイルパスを入力してください(例: C:\work\merged.csv):”
outputPath = Trim(WScript.StdIn.ReadLine())

Set folder = fso.GetFolder(targetDir)
Set outTs = fso.CreateTextFile(outputPath, True)

isFirstFile = True
fileCount = 0

‘ フォルダ内のファイルを走査(名前順ソートされるわけではない点に注意が必要だが、今回はシンプルなコレクション走査)
For Each file In folder.Files
‘ 拡張子がcsvまたはtxtのものを対象とする(自身が出力するファイルは除外)
If LCase(fso.GetExtensionName(file.Name)) = “csv” Or LCase(fso.GetExtensionName(file.Name)) = “txt” Then
If file.Path <> outputPath Then
Set inTs = fso.OpenTextFile(file.Path, 1, False)

If Not inTs.AtEndOfStream Then
If isFirstFile Then
‘ 1つ目のファイルの時はヘッダーを含めて全行書き込み
headerLine = inTs.ReadLine()
outTs.WriteLine headerLine
isFirstFile = False
Else
‘ 2つ目以降のファイルはヘッダー行を読み飛ばす
inTs.ReadLine()
End If

‘ 残りのデータ行を転記
Do While Not inTs.AtEndOfStream
outTs.WriteLine inTs.ReadLine()
Loop
End If

inTs.Close
fileCount = fileCount + 1
End If
End If
Next

outTs.Close
WScript.Echo “結合完了: ” & fileCount & ” 個のファイルを結合しました。” & vbCrLf & “出力先: ” & outputPath
End Sub

3. チーフアーキテクトが教える、現場でハマる「3つの罠」と対策

このコードを実務に導入する際、シニアエンジニアとして知っておくべき「VBScriptの罠」がある。

① 文字コード(Shift-JIS vs UTF-8)の呪縛

`FileSystemObject` の `OpenTextFile` メソッドは、デフォルトではシステム既定の文字コード(日本語環境なら原則としてShift-JIS / CP932)でファイルを読み書きする。
もし対象のCSVが BOM付きUTF-8UTF-8 (NOCBO) である場合、文字化けや、2つ目以降の結合ファイルでヘッダー行のスキップが狂う原因になる。

  • 対策: 厳密なUTF-8制御が必要な場合は、FSOではなく `ADODB.Stream` オブジェクトを使用するべきだ。ただし、社内ニッチなレガシーログや標準的なShift-JISのCSVであれば、上記のFSO実装で十分高速かつ安定して動作する。

② ファイルの処理順序(ソート順の保証)

`MergeFiles` サブルーチンにおける `For Each file In folder.Files` は、OSのファイルシステムが返す順序に依存するため、必ずしもファイル名の昇順(part001, part002…)になるとは限らない

  • 対策: ファイル名が `_part001.csv`, `_part002.csv` のようにゼロパディングされている前提であれば、本来はVBScript側でFileSystemObjectの代わりに `WScript.Shell` から `cmd.exe` の `dir /b /on` を経由して配列に格納するか、外部のファイル名順にソートするロジックを挟むと完璧だ。今回は可読性とポータビリティを重視しシンプルに記述しているが、実運用ではファイル名の命名規則を厳格に統一してほしい。

③ 巨大I/Oにおけるパフォーマンス

VBScriptはインタプリタ言語であり、1行ごとのファイル読み書き(`ReadLine` / `WriteLine`)は、ファイルサイズが数GBを超えるとそれなりに時間がかかる(数百万行で数分程度)。
しかし、途中でメモリ溢れを起こしてシステムを停止させるリスクに比べれば、確実なストリーム処理による安定性は圧倒的なアドバンテージとなる。夜間バッチやタスクスケジューラに組み込んでサイレント実行させるのがベストプラクティスだ。

4. おわりに

「古い技術」と侮られがちなVBScriptだが、オブジェクトのライフサイクルを正しく理解し、メモリ効率を考慮した設計を行えば、現代の巨大データ処理の現場においても「最も手軽で確実な特効薬」になり得る。

スクリプトはツールに過ぎない。重要なのは、背後にあるリソース(メモリ・I/O)の限界をロジカルに見極め、破綻しないアーキテクチャを構築することだ。
今日の業務から、無駄なメモリ枯渇エラーとは無縁の堅牢な自動化ライフを始めてほしい。

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