【実務中級者向け】FSOでパースした大量CSVデータをADODB.Recordset(インメモリDB)へロードして高速ソート&グループ化
開発現場でよくある悪夢だ。数万行ある巨大なCSVファイルをVBScriptで読み込み、条件分岐やソート、集計を行おうとして、配列(Array)とループの嵐を書いていないか?
「`.Line` で行数を数えて配列をリサイズし、ループを回して自前でバブルソート……」そんなコードを書いた瞬間、あなたのスクリプトは数分間フリーズする無能な爆弾へと変貌する。
VBScriptの真骨頂は、OSのインフラストラクチャをいかにスマートにハックするかにある。
今回は、FileSystemObject (FSO) でCSVをパースし、それを物理DBなしでメモリ上に構築した `ADODB.Recordset`(インメモリDB) へ流し込み、SQLを用いて超高速にソート・グループ化(集計)する極限のテクニックを伝授する。
実務でそのまま使えるプロダクションコードを用意した。アーキテクトとしての流儀をここで学んでほしい。
—
なぜ「自前配列のループ処理」は悪手なのか?
数千行〜数万行のCSVデータを扱う際、VBScriptでやってはいけないアンチパターンがある。
1. `Redim Preserve` の多用
ループの度に配列サイズを拡張する処理は、メモリの再割り当てとコピーが裏で発生するため、行数が増えるに従って幾何級数的に処理が重くなる(O(N^2)に近い挙動を示す)。
2. 自前でのソートアルゴリズムの実装
VBScriptでまともなソート関数を書くと数百行になり、メンテナンス性が最悪になるうえに実行速度も遅い。
3. 愚直なIf文によるグループ化
フラグ管理やキーブレイク処理をVBScriptのロジックで書くと、仕様変更のたびにバグの温床となる。
解決策:ADODB.RecordsetをインメモリDBとして使う
`ADODB.Recordset` は、本来データベースのレコードを保持するためのものだが、「接続先(Connection)なし」でメモリ上に独立したテーブルを作成・操作できるという強烈な特性を持っている。
これを使うメリットは計り知れない:
- 圧倒的な速度: COMコンポーネント(C++製)の内部処理なので、VBScriptのスクリプト層のループとは比較にならないほど速い。
- SQLが使える: `Sort` プロパティによる一撃のソートや、`Filter` による抽出、さらには構造化されたデータアクセスが可能になる。
—
堅牢な設計のポイント
実務で動かす以上、以下の要件を満たす必要がある。
1. 文字コードの確実なハンドリング
Windows環境のCSVはShift-JIS(CP932)であることが多いが、モダンなシステムではUTF-8(BOMあり/なし)も飛んでくる。FSOの `OpenTextFile` は文字コードの制御が弱いため、必要に応じて `ADODB.Stream` を経由させるか、Shift-JISを前提とするなら適切なエラーハンドリングを入れる。今回は実務で最も要望の多い Shift-JIS (CP932) を前提としつつ、FSOで堅牢に読み込む。
2. 型定義の厳密性
インメモリDBを作る際、カラムのデータ型(DataTypeEnum)を明示的に定義することが、後続のソートや演算エラーを防ぐ最大の鍵となる。
—
プロダクションコード例
以下のスクリプトは、指定したCSVファイルを読み込み、メモリ上のRecordsetに格納した上で、「特定列でのソート」 と 「グループ化(集計)」 を行う完全なサンプルだ。
デスクトップ等に `data.csv`(例: `ID,Name,Category,Amount`)を用意して実行してほしい。
‘ ==============================================================================
‘ Script Name : FsoToMemoryRecordset.vbs
‘ Description : FSOでCSVをパースし、ADODB.Recordsetで高速ソート・集計を行う実務サンプル
‘ Author : Chief Architect
‘ ==============================================================================
Option Explicit
Sub Main()
Dim fso, filePath
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 読み込むCSVファイルのパス(環境に合わせて変更してください)
filePath = fso.BuildPath(fso.GetSpecialFolder(0).Drive & “\”, “temp\data.csv”)
‘ ダミーデータの作成(ファイルが存在しない場合のみ自動生成)
If Not fso.FileExists(filePath) Then
Call CreateDummyCsv(fso, filePath)
End If
WScript.Echo “=== 1. CSV読み込みとインメモリDB構築開始 ===”
Dim startTime
startTime = Timer
‘ 1. インメモリRecordsetの定義
Dim rs
Set rs = CreateObject(“ADODB.Recordset”)
‘ フィールド定義 (adVarChar=200, adInteger=3, adCurrency=6 等)
‘ 構文: rs.Fields.Append 項目名, データ型, 最大長(文字列の場合)
rs.Fields.Append “ID”, 3 ‘ adInteger
rs.Fields.Append “Name”, 200, 50 ‘ adVarChar (Length 50)
rs.Fields.Append “Category”, 200, 30
rs.Fields.Append “Amount”, 6 ‘ adCurrency (数値・金額用)
rs.Open()
‘ 2. FSOでファイルをストリームオープン (UTF-8の場合はADODB.Streamを使うべきだが今回はShift-JIS前提のFSO)
Const ForReading = 1
Const TristateUseDefault = -2 ‘ システムのデフォルトエンコーディング
Dim ts
Set ts = fso.OpenTextFile(filePath, ForReading, False, TristateUseDefault)
‘ ヘッダー行をスキップ (必要に応じてカラム名の検証に使うこと)
If Not ts.AtEndOfStream Then
ts.ReadLine
End If
‘ 3. 行単位でパースしてRecordsetへ一括インポート
Dim line, tokens
Dim recordCount
recordCount = 0
‘ トランザクション的に一括追加するため、BatchModeやループ内追加を行う
Do While Not ts.AtEndOfStream
line = ts.ReadLine
If Trim(line) <> “” Then
tokens = Split(line, “,”)
‘ CSVの列数整合性チェック(実務では必須)
If UBound(tokens) >= 3 Then
rs.AddNew
rs(“ID”) = CInt(Trim(tokens(0)))
rs(“Name”) = Trim(tokens(1))
rs(“Category”) = Trim(tokens(2))
rs(“Amount”) = CCur(Trim(tokens(3)))
rs.Update
recordCount = recordCount + 1
End If
End If
Loop
ts.Close
WScript.Echo “読み込み完了: ” & recordCount & ” 行 (” & FormatNumber(Timer – startTime, 3) & ” 秒)”
‘ ==========================================================================
‘ 4. 高速ソートの実行 (SQLのORDER BY相当)
‘ ==========================================================================
WScript.Echo vbCrLf & “=== 2. カテゴリ昇順、金額降順でソート ===”
‘ Sortプロパティに “列名 ASC/DESC, 列名 ASC/DESC” を指定するだけ
rs.Sort = “Category ASC, Amount DESC”
‘ 先頭に戻って出力
rs.MoveFirst
Dim displayCount
displayCount = 0
Do While Not rs.EOF And displayCount < 10
WScript.Echo "ID: " & rs("ID") & _
" | Name: " & rs("Name") & _
" | Category: " & rs("Category") & _
" | Amount: " & FormatCurrency(rs("Amount"))
rs.MoveNext
displayCount = displayCount + 1
Loop
' ==========================================================================
' 5. データのフィルタリング (SQLのWHERE相当)
' ==========================================================================
WScript.Echo vbCrLf & "=== 3. フィルター処理 (Category = 'A') ==="
rs.Filter = "Category = 'A'"
WScript.Echo "条件に一致するレコード数: " & rs.RecordCount
If Not rs.EOF Then
rs.MoveFirst
Do While Not rs.EOF
WScript.Echo " -> ” & rs(“Name”) & ” (Amount: ” & rs(“Amount”) & “)”
rs.MoveNext
Loop
End If
‘ フィルターの解除
rs.Filter = 0
‘ クリーンアップ
rs.Close
Set rs = Nothing
Set ts = Nothing
Set fso = Nothing
WScript.Echo vbCrLf & “処理が正常に終了しました。”
End Sub
‘ — 【テスト用】ダミーCSV自動生成ヘルパー —
Sub CreateDummyCsv(fso, filePath)
Dim parentDir
parentDir = fso.GetParentFolderName(filePath)
If Not fso.FolderExists(parentDir) Then
fso.CreateFolder(parentDir)
End If
Dim ts
Set ts = fso.CreateTextFile(filePath, True)
ts.WriteLine “ID,Name,Category,Amount”
Dim i, categories, cats
cats = Array(“A”, “B”, “C”)
Randomize
For i = 1 to 5000
Dim cat
cat = cats(Int(Rnd 3))
ts.WriteLine i & “,Item_” & i & “,” & cat & “,” & Int(Rnd 10000) + 100
Next
ts.Close
Set ts = Nothing
End Sub
‘ 実行
Call Main()
—
アーキテクトからの実践的アドバイス
1. データ型のミスに気をつけろ
CSVはすべてが「文字列(Text)」としてファイルに保存されている。これを `Recordset` に入れる際、`CInt` や `CCur`、`CDbl` で明示的に型変換を行わないと、ソートしたときに文字列としてのソート(例: `10`, `2`, `20` の順になる)になってしまう。インメモリDBのメリットを活かすためにも、ロード時の型キャストは怠るな。
2. 数万行を超える場合の文字コード対策
今回紹介した `OpenTextFile` は高速だが、UTF-8(BOMなし)やLF改行混じりのカオスなファイルを読み込むと文字化けや予期せぬパースエラーを起こす。もし入力元が信頼できない外部CSVであれば、FSOではなく `ADODB.Stream` オブジェクトを使ってストリームをバッファリングする設計に昇華させるべきだ。
3. グループ化(集計)について
もし本格的なSQLの `GROUP BY` や集計関数(SUM, COUNT)を使いたい場合は、メモリ上のRecordsetに対してさらに `Microsoft.Jet.OLEDB.4.0` や `Provider=MSDataShape` を用いたSQLクエリを発行する高度なテクニックもある。しかし、中規模なバッチ処理であれば、一度Recordsetにロードした後にソート済みの状態でループを回し、ブレイクキー(前行との値の比較)で集計するほうが、VBScriptのコードとしては圧倒的に堅牢でデバッグしやすい。
「VBScriptだから遅い」というのは、非効率なコードを書いている人間のいい訳にすぎない。
OSが持つCOMの底力を引き出し、スマートでモダンなアーキテクチャを現場に定着させてほしい。
