VBAでRFC4180 CSVを状態遷移で解析する

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) DQUOTE
non-escaped = *TEXTDATA
COMMA = %x2C
CR = %x0D
DQUOTE = %x22
LF = %x0A
CRLF = CR LF
TEXTDATA = %x20-21 / %x23-2B / %x2D-7E

重要なのは、フィールドが2種類あることです。

  • escaped: ダブルクオートで囲まれたフィールド

  • non-escaped: ダブルクオートで囲まれていないフィールド

ダブルクオートで囲まれたフィールドの中では、カンマや改行もデータとして扱えます。

状態遷移として読む

ABNFをそのまま実装すると、相互再帰的な構造になりがちです。

しかし、VBAで短く実装するなら、状態遷移へ変換した方が扱いやすくなります。

Posfie画像では、次の方針が示されていました。

  1. 相互再帰ではなく状態遷移に変更する

  2. 状態数をできるだけ減らす

  3. 2つ並んだダブルクオートを Mid で認識する

  4. 最後の行の末尾にCRLFがないケースを扱うため、状態1からEOTへ進める

  5. 先読みやトークン戻しをできるだけ使わない

  6. ファイル終端として番兵 EOT を追加する

  7. ヘッダとレコードを区別しない

定数定義

画像から判読できる範囲では、CSV解析モジュールの定数は次のように置かれていました。

Private Const CSV2_RUNTIME_ERROR = 2000
Private Const enc_utf8 = "UTF-8"
Private Const enc_utf16 = "UTF-16"
Private Const enc_sjis = "Shift_JIS"
Private Const LF = vbLf
Private Const CR = vbCr
Private Const CRLF = vbCrLf
Private Const DQUOTE = """"
Private Const DDQUOTE = DQUOTE & DQUOTE
Private Const COMMA = ","
Private EOT

EOT は実際の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 = rbuff
End Function

ADODB.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_ERROR
If 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 = 1
rp = 1
Do
    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
    Next
Loop While True

非エスケープ中は、DQUOTE、COMMA、EOT、CR/LF を分岐点にしています。

下請け処理

画像では、下請け処理として次のようなラベルが使われていました。

flash00:
    ' fieldの内容をバッファへ書き出し、fieldをクリアする。
    Call tk_csv_field_extend(a_rbuff(UBound(a_rbuff)), field)
    field = ""
    Return
skip00:
    ' CR、LFを読み飛ばす。
    Do
        GoSub read_next00
    Loop While ch = CR Or ch = LF
    GoSub unread00
    Return
unread00:
    ' 前回の読み込みをなかったことにする。
    rp = rp - 1
    Return
read_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
    Return
extend00:
    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