【実務・中級編】【CSVデータ内カンマ・改行のエスケープ復元】他システムから出力された複雑なCSVフィールドを壊さずに正しいデータ配列表に展開するパース手法 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【CSVデータ内カンマ・改行のエスケープ復元】他システムから出力された複雑なCSVフィールドを壊さずに正しいデータ配列表に展開するパース手法

開発現場で幾度となくエンジニアの心を折ってきた「CSVパース問題」。
「なんだ、`Split(line, “,”)` すれば一瞬じゃないか」――そう安易に実装した結果、他システムからきれいにエスケープされたはずのダブルクォーテーション(`”`)付きフィールドや、セル内改行が含まれた途端、行数が狂い、データが盛大にズレる。夜間バッチが異常終了し、翌朝に担当者から冷や汗モノの連絡を受ける……。あなたも似たような修羅場をくぐってきたのではないだろうか。

特にVBScript(WSH)環境において、レガシーなシステム連携やRPAのデータ前処理を行う際、この「カンマ・改行包摂型CSV」のハンドリングは避けて通れない。
今回は、単なる文字列置換の小細工を捨て、文字単位の「ステートマシン(状態機械)」によって完全かつ高速にパースし、二次元配列へと昇華させる極限のVBScriptコードを授けよう。

—

1. なぜ `Split` 関数ではCSVを正しく処理できないのか

RFC 4180に代表されるCSVの仕様では、フィールド内に「区切り文字(カンマ)」や「改行文字」、あるいは「ダブルクォーテーション自体」が含まれる場合、フィールド全体をダブルクォーテーションで囲むことが規定されている。

“ID”,”Name”,”Description”,”Price”
101,”山田 太郎”,”「商品A」を購入。納期は来週、別途連絡あり。”,1500
102,”鈴木 花子”,”備考:
特になし”,2000

このデータを単に `Split(text, “,”)` で切断してしまうとどうなるか。
1. `101` の説明文にある「納期は来週、別途連絡あり。」のカンマで無理やり切断される。
2. `102` の説明文にある改行コードでレコードの境界が狂い、以降の全データがパースエラーを起こす。

VBScriptには .NET Framework のような高機能なCSVパーサークラス(`TextFieldParser` など)標準ではない。だからこそ、一文字ずつ走査し、現在の「状態(ダブルクォーテーションの中にいるか否か)」を厳密に追跡するパーサを自前で実装する必要がある。

—

2. 堅牢なCSVパーサの設計思想(ステートマシン)

今回構築するエンジンは、以下の状態を厳密に管理する。

  • 通常状態 (InNormal): フィールドの区切りや改行を監視するモード。
  • クォート状態 (InQuote): ダブルクォーテーション内部にいるモード。この中ではカンマも改行も「ただの文字」としてバッファに蓄積する。
  • エスケープ状態 (EscapedQuote): クォート内で連続するダブルクォーテーション(`””`)をエスケープ文字として処理するモード。

このステートを `Boolean` フラグや数値のステータス変数で制御し、1文字ずつ `Mid` 関数で舐めていく。VBScriptのパフォーマンス特性を考慮し、文字列結合のオーバーヘッドを最小限に抑えるために `Scripting.Dictionary` や `ArrayList`(または動的配列 `ReDim Preserve` のスマートな制御)を駆使するのがプロの技量だ。

—

3. プロダクションコード:完全版CSVパース・二次元配列化スクリプト

以下のコードは、ファイル全体を読み込み、カンマ・改行エスケープを完全に考慮した上で、VBScriptの二次元配列(`Variant(Row, Col)`)としてメモリ上に展開する実用モジュールである。

‘ ==============================================================================
‘ ファイル名: ParseCsvAdvanced.vbs
‘ 概要 : カンマ・セル内改行・エスケープを含む複雑なCSVを完全パースし、
‘ 二次元配列として返すプロダクションコード
‘ ==============================================================================
Option Explicit

Dim fso, targetPath, rawData, parsedArray
targetPath = “C:\Data\sample_complex.csv”

Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FileExists(targetPath) Then
WScript.Echo “エラー: 対象ファイルが存在しません -> ” & targetPath
WScript.Quit 1
End If

‘ 1. ファイル全体をバイナリ/テキストとして一括読み込み(ストリーム使用推奨だが簡潔のためTextStream)
Dim ts
Set ts = fso.OpenTextFile(targetPath, 1, False, 0) ‘ 0 = ASCII/Shift-JIS前提 (環境に合わせて変更)
rawData = ts.ReadAll
ts.Close
Set ts = Nothing

‘ 2. CSVパース実行
parsedArray = ParseCSV(rawData)

‘ 3. 结果確認(デバッグ用:取得した二次元配列のサイズと一部を表示)
If IsArray(parsedArray) Then
Dim rowCount, colCount
rowCount = UBound(parsedArray, 1)
colCount = UBound(parsedArray, 2)

WScript.Echo “パース成功! 総行数: ” & (rowCount + 1) & “, 総列数: ” & (colCount + 1)

‘ 先頭行(ヘッダー)と1行目のデータを表示
Dim c
For c = 0 To colCount
WScript.Echo “Col[” & c & “]: ” & parsedArray(0, c) & ” | 1stData: ” & parsedArray(1, c)
Next
Else
WScript.Echo “パースに失敗しました。”
End If

Set fso = Nothing
WScript.Quit 0

‘ ==============================================================================
‘ 関数名 : ParseCSV
‘ 引数 : strData (String) – CSVの生文字列
‘ 戻り値 : Variant (二次元配列 0-indexed: Array(Row, Col))
‘ 処理説明 : 文字単位のステートマシンにより、クォート内のカンマ/改行を保護する
‘ ==============================================================================
Function ParseCSV(ByVal strData)
Dim i, length, char
Dim inQuote, currentField, currentRow, rows
Dim fieldList, rowList

‘ 内部データ保持用(動的配列の代わりに入れ子状の動的配列、またはコレクションを使用)
‘ VBScriptで二次元配列を動的にリサイズするのは重いため、一時的にVBScriptの動的配列を構築

Set rowList = CreateObject(“System.Collections.ArrayList”)
Set fieldList = CreateObject(“System.Collections.ArrayList”)

inQuote = False
currentField = “”
length = Len(strData)

i = 1
Do While i <= length char = Mid(strData, i, 1) If inQuote Then ' --- クォート内部の処理 --- If char = """" Then ' 次の文字がさらにダブルクォーテーションか確認(エスケープ表現 "" の判定) If i < length And Mid(strData, i + 1, 1) = """" Then currentField = currentField & """" i = i + 1 ' 2文字分スキップ Else ' クォート終了 inQuote = False End If Else ' クォート内の文字(カンマや改行もそのまま蓄積) currentField = currentField & char End If Else ' --- 通常状態の処理 --- If char = """" Then inQuote = True ElseIf char = "," Then ' フィールドの区切り fieldList.Add currentField currentField = "" ElseIf char = vbCr Then ' 改行コードの処理 (CRLF または単独CR) If i < length And Mid(strData, i + 1, 1) = vbLf Then i = i + 1 ' LFもスキップしてペアとして処理 End If fieldList.Add currentField currentField = "" ' 行の確定 rowList.Add fieldList.ToArray() Set fieldList = CreateObject("System.Collections.ArrayList") ElseIf char = vbLf Then ' 単独LFの処理 fieldList.Add currentField currentField = "" ' 行の確定 rowList.Add fieldList.ToArray() Set fieldList = CreateObject("System.Collections.ArrayList") Else ' 通常文字 currentField = currentField & char End If End If i = i + 1 Loop ' 最終行のバッファリング処理(ファイル末尾に改行がない場合への対策) If currentField <> “” Or fieldList.Count > 0 Then
fieldList.Add currentField
rowList.Add fieldList.ToArray()
End If

‘ — ArrayList から VBScript の二次元配列へ変換 —
Dim totalRows, maxCols, r, c, tempRowArray
totalRows = rowList.Count

If totalRows = 0 Then
ParseCSV = Empty
Exit Function
End If

‘ 最大列数を算出して不整合を防ぐ
maxCols = 0
For r = 0 To totalRows – 1
If rowList(r).Length > maxCols Then
maxCols = rowList(r).Length
End If
Next

‘ 二次元配列の初期化 (0-indexed: Rows – 1, Cols – 1)
Dim resultMatrix()
ReDim resultMatrix(totalRows – 1, maxCols – 1)

For r = 0 To totalRows – 1
tempRowArray = rowList(r)
For c = 0 to UBound(tempRowArray)
resultMatrix(r, c) = tempRowArray(c)
Next
‘ 足りない列は空文字でパディング
For c = UBound(tempRowArray) + 1 To maxCols – 1
resultMatrix(r, c) = “”
Next
Next

ParseCSV = resultMatrix
End Function

—

4. 実務・プロダクション環境における重要注意点

このスクリプトを実際の業務システムやRPA、ファイル・データベース連携に組み込むにあたり、プロのエンジニアとして押さえておくべき「罠と対策」を共有する。

① .NETコンポーネント(`System.Collections.ArrayList`)の活用

VBScript標準の `ReDim Preserve` は、多次元配列の拡張ができない上に、ループ内で頻繁に行うとO(N^2)のメモリコピーが発生し、巨大なCSV(数万行以上)を処理した瞬間にスクリプトがフリーズする。
上記コードでは、`.NET Framework` の `System.Collections.ArrayList` をCOM経由でインスタンス化し、動的なリスト構造を高速に構築している。これにより、メモリ効率と実行速度の課題を完璧にクリアしている。
(※モダンなWindows環境では標準で利用可能)

② 文字コードの闇(Shift-JIS vs UTF-8)

他システム(特に基幹系や古いレガシーDB)からの出力は、未だに Shift-JIS (CP932) であることが多い。
ファイルストリームを扱う際、文字コードの指定を誤ると、日本語の2バイト目(いわゆる「ダメ文字」:表、構、ソなど)に含まれる `0x5C`(バックスラッシュ/円マーク)などが原因でパースが狂うことがある。
今回のスクリプトでは `ReadAll` で一括読み込みを行っているが、文字化けが起きる環境では `ADODB.Stream` オブジェクトを使用して明示的に “UTF-8” や “Shift-JIS” を指定してテキスト化する前処理を挟むのが鉄則だ。

③ 不正なCSV構造への耐性 (Jagged Row対策)

世の中のCSVは、必ずしもすべての行の列数が一致しているとは限らない(データの欠損など)。
今回の実装では、全行の中で「最も多い列数」を基準に二次元配列のサイズを動的に決定し、足りない列は自動的に空文字(`””`)でパディングする仕様にしている。これにより、後続の `For` ループでの「インデックスが範囲外です」エラーを完全にかわすことができる。

—

総括

VBScriptはレガシーな言語と嘲笑されることもあるが、OSの標準機能だけで動作し、インフラを選ばないという圧倒的な機動力を持っている。
しかし、そのシンプルさゆえに「甘い実装」は即座にバグという形で牙をむく。

今回解説したステートマシンによるパース手法は、言語の垣根を超えてあらゆるテキスト処理の根幹をなす美しいアルゴリズムだ。ぜひあなたの業務自動化スクリプトに組み込み、二度と「CSVの文字化け・ズレ」で夜中に叩き起こされない堅牢なシステムを構築してほしい。

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