CSV形式テキストデータの読み込み(カンマ数不定版)

では、カンマの数に依存しない方法を紹介します。
結局は、行単位で読み込んで、カンマ位置で分解する方法です。 読み込み自体は、この章の最初の「テキストデータの読み込み」と同じ方法なので新しい説明はありませんが、1つの「手段」として考えてみて下さい。
しかし、「なぜ、そんなに難しいコードになるのか」などと質問がありました。データの「精度」についての苦労を知らない方だとは思いますが、単にSplit関数を使ってカンマで分離させる方法ではダブルクォーテーションで囲われた文字列中にカンマが出現した場合、おかしな結果になってしまうのです。
「セル内での改行」や「セル内にカンマ」がある状態で読み込めるサンプルを用意しました。   このページの方法では先に改行までを1(1)のデータとして扱っているので、 1(1)のデータ中でさらにセル内改行がある場合には対応はできませんでした。
Excelでは「セル内での改行」や「セル内にカンマ」がある状態でCSV形式ファイルに保存させたものを正しく開けるわけですから、 そのまま開く方法ではなくマクロで読み込む方法でも同様の行列判定ができるようにしたいものです。



これを実現させるものを「ダウンロード」「カンマ数不定のCSVファイル読み込み」に用意しました。
3重ループ構成になるなど構造が難しいので細かい解説はしていませんが、ソースコードは提示しているのでコメントなどから学習はできると思います。 主機能部分はクラス化させており、大半のケースでは内容の改変なくインポートさせてご利用いただけるようにしています。



FileSystemObject(FSO)によるサンプルです。
このページでは、Open, Inputといった古いステートメントではなく、FileSystemObject(FSO)で記述します。 さらに、カンマ分解部分は他に応用が利くようにFunctionプロシージャに分けることにします。
※この記述方法では「Microsoft Scripting Runtime」の参照設定が必要です。

'***************************************************************************************************
'   CSVテキストファイル読み込みサンプル(FSO版、カンマ数不定対応)    Module1(Module)
'
'   作成者:井上治  URL:https://www.ne.jp/asahi/excel/inoue/ [Excelでお仕事!]
'***************************************************************************************************
'   [参照設定]
'   ・Microsoft Scripting Runtime
'***************************************************************************************************
'変更日付 Rev  変更履歴内容------------------------------------------------------------------------>
'03/07/25(1.00)新規作成
'03/12/04(1.01)初回修正
'20/02/26(1.10)*.xlsm化、他
'***************************************************************************************************
Option Explicit
'===================================================================================================
Private Const g_cnsTitle As String = "CSVテキストファイル読み込み"
Private Const g_cnsFilter As String = "CSV形式ファイル (*.csv),*.csv,全てのファイル(*.*),*.*"
Private Const g_cnsSC As String = "'"                           ' シングルクォーテーション
Private Const g_cnsDC As String = """"                          ' ダブルクォーテーョン
Private Const g_cnsCOM As String = ","                          ' カンマ
' ※Tab区切りの場合は上の「g_cnsCOM」を変更して下さい。
'Private Const g_cnsCOM As String = vbTab                        ' Tab(項目区切り文字)

'***************************************************************************************************
'   ■■■ ワークシート側からの呼び出し処理 ■■■
'***************************************************************************************************
'* 処理名 :READ_CsvFile3
'* 機能  :CSVテキストファイル読み込みサンプル
'---------------------------------------------------------------------------------------------------
'* 返り値 :(なし)
'* 引数  :(なし)
'---------------------------------------------------------------------------------------------------
'* 作成日 :2003年07月25日
'* 作成者 :井上 治
'* 更新日 :2020年02月26日
'* 更新者 :井上 治
'* 機能説明:FSO版、カンマ数不定対応
'* 注意事項:サンプルなのでエラー処理は行なっていません
'***************************************************************************************************
Sub READ_CsvFile3()
    '-----------------------------------------------------------------------------------------------
    Dim objFso As FileSystemObject                                  ' FileSystemObject
    Dim objTs As TextStream                                         ' TextStream
    Dim lngRow As Long                                              ' 収容するセルの行
    Dim lngRec As Long                                              ' レコード件数カウンタ
    Dim lngCol As Long                                              ' 行毎の最終カラム
    Dim strFileName As String                                       ' OPENするファイル名(フルパス)
    Dim vntFileName As Variant                                      ' ファイル名受取り用
    Dim vntRec As Variant                                           ' 項目配列受け取り用
    '-----------------------------------------------------------------
    ' ①「ファイルを開く」のダイアログでファイル名の指定を受ける
    Application.StatusBar = "読み込むファイル名を指定して下さい。"
    vntFileName = Application.GetOpenFilename(FileFilter:=g_cnsFilter, Title:=g_cnsTitle)
    ' キャンセルされた場合はFalseが返るので以降の処理は行なわない
    If VarType(vntFileName) = vbBoolean Then Exit Sub
    strFileName = vntFileName
    '-----------------------------------------------------------------
    ' ②FileSystemObject
    Set objFso = New FileSystemObject
    ' 指定ファイルをOPEN(入力モード)
    Set objTs = objFso.OpenTextFile(Filename:=strFileName, IOMode:=ForReading)
    Set objFso = Nothing
    ' 先頭行の設定(2行目から掻き出すので1とする)
    lngRow = 1
    '-----------------------------------------------------------------
    ' ③ファイルのEOFまで繰り返す
    Do Until objTs.AtEndOfStream
        ' レコード件数カウンタの加算
        lngRec = lngRec + 1
        Application.StatusBar = "読み込み中です....(" & lngRec & "レコード目)"
        ' 改行単位に読み込み、レコードを配列化する処理を呼び出す
        vntRec = FP_MakeArrayFromCSV(objTs.ReadLine)
        ' 行を加算
        lngRow = lngRow + 1
        ' 空行対策
        If IsArray(vntRec) Then
            ' 項目配列から最終カラムを算出
            lngCol = UBound(vntRec) + 1
            ' 配列を横のセル範囲(要素数を合わせる)に転記
            Range(Cells(lngRow, 1), Cells(lngRow, lngCol)).Value = vntRec
        End If
    Loop
    '-----------------------------------------------------------------
    ' ④処理終了
    objTs.Close
    Set objTs = Nothing
    Application.StatusBar = False
    ' 終了の表示
    MsgBox "ファイル読み込みが完了しました。" & vbCr & _
        "レコード件数=" & lngRec & "件", vbInformation, g_cnsTitle
End Sub

'***************************************************************************************************
'   ■■■ サブ処理(Private) ■■■
'***************************************************************************************************
'* 処理名 :FP_MakeArrayFromCSV
'* 機能  :CSVレコードを配列に変換
'---------------------------------------------------------------------------------------------------
'* 返り値 :カンマで分解した配列(Variant)
'* 引数  :Arg1 = 1レコード内容(String)
'---------------------------------------------------------------------------------------------------
'* 作成日 :2003年07月25日
'* 作成者 :井上 治
'* 更新日 :2020年02月26日
'* 更新者 :井上 治
'* 機能説明:
'* 注意事項:
'***************************************************************************************************
Private Function FP_MakeArrayFromCSV(ByVal strRec As String) As Variant
    '-----------------------------------------------------------------------------------------------
    Dim lngIx As Long                                               ' 配列INDEX
    Dim lngPos As Long                                              ' 文字位置
    Dim lngPosMax As Long                                           ' 最終文字位置
    Dim lngPos1 As Long                                             ' 項目単位先頭文字位置
    Dim vntRec As Variant                                           ' 配列WORK
    Dim strChar As String                                           ' 1文字WORK
    Dim strChar1 As String                                          ' 1文字WORK
    '-----------------------------------------------------------------
    ' 変数を初期化
    lngIx = -1
    ReDim vntRec(0)
    lngPos = 1
    lngPosMax = Len(strRec)
    '-----------------------------------------------------------------
    ' レコード全体のループ
    Do While lngPos <= lngPosMax
        ' 項目当りの先頭文字の判定(セパレータか)
        strChar1 = Mid(strRec, lngPos, 1)
        Select Case strChar1
            Case g_cnsSC                            ' シングルクォーテーション
                lngPos = lngPos + 1
            Case g_cnsDC                            ' ダブルクォーテーョン
                lngPos = lngPos + 1
            Case Else                               ' なし(カンマで判定)
                strChar1 = g_cnsCOM
        End Select
        ' 項目当りの先頭位置
        lngPos1 = lngPos
        ' 項目当りの終了位置判定
        Do While lngPos <= lngPosMax
            strChar = Mid(strRec, lngPos, 1)
            ' 終了文字判定
            If strChar = strChar1 Then
                ' カンマでないか
                If strChar1 <> g_cnsCOM Then
                    ' レコード探索終了か
                    If lngPos >= lngPosMax Then
                        Exit Do
                    ElseIf ((Mid(strRec, lngPos + 1, 1) = strChar1) And (strChar1 = g_cnsDC)) Then
                        ' ダブルクォーテーョン連記は1文字として扱う(1文字分進める)
                        lngPos = lngPos + 1
                    ElseIf Mid(strRec, lngPos + 1, 1) = g_cnsCOM Then
                        ' カンマなら内ループを抜ける
                        Exit Do
                    End If
                Else
                    ' カンマなら内ループを抜ける
                    Exit Do
                End If
            End If
            ' 次の文字位置へ
            lngPos = lngPos + 1
        Loop
        ' 1項目の配列登録(配列の要素数を増やして登録)
        lngIx = lngIx + 1
        ReDim Preserve vntRec(lngIx)
        ' 項目内容があるか
        If lngPos > lngPos1 Then
            ' ※本処理では特にデータ型の判断は行なわない
            vntRec(lngIx) = Mid$(strRec, lngPos1, lngPos - lngPos1)
        Else
            vntRec(lngIx) = Empty
        End If
        ' 次項目の先頭に移動
        If strChar <> g_cnsCOM Then
            lngPos = lngPos + 2
        Else
            lngPos = lngPos + 1
        End If
    Loop
    '-----------------------------------------------------------------
    ' 有効レコードか
    If strRec <> "" Then
        ' レコード右端がカンマの場合は配列要素を1つ増やす
        If Mid(strRec, lngPosMax, 1) = g_cnsCOM Then
            lngIx = lngIx + 1
            ReDim Preserve vntRec(lngIx)
            vntRec(lngIx) = Empty
        End If
    End If
    ' 戻り値にセット
    If lngIx >= 0 Then
        FP_MakeArrayFromCSV = vntRec
    Else
        FP_MakeArrayFromCSV = ""
    End If
End Function

'----------------------------------------<< End of Source >>----------------------------------------
(ここをクリックすると、このサンプルがダウンロードできます)
このように、メインの「READ_CsvFile3」はスッキリとしたものになります。 I/Oの概要は「テキストデータの読み込み」の後半の説明と変わりません。



一方、カンマごとの分解については、ダブルクォーテーションで囲われた間のカンマは項目の分解点にならないように配慮したもので、先の記述よりやや高度な方法になっていますが、 プロシージャを分けてあるので理解はしやすいのではないでしょうか。
但し、このページの方法は項目の途中に改行があるデータには対応していません。 項目の途中に改行があるデータの対応については「カンマ数不定のCSVファイル読み込み(ダウンロード)の方をご覧下さい。

ものはついでですから、このコードの先頭にある、

Private Const g_cnsCOM As String = ","                          ' カンマ
これを、

Private Const g_cnsCOM As String = vbTab                        ' タブ
に換えるだけでタブ区切りのテキストファイルの読み込み処理ができます。
何でこんな面倒なことをするのか....   CSV形式ファイルだったら、行で読み込んでセパレータ文字をSplit関数で分解するだけで良いではないか、とメールで指摘してきた人がいました。
たぶん「きれいな」CSV形式ファイルしか扱ったことがないのでしょう。文字列境界、日付境界の文字の有無や、もっとも問題なのは1項目の文字列の間にカンマが含まれる場合があるなど、 CSV形式ファイルというあいまいな形式にはほんとうに悩まされました。