VBAでRFC4180 CSVを状態遷移で解析する
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
CSV解析は、単にカンマで分割すればよい処理ではありません。
RFC4180のCSVでは、ダブルクオートで囲まれたフィールド、フィールド内のカンマ、改行、2つ並んだダブルクオート、最後のCRLFがないケースを扱う必要があります。
この記事では、RFC4180のABNFをそのまま相互再帰で実装するのではなく、VBAで扱いやすい状態遷移として整理する方法を説明します。
本記事の対象とゴール
VBAでCSVを正しく解析したい人
Split(line, ",") の限界を理解したい人
RFC4180、ABNF、レールロード図、状態遷移をVBA実装へつなげたい人
先に結論
RFC4180 CSVは、状態遷移で読むとVBAに落とし込みやすくなります。
ポイントは次の通りです。
相互再帰ではなく状態遷移へ変換する
状態数をできるだけ減らす
非エスケープ中とエスケープ中を分ける
DQUOTE、COMMA、CRLF、EOT を明示する
ファイル読み込みとCSV解析を分離する
Mid で2文字を読める性質を使い、先読みや戻しを減らす
<!-- LWP_REWRITE_EXPANSION_記事109 -->
1. CSVは単なるカンマ区切りではない
CSVを読む処理でよくある失敗は、文字列を単純にカンマで分割してしまうことです。RFC4180に近いCSVでは、引用符で囲まれたフィールド、引用符内のカンマ、改行、二重引用符のエスケープを扱う必要があります。
つまり、CSV解析は文字列分割ではなく、状態遷移の問題です。今いる位置が通常状態なのか、引用符内なのか、引用符を閉じた直後なのかで、同じ文字の意味が変わります。
2. 状態を名前で持つ
VBAでCSVパーサーを書くなら、状態を数値のまま扱わず、Enumなどで名前を付けると読みやすくなります。通常状態、引用符内、引用符後、レコード終端のように、状態の名前をコードに出します。
状態名があると、分岐の理由を説明できます。カンマを見たとき、通常状態ならフィールド区切り、引用符内なら文字、引用符後ならエラーまたは区切り確認、というように整理できます。
3. テストケースを先に用意する
CSV解析は、うまく動いているように見えても境界条件で壊れます。空フィールド、末尾カンマ、引用符内改行、引用符のエスケープ、不正な引用符をテストケースとして持つべきです。
実務では、外部システムから来るCSVの癖もあります。仕様に従うだけでなく、実際の入力でどこまで許容するかを決めることが重要です。
RFC4180の基本
RFC4180では、CSVはおおむね次のような構造で定義されています。
file = [header CRLF] record *(CRLF record) [CRLF]header = name *(COMMA name)record = field *(COMMA field)field = (escaped / non-escaped)escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTEnon-escaped = *TEXTDATACOMMA = %x2CCR = %x0DDQUOTE = %x22LF = %x0ACRLF = CR LFTEXTDATA = %x20-21 / %x23-2B / %x2D-7E重要なのは、フィールドが2種類あることです。
escaped: ダブルクオートで囲まれたフィールド
non-escaped: ダブルクオートで囲まれていないフィールド
ダブルクオートで囲まれたフィールドの中では、カンマや改行もデータとして扱えます。
状態遷移として読む
ABNFをそのまま実装すると、相互再帰的な構造になりがちです。
しかし、VBAで短く実装するなら、状態遷移へ変換した方が扱いやすくなります。
Posfie画像では、次の方針が示されていました。
相互再帰ではなく状態遷移に変更する
状態数をできるだけ減らす
2つ並んだダブルクオートを Mid で認識する
最後の行の末尾にCRLFがないケースを扱うため、状態1からEOTへ進める
先読みやトークン戻しをできるだけ使わない
ファイル終端として番兵 EOT を追加する
ヘッダとレコードを区別しない
定数定義
画像から判読できる範囲では、CSV解析モジュールの定数は次のように置かれていました。
Private Const CSV2_RUNTIME_ERROR = 2000Private Const enc_utf8 = "UTF-8"Private Const enc_utf16 = "UTF-16"Private Const enc_sjis = "Shift_JIS"Private Const LF = vbLfPrivate Const CR = vbCrPrivate Const CRLF = vbCrLfPrivate Const DQUOTE = """"Private Const DDQUOTE = DQUOTE & DQUOTEPrivate Const COMMA = ","Private EOTEOT は実際のCSV文字ではなく、読み込み終端を表す番兵です。
ファイル読み込みと解析を分離する
画像のコードでは、テスト関数でUTF-8とShift_JISのCSVを読み、同じ解析ドライバーへ渡していました。
Public Sub tk_csv_driver2_test() Dim r1, r2 Dim f1, f2 Dim bp bp = ThisWorkbook.Path f1 = tk_path_join(bp, "copyfile", "personal_utf8.csv") r1 = tk_csv_driver2(f1, , "UTF-8") f2 = tk_path_join(bp, "copyfile", "personal_sjis.csv") r2 = tk_csv_driver2(f2, , "Shift_JIS")End Subポイントは、文字コード対応とCSV解析を分けていることです。
Public Function tk_csv_driver2(a_path, Optional a_fsep, Optional a_encoding) Dim csv_data Dim rbuff If IsMissing(a_encoding) Then a_encoding = enc_sjis Select Case a_encoding Case enc_utf8, enc_utf16, enc_sjis Case Else Err.Raise CSV2_RUNTIME_ERROR End Select With CreateObject("ADODB.Stream") .Charset = a_encoding .Open .LoadFromFile a_path csv_data = .ReadText(-1) End With rbuff = Array() Call tk_csv_jagarr_make(rbuff, csv_data, a_fsep) tk_csv_driver2 = rbuffEnd FunctionADODB.Stream でファイル全体を文字列へ読み、解析ルーチンへ渡します。
解析ルーチンの入口
解析ルーチンは、呼び出し元で初期化した戻り配列を受け取る形です。
Public Sub tk_csv_jagarr_make(a_rbuff, a_csv_data, Optional a_fsep) ' CSVをファイルを解析してJag配列を作成する。 ' a_rbuff: 解析結果戻り配列 ' 1次元目は行、2次元目は各項目 ' a_csv_data: CSVデータ文字列 ' a_fsep: 区切り記号End Subここでは、2次元配列ではなくJag配列として扱う構成です。
メイン制御の骨格
画像から判読できる範囲で、メイン制御は次のような骨格でした。
If Not IsArray(a_rbuff) Then Err.Raise CSV2_RUNTIME_ERRORIf IsMissing(a_fsep) Then COMMA = ","EOT = Chr(3)Dim rp ' 読み込み位置Dim status ' 1: non-escape認識中 2: escaped認識中Dim ch ' 先読み文字列Dim nread ' 読み込み文字数Dim field ' 読込中の1項目status = 1rp = 1Do GoSub read_next00 If ch = EOT Then Exit Do GoSub unread00 GoSub extend00 For i = 1 To 1 Step 0 DoEvents GoSub read_next00 Select Case status Case 1 Select Case ch Case DQUOTE If field <> "" Then Err.Raise CSV2_RUNTIME_ERROR GoSub unread00 status = 2 Case COMMA GoSub flash00 Case EOT GoSub flash00 Exit For Case CR, LF GoSub flash00 GoSub skip00 Exit For Case Else field = field & ch End Select Case 2 ' escaped認識中の処理 Case Else Err.Raise CSV2_RUNTIME_ERROR End Select NextLoop While True非エスケープ中は、DQUOTE、COMMA、EOT、CR/LF を分岐点にしています。
下請け処理
画像では、下請け処理として次のようなラベルが使われていました。
flash00: ' fieldの内容をバッファへ書き出し、fieldをクリアする。 Call tk_csv_field_extend(a_rbuff(UBound(a_rbuff)), field) field = "" Returnskip00: ' CR、LFを読み飛ばす。 Do GoSub read_next00 Loop While ch = CR Or ch = LF GoSub unread00 Returnunread00: ' 前回の読み込みをなかったことにする。 rp = rp - 1 Returnread_next00: If rp > Len(a_csv_data) Then ch = EOT Else ch = Mid(a_csv_data, rp, nread) End If rp = rp + 1 nread = 1 Returnextend00: ReDim Preserve a_rbuff(LBound(a_rbuff) To UBound(a_rbuff) + 1) a_rbuff(UBound(a_rbuff)) = Array() Return全文ではなく、判読できた範囲の骨格です。
GoSubを局所処理として使う
このコードでは、GoSub が多用されています。
GoSub は古い構文ですが、同じプロシージャ内で小さな下請け処理をまとめる用途では有効です。
CSV解析のように、読み進める、戻す、吐き出す、行を増やす、といった局所操作が何度も出る場合、GoSub によってメイン制御を短くできます。
ただし、処理が大きくなるなら、SubやFunctionへ分けた方が安全です。
まとめ
RFC4180 CSVをVBAで解析するには、単純なカンマ分割では足りません。
ダブルクオート、フィールド内改行、2つ並んだダブルクオート、最後のCRLF省略を扱う必要があります。
ABNFをそのまま実装するより、状態遷移へ落とすとVBAで書きやすくなります。
ファイル読み込みとCSV解析を分離し、解析側では DQUOTE、COMMA、CR/LF、EOT を明示して状態を進める。
この考え方は、CSVだけでなく、他のテキスト解析にも応用できます。
出典メモ- 元Posfie URL: https://posfie.com/@hoehoe1234/p/7Z57duj
元Posfieタイトル: 2022-03-26 VBA CSVの解析Part3
投稿者: excelvba_diary, hoehoe1234
投稿数: 24
画像確認: 画像10点を取得し、記事化時に内容を確認
取得・確認日: 2026-06-11
画像・他者投稿として確認した主な投稿:
@hoehoe1234 2022-03-24 17:47:11: https://x.com/hoehoe1234/status/1506915517030080518
@excelvba_diary 2022-03-24 18:08:58: https://x.com/excelvba_diary/status/1506920999333535744
@hoehoe1234 2022-03-24 18:13:48: https://x.com/hoehoe1234/status/1506922218110226438
@excelvba_diary 2022-03-24 18:58:40: https://x.com/excelvba_diary/status/1506933510325485568
@hoehoe1234 2022-03-24 19:55:36: https://x.com/hoehoe1234/status/1506947835690127365
@hoehoe1234 2022-03-25 15:21:47: https://x.com/hoehoe1234/status/1507241314009911298
@hoehoe1234 2022-03-25 15:23:15: https://x.com/hoehoe1234/status/1507241683549061125
@hoehoe1234 2022-03-25 15:25:14: https://x.com/hoehoe1234/status/1507242184403308545
@hoehoe1234 2022-03-25 15:26:50: https://x.com/hoehoe1234/status/1507242588545650689
@hoehoe1234 2022-03-25 15:29:13: https://x.com/hoehoe1234/status/1507243187437699074
@hoehoe1234 2022-03-25 15:30:08: https://x.com/hoehoe1234/status/1507243417231052804
参考: RFC 4180 Common Format and MIME Type for CSV Files, https://www.rfc-editor.org/rfc/rfc4180
