VBScript型推論エンジン構築:先頭データ走査による動的スキーマ自動解析ロジック
企業システムにおけるデータ連携基盤において、「ヘッダーが存在しない巨大なCSVデータ」の取り扱いは日常茶飯事です。
多くの開発者が陥る愚策は、「全データを文字列(VARCHAR)として受けてから後処理で型変換する」、あるいは「全行を配列に読み込んでメモリを食いつぶしながら判定する」という設計です。これらは大容量データに直面した瞬間にメモリ不足(OutOfMemory)や致命的な処理遅延を引き起こします。
本稿では、VBScript(Windows Script Host)の制約を完全に理解した上で、先頭サンプル数千行を高速ストリーミング走査し、数値・日付・文字列を全自動で高精度に判別、即座に最適なSQL DDL(CREATE TABLE文)を生成する型推論エンジンの設計と完全実装を解説します。
—
1. 動的スキーマ解析におけるアーキテクチャ設計方針
データ型推論において最も重要なのは「データ型の昇格マトリクス(Type Promotion Matrix)」の設計です。
CSVの各セルは、パース時点では単なる「文字列」に過ぎません。先頭からデータを行単位で走査する際、列ごとに「現在判明している最も制約の強い型」を保持し、矛盾するデータが現れるたびに安全な型へと「昇格(Promote)」させる必要があります。
型の優先順位と昇格ルール(Type Hierarchy)
型推論の優先階層(右に行くほど制約が緩い型):
$$\text{UNKNOWN (空)} \longrightarrow \text{INTEGER} \longrightarrow \text{DOUBLE} \longrightarrow \text{DATE} \longrightarrow \text{STRING}$$
| 現在の推論型 | 新規出現データの判定型 | 昇格後の型 | 判定のロジック・注意点 |
| :— | :— | :— | :— |
| UNKNOWN | Any | その型 | 初期状態。ヌル値・空文字は無視して型を維持 |
| INTEGER | DOUBLE | DOUBLE | 整数から浮動小数点数への自然な拡張 |
| INTEGER | DATE | STRING | 整数と日付が混在する列は安全のため文字列へ |
| DOUBLE | DATE | STRING | 浮動小数点と日付が混在する列は文字列へ |
| INTEGER/DOUBLE/DATE | STRING | STRING | 文字列が現れた時点で不可逆的にSTRING確定 |
重要な例外処理:郵便番号やコード値の「前ゼロ(Leading Zero)」問題
数値のみで構成されていても、`”00123″` のようなデータは INTEGER型と判定してはなりません。DBにINTEGERとして投入すると `123` に変換され、桁落ち(データ損壊)が発生するためです。
本エンジンでは、「桁数が2桁以上で先頭が ‘0’ かつ数値」のデータは強制的にSTRING型と判定するロジックを組み込みます。
—
2. 実用プロダクションコード:`CsvSchemaInferrer.vbs`
以下のコードは、外部ライブラリに一切依存せず、Windows標準のWSH環境(`cscript.exe`)のみでそのまま動作する完全実装です。
‘ ==============================================================================
‘ ファイル名: CsvSchemaInferrer.vbs
‘ 概要 : ヘッダーなしCSVの先頭データを高速走査し、データ型を自動推論して
‘ CREATE TABLE DDLおよび型情報を出力する動的スキーマ解析エンジン
‘ 動作環境 : Windows Script Host (cscript.exe 実行推奨)
‘ ==============================================================================
Option Explicit
‘ ——————————————————————————
‘ 型定数定義
‘ ——————————————————————————
Const TYPE_UNKNOWN = 0
Const TYPE_INTEGER = 1
Const TYPE_DOUBLE = 2
Const TYPE_DATE = 3
Const TYPE_STRING = 4
‘ ——————————————————————————
‘ メイン処理実行ルーチン
‘ ——————————————————————————
Sub Main()
Dim csvFilePath, sampleRowCount, inferrer, ddl
‘ 設定パラメータ
csvFilePath = “C:\Data\Input_Headerless.csv” ‘ 解析対象CSVパス
sampleRowCount = 2000 ‘ サンプル走査行数(推論用)
‘ コマンドライン引数があれば優先取得
If WScript.Arguments.Count > 0 Then
csvFilePath = WScript.Arguments(0)
End If
WScript.Echo “=== CSV Schema Inference Engine Started ===”
WScript.Echo “Target File: ” & csvFilePath
WScript.Echo “Scanning Top ” & sampleRowCount & ” lines…”
‘ クラスインスタンス生成と解析実行
Set inferrer = New CsvSchemaInferrer
If inferrer.AnalyzeCSV(csvFilePath, sampleRowCount) Then
WScript.Echo vbCrLf & “— Inferred DDL Statement —”
ddl = inferrer.GenerateDDL(“TargetTable”)
WScript.Echo ddl
WScript.Echo “==========================================”
Else
WScript.Echo “[ERROR] Analysis failed.”
End If
Set inferrer = Nothing
End Sub
‘ ——————————————————————————
‘ クラス名: CsvSchemaInferrer
‘ 機能 : CSV解析および型推論エンジン本体
‘ ——————————————————————————
Class CsvSchemaInferrer
Private m_columnTypes() ‘ 各列の推論型配列
Private m_columnLengths()’ 各列の最大文字長配列 (STRING用)
Private m_regExInt ‘ INTEGER判定用正規表現
Private m_regExDouble ‘ DOUBLE判定用正規表現
Private m_regExDate ‘ DATE判定用正規表現
Private m_columnCount ‘ 列数
Private Sub Class_Initialize()
m_columnCount = 0
‘ 正規表現オブジェクトの事前生成(ループ内での生成破棄によるオーバーヘッドを回避)
Set m_regExInt = New RegExp
m_regExInt.Pattern = “^-?\d+$”
m_regExInt.IgnoreCase = True
Set m_regExDouble = New RegExp
m_regExDouble.Pattern = “^-?\d+\.\d+$”
m_regExDouble.IgnoreCase = True
‘ ISO形式(YYYY-MM-DD / YYYY/MM/DD)および時刻付き形式に対応する正規表現
Set m_regExDate = New RegExp
m_regExDate.Pattern = “^\d{4}[-/]\d{1,2}[-/]\d{1,2}(\s+\d{1,2}:\d{2}:\d{2})?$”
m_regExDate.IgnoreCase = True
End Sub
Private Sub Class_Terminate()
Set m_regExInt = Nothing
Set m_regExDouble = Nothing
Set m_regExDate = Nothing
End Sub
‘ ————————————————————————–
‘ メソッド: AnalyzeCSV
‘ 機能 : CSVファイルを指定行数読み込み、列ごとの型を推論する
‘ ————————————————————————–
Public Function AnalyzeCSV(ByVal filePath, ByVal maxRows)
On Error Resume Next
‘ ADODB.Stream を利用して UTF-8 / ASCII を安全にストリーミング読み込み
Dim stream
Set stream = CreateObject(“ADODB.Stream”)
stream.Type = 2 ‘ adTypeText
stream.Charset = “UTF-8” ‘ 環境に応じて “Shift_JIS” 等に変更可能
stream.Open
stream.LoadFromFile(filePath)
If Err.Number <> 0 Then
WScript.Echo “[ERROR] File open error: ” & Err.Description
AnalyzeCSV = False
Exit Function
End If
Dim lineCount, lineText, fields, i, fieldVal, inferredType
lineCount = 0
Do Until stream.EOS Or (lineCount >= maxRows)
lineText = stream.ReadText(-2) ‘ -2: adReadLine
lineCount = lineCount + 1
‘ 空行のスキップ
If Trim(lineText) <> “” Then
‘ CSV行のパース (簡易Splitではなくダブルクォーテーション考慮パース)
fields = ParseCsvLine(lineText)
‘ 初回行にて配列領域を動的確保
If m_columnCount = 0 Then
m_columnCount = UBound(fields) + 1
ReDim m_columnTypes(m_columnCount – 1)
ReDim m_columnLengths(m_columnCount – 1)
For i = 0 To m_columnCount – 1
m_columnTypes(i) = TYPE_UNKNOWN
m_columnLengths(i) = 0
Next
End If
‘ 列ごとの評価ルーチン
For i = 0 To UBound(fields)
If i < m_columnCount Then
fieldVal = Trim(fields(i))
' 文字列長更新
If Len(fieldVal) > m_columnLengths(i) Then
m_columnLengths(i) = Len(fieldVal)
End If
‘ 型判定と昇格 logic
If fieldVal <> “” Then
inferredType = EvaluateValueType(fieldVal)
m_columnTypes(i) = PromoteType(m_columnTypes(i), inferredType)
End If
End If
Next
End If
Loop
stream.Close
Set stream = Nothing
AnalyzeCSV = True
End Function
‘ ————————————————————————–
‘ メソッド: EvaluateValueType
‘ 機能 : 単一の文字列値からデータ型を自動判定する
‘ ————————————————————————–
Private Function EvaluateValueType(ByVal val)
‘ 1. 先頭ゼロ(Leading Zero)の判定 -> 強制STRING (例: “00123”, “08012345678”)
If Len(val) > 1 And Left(val, 1) = “0” And IsNumeric(val) Then
EvaluateValueType = TYPE_STRING
Exit Function
End If
‘ 2. INTEGER 判定
If m_regExInt.Test(val) Then
‘ VBScriptのLong範囲(-2,147,483,648 ~ 2,147,483,647)チェック
On Error Resume Next
Dim cLngVal
cLngVal = CLng(val)
If Err.Number = 0 Then
EvaluateValueType = TYPE_INTEGER
Else
EvaluateValueType = TYPE_DOUBLE ‘ オーバーフロー時はDoubleへ
Err.Clear
End If
On Error GoTo 0
Exit Function
End If
‘ 3. DOUBLE 判定
If m_regExDouble.Test(val) Then
EvaluateValueType = TYPE_DOUBLE
Exit Function
End If
‘ 4. DATE 判定 (正規表現とIsDateのダブルチェック)
If m_regExDate.Test(val) Then
If IsDate(val) Then
EvaluateValueType = TYPE_DATE
Exit Function
End If
End If
‘ 5. いずれにも該当しない場合は STRING
EvaluateValueType = TYPE_STRING
End Function
‘ ————————————————————————–
‘ メソッド: PromoteType
‘ 機能 : 現在の型と新規判定型を比較し、適切な上位型へ昇格させる
‘ ————————————————————————–
Private Function PromoteType(ByVal currentType, ByVal newType)
‘ 同一型、または新規データがUNKNOWNの場合は維持
If currentType = newType Or newType = TYPE_UNKNOWN Then
PromoteType = currentType
Exit Function
End If
‘ 現在がUNKNOWNなら新規型を採用
If currentType = TYPE_UNKNOWN Then
PromoteType = newType
Exit Function
End If
‘ STRINGへの昇格(不可逆)
If currentType = TYPE_STRING Or newType = TYPE_STRING Then
PromoteType = TYPE_STRING
Exit Function
End If
‘ 数値型の昇格マトリクス
If currentType = TYPE_INTEGER And newType = TYPE_DOUBLE Then
PromoteType = TYPE_DOUBLE
Exit Function
ElseIf currentType = TYPE_DOUBLE And newType = TYPE_INTEGER Then
PromoteType = TYPE_DOUBLE
Exit Function
End If
‘ 数値系と日付系が衝突した場合は安全のためSTRINGに落とす
PromoteType = TYPE_STRING
End Function
‘ ————————————————————————–
‘ メソッド: ParseCsvLine
‘ 機能 : エスケープされたカンマ・ダブルクォーテーションに対応した標準CSVパース
‘ ————————————————————————–
Private Function ParseCsvLine(ByVal lineText)
Dim list(), count, inQuotes, i, ch, nextCh, token
count = 0
inQuotes = False
token = “”
ReDim list(10)
For i = 1 To Len(lineText)
ch = Mid(lineText, i, 1)
If ch = “””” Then
‘ クォート処理
If inQuotes And i < Len(lineText) Then
nextCh = Mid(lineText, i + 1, 1)
If nextCh = """" Then
token = token & """"
i = i + 1 ' エスケープされたダブルクォートをスキップ
Else
inQuotes = False
End If
Else
inQuotes = True
End If
ElseIf ch = "," And Not inQuotes Then
' カンマ区切り
If count > UBound(list) Then ReDim Preserve list(UBound(list) + 10)
list(count) = token
count = count + 1
token = “”
Else
token = token & ch
End If
Next
‘ 最終列の追加
If count > UBound(list) Then ReDim Preserve list(count)
list(count) = token
‘ 配列のサイズを適正化して返却
ReDim Preserve list(count)
ParseCsvLine = list
End Function
‘ ————————————————————————–
‘ メソッド: GenerateDDL
‘ 機能 : 解析結果に基づき ANSI-SQL 準拠の CREATE TABLE 文を自動生成
‘ ————————————————————————–
Public Function GenerateDDL(ByVal tableName)
Dim ddl, i, colType, colLen, typeName
ddl = “CREATE TABLE ” & tableName & ” (” & vbCrLf
For i = 0 To m_columnCount – 1
colType = m_columnTypes(i)
colLen = m_columnLengths(i)
Select Case colType
Case TYPE_INTEGER
typeName = “INT”
Case TYPE_DOUBLE
typeName = “FLOAT”
Case TYPE_DATE
typeName = “DATETIME”
Case TYPE_STRING, TYPE_UNKNOWN
‘ 判定不能および文字列はVARCHAR設定(余裕を持たせてサイズ決定)
If colLen = 0 Then colLen = 255 Else colLen = Fix(colLen 1.5) + 10
If colLen > 4000 Then
typeName = “VARCHAR(MAX)”
Else
typeName = “VARCHAR(” & colLen & “)”
End If
End Select
ddl = ddl & ” Col_” & Right(“000″ & (i + 1), 3) & ” ” & typeName
If i < m_columnCount - 1 Then
ddl = ddl & "," & vbCrLf
Else
ddl = ddl & vbCrLf
End If
Next
ddl = ddl & ");"
GenerateDDL = ddl
End Function
End Class
' メインルーチン起動
Call Main()
---
3. コードの極限的解説とアーキテクチャの根拠
上記コードがなぜ「プロダクションクオリティ」なのか、技術的理由を解説します。
① `ADODB.Stream` によるストリーミング処理
`Scripting.FileSystemObject (FSO)` はデフォルトでShift_JIS/Unicode(UTF-16)しか扱えず、ファイル全件をメモリに読み込みがちです。本設計では `ADODB.Stream` を用い、`ReadText(-2)`(1行読み込み)により指定サンプル行数(例: 2,000行)に到達した瞬間に読み込みを停止します。数GiBの超巨大ファイルであっても、メモリ消費量は数MiB以下で一定に保たれます。
② 正規表現オブジェクト(`RegExp`)のインスタンス再利用
VBScriptにおいて `New RegExp` は非常に重い処理です。ループの内部(セル単位の評価)で `New RegExp` を呼ぶコードは、一見綺麗に見えても膨大なガベージコレクションを発生させパフォーマンスを破壊します。
本コードでは `Class_Initialize` 内で正規表現オブジェクトを1度だけ生成・コンパイルし、全走査で使い回す設計(オプティマイズ)を行っています。
③ `IsDate` 関数だけに頼らない二重判定
VBScriptの `IsDate()` は非常に曖昧で危険な関数です。例えば `IsDate(“1.2”)` や `IsDate(“10-01”)` を環境のロケール設定によっては `True` と誤判定します。
そのため本エンジンでは、厳格なISO形式正規表現(`^\d{4}[-/]…`)に合致した場合にのみ `IsDate` を適用する二段構え(Short-circuit evaluation)を取っています。
—
4. 現場運用で差がつくボトルネック回避策
開発者が現場で遭遇する「落とし穴」とその回避策を提示します。
【現場の落とし穴】
1. サンプル件数(サンプリングサイズ)の設定誤り
先頭50行だけを走査した場合:51行目に日付「2026-03-31」が現れてDB投入時に型キャストエラーでバッチが死ぬ。
2. 大容量配列の頻繁な `ReDim Preserve`
1行パースするたびに `ReDim Preserve array(UBound + 1)` を行うと、メモリ再割り当てコストで $O(N^2)$ の遅延が発生する。
対策:バッファ型配列割り当てと最適サンプリング
1. 最適サンプル件数の算出ルール
全データ行数の 1%〜5%(最低1,000行、上限10,000行) の走査を推奨します。本スクリプトのように `maxRows` を外部設定にすることで、バッチ処理の重要度に応じたチューニングを可能にしています。
2. CSVパース時の配列拡張最適化
上記コードの `ParseCsvLine` 内では、`ReDim Preserve list(UBound(list) + 10)` のように 10要素単位でバッファをまとめて再確保 しています。ループごとの配列再割り当て回数を1/10に削減するプロの定石テクニックです。
—
5. まとめ
VBScriptのようなレガシーかつシンプルな環境であっても、データ構造の基本原理とオブジェクトのライフサイクル(生成・破棄コスト)を熟知して設計すれば、現代の最新言語に劣らない超高速かつ堅牢なデータ処理エンジンを構築することが可能です。
本稿で示した「型昇格マトリクス」および「非破壊型ストリーミングパース」の設計思想を、ぜひ貴社の現場の自動化ツール・ETLバッチ基盤に組み込んで活用してください。
