PR
私が使っているPCガジェット類

作業環境で実際に使っている・気になっている周辺機器などをまとめました

※ 一部のリンクは広告(アフィリエイト)を含みます

VBAのCSV読み込み ダブルクォーテーション内のカンマと改行

VBAでCSV読み込んでますか?

VBAでCSVを読み込むとき、もしダブルクォーテーションの囲みもなく、Split関数を使うだけで読み込めるきれいなCSVにしか出会ったことがないあなたは幸せ者です

一般的にはデータの中に区切り文字のカンマが紛れていたり、囲み文字になるはずのダブルクォーテーションが紛れていたり、文字コードがあれこれとか改行コードが含まれているなど考慮が必要なものです

今回はExcelとAccessの両VBAで同じコードを使いまわすために、CSVを1行ずつ読んで配列で返す関数を作りました
色々考慮してもデータ固有の問題もあるので100%完璧は難しくても、なるべく汎用的にちょっと味付けすれば対応できるように、が目標です

コード全文はそのまま貼れる形で載せていますのでデータにあわせて改修しながら使ってください

目次

CSVファイルの特徴のおさらい

CSVはComma Separated Values(カンマ・セパレーテッド・バリュー)の略で、その名の通りカンマで区切られた文字列のことです※タブで区切るTSV(タブ・セパレーテッド・バリュー)なんてのもあります

特徴として囲み文字(ダブルクォーテーション)の中にあるカンマは区切り文字としては取り扱わずに、ただの文字として取り扱います

具体例として囲み文字がない場合は

「ほげほげ,1,000,hogehoge」→ 「ほげほげ / 1 / 000 / hogehoge」

の4データになってしまいますが、各データに囲み文字を付けると

「"ほげほげ","1,000","hogehoge"」→ 「ほげほげ / 1,000 / hogehoge」

の通り3データに分割されます

似たような話で、データの中にダブルクォーテーションが含まれる場合、ダブルクォーテーションをもう一つ付けることでエスケープと言われる処理になり囲み文字としてのダブルクォーテーションにならずただの文字のダブルクォーテーションとして取り扱います

どちらかと言うと、ちゃんとエスケープしないと起きる予期せぬ解釈を避ける意味の方が強い

「1234,"hoge"hoge","ほげほげ"」 → 「1234 / hogehoge" / ほげほげ」

「1234,"hoge""hoge","ほげほげ"」 → 「1234 / hoge"hoge / ほげほげ」

Splitだとダブルクォーテーション内のカンマで列がずれる

次はSplit関数のおさらい

Split(expression, [ delimiter, [ limit, [ compare ]]])

https://learn.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/split-function

expressionで指定した文字列をdelimiterで区切って配列として返すのが一般的な使い方で、CSVならdelimiterをカンマにすればうまくいきそう

なんですが、Split は囲み文字の中かどうかを見ないので、カンマを見つけるたびに分割します

前述の例だとデータを4つに分割してしまうパターンが発生します

つまりコードリストなどよっぽどきれいなCSVじゃないと使い物になりません

ダブルクォーテーションを正しく扱うCSV読み込み関数 Excel・Access共通

結論から言うと、1行ずつ読みながら「いま囲み文字の中か」を判定して、囲みの中のカンマでは分割しない関数にすれば解決します
参照設定も特に要りません

Excelで開くときとほぼ同じ条件でデータを分割する仕様で、VBAの中で値を受け取って加工したいとき・Excelを介さずAccessで読みたいとき・先頭のゼロや日付の自動変換を通したくないときに有効です

使う関数は3つ

関数すること返すもの
CsvReadFileCSVファイルを1行ずつ読んで分割するrows(行)(列) で取り出せる配列(0始まり)、0件なら UBound が -1
CsvParseLine1レコード分の文字列を分割するString の配列(0始まり)
CsvTo2DArrayCsvReadFile の結果を2次元配列にするRange.Value に入れる形の2次元配列(足りない列は Empty)、0件なら Empty

基本的には CsvReadFile だけで足りるはずです
Excelのシートに貼るときは CsvTo2DArray を挟み、読み込みは自前で書いて分割だけ任せたいときは CsvParseLine を使います

戻り値で配列を返したいので、Sub ではなく Function にしました
ADODB.Stream は CreateObject で呼んでいて、Windows API の宣言(Declare)も使っていないので、ExcelでもAccessでも標準モジュールに貼るだけで動く作りにしてます

引数で文字コードと区切り文字を変えられる

引数省略したとき指定の例
filePath(省略できない)CSVファイルのパス
charset"auto"(自動判定)"Shift_JIS" / "UTF-8" / "Unicode"(UTF-16LE)
delimiter","TSVなら vbTab
quoteChar""""(" 1文字)"" を渡すと囲みを解釈しない
skipEmptyLinesTrue(空行を読み飛ばす)False なら空行を空文字1つのレコードにする

charset には、ADODB.Stream の Charset が受け付ける名前(そのPCのレジストリに登録されている文字セット名)なら何でも渡せます
ふだんは省略して自動判定に任せ、うまく読めないときだけ指定する使い方を想定してます

囲みの判定はフィールドの先頭だけで行う

仕組みはシンプルで、" を囲み文字の始まりとみなすのはフィールドの先頭にあるときだけです
値の途中にある "(5"3 など)は、ただの文字として読みます

囲みに入ったら、次の " までを InStr でまとめて取り込みます
そこで " が2つ続いていれば、おさらいで出てきたエスケープなので " 1文字に戻し、1つだけなら囲みの終わりです

1文字ずつ文字列をつなげていく書き方は、長い値で遅くなるので避けました

コード全文と貼り方

コードは、標準モジュールに全文をそのまま貼ってください
.bas ファイルの先頭に付く Attribute VB_Name の行は、最初から入れていません

VBEの「変数の宣言を強制する」をオンにしている方は、先頭に Option Explicit が2行並ぶはずです
片方を消しておいてください
Accessの Option Compare Database の行は、そのまま残して大丈夫です

コード全文(コピペ用)
'==============================================================================
' modCsv : CSV 読み込みモジュール (Excel / Access 共通・参照設定不要)
'
' 対応していること
'   ・囲み文字内の区切り文字           "東京,大阪"         → 東京,大阪
'   ・エスケープされた囲み文字         "say ""hi"""        → say "hi"
'   ・囲み文字内の改行 (複数行にまたがるフィールド。改行コードも元のまま保持)
'   ・改行コード CRLF / LF / CR を自動判定 (CRLF と LF の混在も可)
'   ・文字コード自動判定 (BOM 付き UTF-8 / UTF-16、BOM 無し UTF-8、それ以外は Shift_JIS)
'   ・区切り文字・囲み文字の変更 (TSV など。囲み文字 "" で囲み処理なし)
'
' 崩れたデータもエラーにせず、次のように読む (Excel / Python の csv とほぼ同じ挙動)
'   ・囲まれていないフィールド途中の "      5"3          → 5"3
'   ・閉じ囲みの直後に続く文字             "abc"def     → abcdef
'   ・ファイル末尾まで閉じられない囲み     → そこまでを 1 フィールドとして確定
'
' 比較はすべてバイナリ比較で書いているので、Access の Option Compare Database
' のモジュールに貼り付けても動作は変わらない
'
' 使用例
'   Dim rows As Variant, r As Long
'   rows = CsvReadFile("C:\data\sample.csv")
'   For r = 0 To UBound(rows)          ' 0 件なら UBound = -1 なのでループしない
'       Debug.Print rows(r)(0), rows(r)(1)
'   Next
'==============================================================================
Option Explicit

Private Const adTypeBinary As Long = 1
Private Const adTypeText As Long = 2
Private Const adReadLine As Long = -2
Private Const adCR As Long = 13
Private Const adLF As Long = 10

Private Const SAMPLE_BYTES As Long = 1048576     ' 文字コード・改行コード判定に使う先頭バイト数

'------------------------------------------------------------------------------
' CSV ファイルを 1 行ずつ読み込み、レコードの配列を返す
'
'   filePath       : CSV ファイルのパス
'   charset        : "auto"(既定) / "Shift_JIS" / "UTF-8" / "Unicode"(UTF-16LE) など
'                    ※ADODB.Stream の Charset に渡せる名前ならなんでも可
'   delimiter      : 区切り文字 (既定 ",")。TSV なら vbTab
'   quoteChar      : 囲み文字 (既定 """")。"" を渡すと囲みを解釈しない
'   skipEmptyLines : True(既定) なら空行を読み飛ばす。False なら空文字 1 個のレコードになる
'
'   戻り値 : 0 始まりの Variant 配列。各要素はそのレコードの String 配列 (0 始まり)
'            → rows(行)(列) で参照する。0 件のときは UBound = -1 の空配列
'            行ごとに列数が違ってもそのまま返す (揃えたいときは CsvTo2DArray)
'------------------------------------------------------------------------------
Public Function CsvReadFile(ByVal filePath As String, _
                            Optional ByVal charset As String = "auto", _
                            Optional ByVal delimiter As String = ",", _
                            Optional ByVal quoteChar As String = """", _
                            Optional ByVal skipEmptyLines As Boolean = True) As Variant
    Dim stm As Object
    Dim sample() As Byte
    Dim lineSep As Long
    Dim rows() As Variant, rowCount As Long
    Dim fields() As String, fieldCount As Long
    Dim fieldBuf As String, inQuotes As Boolean
    Dim lineText As String, lineNo As Long, recordStartLine As Long
    Dim hadCR As Boolean, pendingNewline As String
    Dim doParse As Boolean

    ValidateChars delimiter, quoteChar

    Set stm = CreateObject("ADODB.Stream")
    stm.Type = adTypeBinary
    stm.Open
    stm.LoadFromFile filePath

    If stm.Size = 0 Then
        stm.Close
        CsvReadFile = Array()
        Exit Function
    End If

    ' 先頭部分のバイト列から文字コードと改行コードを判定する
    If stm.Size < SAMPLE_BYTES Then sample = stm.Read(stm.Size) Else sample = stm.Read(SAMPLE_BYTES)
    If LCase$(charset) = "auto" Then charset = DetectCharset(sample)
    lineSep = DetectLineSeparator(sample)

    ' テキストモードに切り替えて先頭から 1 行ずつ読む
    stm.Position = 0
    stm.Type = adTypeText
    stm.Charset = charset
    stm.LineSeparator = lineSep     ' CRLF のファイルも LF で区切り、行末の CR は下で外す

    ReDim rows(0 To 255)
    ReDim fields(0 To 15)

    Do Until stm.EOS
        lineText = stm.ReadText(adReadLine)
        lineNo = lineNo + 1

        ' BOM (U+FEFF) が残っていたら除去
        If lineNo = 1 Then
            If CharCodeAt(lineText, 1) = &HFEFF& Then lineText = Mid$(lineText, 2)
        End If

        ' 行末の CR を外す (囲みの中の改行だった場合は pendingNewline で元に戻す)
        hadCR = (CharCodeAt(lineText, Len(lineText)) = 13)
        If hadCR Then lineText = Left$(lineText, Len(lineText) - 1)

        If inQuotes Then
            fieldBuf = fieldBuf & pendingNewline        ' 囲みの中の改行はデータとして残す
            doParse = True
        Else
            recordStartLine = lineNo
            doParse = Not (skipEmptyLines And Len(lineText) = 0)
        End If

        If doParse Then
            If ParseLine(lineText, delimiter, quoteChar, inQuotes, fieldBuf, fields, fieldCount) Then
                AddRecord rows, rowCount, fields, fieldCount
            ElseIf lineSep = adCR Then
                pendingNewline = vbCr                   ' 囲みの中で行が終わった → 次の行へ続く
            ElseIf hadCR Then
                pendingNewline = vbCrLf
            Else
                pendingNewline = vbLf
            End If
        End If
    Loop
    stm.Close

    ' 囲みが閉じないままファイルが終わった → 読めたところまでで確定する
    If inQuotes Then
        Debug.Print "CsvReadFile: " & recordStartLine & " 行目から始まる囲み文字が閉じられていません : " & filePath
        AddField fields, fieldCount, fieldBuf
        AddRecord rows, rowCount, fields, fieldCount
    End If

    If rowCount = 0 Then
        CsvReadFile = Array()
    Else
        ReDim Preserve rows(0 To rowCount - 1)
        CsvReadFile = rows
    End If
End Function

'------------------------------------------------------------------------------
' 1 レコード分の文字列を分割して String 配列 (0 始まり) で返す
'   Line Input などで自前で読んだ行を分割したいとき用
'   (囲みの中に改行が入っている文字列を渡してもよい)
'------------------------------------------------------------------------------
Public Function CsvParseLine(ByVal lineText As String, _
                             Optional ByVal delimiter As String = ",", _
                             Optional ByVal quoteChar As String = """") As String()
    Dim fields() As String, fieldCount As Long
    Dim fieldBuf As String, inQuotes As Boolean

    ValidateChars delimiter, quoteChar
    ReDim fields(0 To 15)

    If Not ParseLine(lineText, delimiter, quoteChar, inQuotes, fieldBuf, fields, fieldCount) Then
        AddField fields, fieldCount, fieldBuf           ' 囲みが閉じていない → 残りを最後のフィールドにする
    End If

    ReDim Preserve fields(0 To fieldCount - 1)
    CsvParseLine = fields
End Function

'------------------------------------------------------------------------------
' CsvReadFile の戻り値を 2 次元配列 (行, 列) に変換する (どちらも 0 始まり)
'   列数が足りない行は Empty で埋める。Excel の Range.Value にそのまま代入できる
'   0 件のときは Empty を返す
'------------------------------------------------------------------------------
Public Function CsvTo2DArray(ByRef rows As Variant) As Variant
    Dim result() As Variant
    Dim r As Long, c As Long, colCount As Long

    If UBound(rows) < 0 Then Exit Function

    For r = 0 To UBound(rows)
        If UBound(rows(r)) + 1 > colCount Then colCount = UBound(rows(r)) + 1
    Next

    ReDim result(0 To UBound(rows), 0 To colCount - 1)
    For r = 0 To UBound(rows)
        For c = 0 To UBound(rows(r))
            result(r, c) = rows(r)(c)
        Next
    Next
    CsvTo2DArray = result
End Function

'------------------------------------------------------------------------------
' 1 行分を解析してフィールドを fields に追加していく (内部処理)
'   inQuotes / fieldBuf / fields / fieldCount は行をまたいで状態を引き継ぐ
'   戻り値 : True  = レコードが完結した
'            False = 囲みの中で行が終わった (次の行に続く)
'------------------------------------------------------------------------------
Private Function ParseLine(ByRef lineText As String, ByVal delimiter As String, ByVal quoteChar As String, _
                           ByRef inQuotes As Boolean, ByRef fieldBuf As String, _
                           ByRef fields() As String, ByRef fieldCount As Long) As Boolean
    Dim pos As Long, hit As Long, lineLen As Long
    Dim quoteCode As Long
    Dim atFieldStart As Boolean

    If Len(quoteChar) = 0 Then quoteCode = -1 Else quoteCode = AscW(quoteChar) And &HFFFF&
    lineLen = Len(lineText)
    pos = 1
    atFieldStart = Not inQuotes

    Do While pos <= lineLen
        If inQuotes Then
            ' 囲みの中 : 次の囲み文字までをそのまま取り込む
            hit = InStr(pos, lineText, quoteChar, vbBinaryCompare)
            If hit = 0 Then
                fieldBuf = fieldBuf & Mid$(lineText, pos)
                Exit Function                               ' 囲みの中で行末 → 次の行へ続く
            End If
            fieldBuf = fieldBuf & Mid$(lineText, pos, hit - pos)

            If CharCodeAt(lineText, hit + 1) = quoteCode Then
                fieldBuf = fieldBuf & quoteChar             ' "" → " (エスケープ)
                pos = hit + 2
            Else
                inQuotes = False                            ' 閉じ囲み
                pos = hit + 1
            End If

        ElseIf atFieldStart And CharCodeAt(lineText, pos) = quoteCode Then
            ' フィールド先頭の囲み文字 → 囲み開始
            inQuotes = True
            atFieldStart = False
            pos = pos + 1

        Else
            ' 囲みの外 : 次の区切り文字までを取り込む
            ' (途中にある " や、閉じ囲みの後ろに続く文字はそのまま値として扱う)
            hit = InStr(pos, lineText, delimiter, vbBinaryCompare)
            If hit = 0 Then
                fieldBuf = fieldBuf & Mid$(lineText, pos)
                pos = lineLen + 1
            Else
                fieldBuf = fieldBuf & Mid$(lineText, pos, hit - pos)
                AddField fields, fieldCount, fieldBuf
                atFieldStart = True
                pos = hit + 1
            End If
        End If
    Loop

    If inQuotes Then Exit Function      ' 行末が """ や空行など、囲みの中で終わった → 次の行へ続く

    AddField fields, fieldCount, fieldBuf       ' 行末 = 最後のフィールドの終わり
    ParseLine = True
End Function

' fieldBuf を 1 フィールドとして確定する
Private Sub AddField(ByRef fields() As String, ByRef fieldCount As Long, ByRef fieldBuf As String)
    If fieldCount > UBound(fields) Then ReDim Preserve fields(0 To fieldCount * 2 + 1)
    fields(fieldCount) = fieldBuf
    fieldCount = fieldCount + 1
    fieldBuf = vbNullString
End Sub

' fields を 1 レコードとして確定する
Private Sub AddRecord(ByRef rows() As Variant, ByRef rowCount As Long, _
                      ByRef fields() As String, ByRef fieldCount As Long)
    ReDim Preserve fields(0 To fieldCount - 1)
    If rowCount > UBound(rows) Then ReDim Preserve rows(0 To rowCount * 2 - 1)
    rows(rowCount) = fields                     ' 配列のコピーが格納される
    rowCount = rowCount + 1
    fieldCount = 0
End Sub

' 指定位置の文字コード (0~65535)。範囲外なら -1
Private Function CharCodeAt(ByRef s As String, ByVal pos As Long) As Long
    If pos >= 1 And pos <= Len(s) Then
        CharCodeAt = AscW(Mid$(s, pos, 1)) And &HFFFF&
    Else
        CharCodeAt = -1
    End If
End Function

Private Sub ValidateChars(ByVal delimiter As String, ByVal quoteChar As String)
    If Len(delimiter) <> 1 Or Len(quoteChar) > 1 Then
        Err.Raise 5, "modCsv", "区切り文字は 1 文字、囲み文字は 1 文字か空文字で指定してください。"
    End If
    If StrComp(delimiter, quoteChar, vbBinaryCompare) = 0 Then
        Err.Raise 5, "modCsv", "区切り文字と囲み文字に同じ文字は指定できません。"
    End If
End Sub

'------------------------------------------------------------------------------
' 文字コードの推定
'   BOM があればそれに従う。無ければ UTF-8 として正しいバイト列かを検査し、
'   正しければ UTF-8、そうでなければ Shift_JIS とみなす
'   (ASCII だけのファイルは Shift_JIS 扱い。どちらで読んでも結果は同じ)
'------------------------------------------------------------------------------
Private Function DetectCharset(ByRef b() As Byte) As String
    Dim i As Long, k As Long, last As Long, need As Long
    Dim hasMultiByte As Boolean

    last = UBound(b)
    If last >= 2 Then
        If b(0) = &HEF And b(1) = &HBB And b(2) = &HBF Then DetectCharset = "UTF-8": Exit Function
    End If
    If last >= 1 Then
        If b(0) = &HFF And b(1) = &HFE Then DetectCharset = "Unicode": Exit Function
        If b(0) = &HFE And b(1) = &HFF Then DetectCharset = "unicodeFFFE": Exit Function
    End If

    i = 0
    Do While i <= last
        Select Case b(i)
            Case Is < &H80:     need = 0
            Case &HC2 To &HDF:  need = 1
            Case &HE0 To &HEF:  need = 2
            Case &HF0 To &HF4:  need = 3
            Case Else:          DetectCharset = "Shift_JIS": Exit Function
        End Select

        For k = 1 To need
            If i + k > last Then Exit Do            ' サンプルの末尾で文字が途切れた
            If b(i + k) < &H80 Or b(i + k) > &HBF Then DetectCharset = "Shift_JIS": Exit Function
        Next
        If need > 0 Then hasMultiByte = True
        i = i + need + 1
    Loop

    If hasMultiByte Then DetectCharset = "UTF-8" Else DetectCharset = "Shift_JIS"
End Function

'------------------------------------------------------------------------------
' 改行コードの推定
'   LF があれば LF 区切り (CRLF は行末の CR を外して対応)、CR しか無ければ CR 区切り
'------------------------------------------------------------------------------
Private Function DetectLineSeparator(ByRef b() As Byte) As Long
    Dim i As Long, hasCR As Boolean

    For i = 0 To UBound(b)
        If b(i) = 10 Then DetectLineSeparator = adLF: Exit Function
        If b(i) = 13 Then hasCR = True
    Next
    If hasCR Then DetectLineSeparator = adCR Else DetectLineSeparator = adLF
End Function

基本の読み方

CsvReadFile にファイルのパスを渡したら、あとは rows(r)(c) で取り出すだけです(行も列も0始まり)

Dim rows As Variant, r As Long
rows = CsvReadFile("C:\data\sample.csv")
For r = 0 To UBound(rows)          ' 0 件なら UBound = -1 なのでループしない
    Debug.Print rows(r)(0), rows(r)(1)
Next

ひとつ注意したいのは、行ごとに列の数が違ってもそのまま返すところ
上の例のように rows(r)(1) と決め打ちで読むと、列が足りない行で実行時エラー9(インデックスが有効範囲にありません)になります
列の数は行ごとに UBound(rows(r)) で確かめておくのがおすすめです

ループや配列の書き方をおさらいしたいときは、VBAのループ処理の記事もどうぞ

Excelのシートに貼る例

シートに貼るなら、CsvTo2DArray で2次元配列にしてから Range.Value にまとめて入れます

Dim data As Variant
data = CsvTo2DArray(CsvReadFile("C:\data\sample.csv"))
If Not IsEmpty(data) Then
    With Worksheets(1).Range("A1").Resize(UBound(data, 1) + 1, UBound(data, 2) + 1)
        .NumberFormat = "@"     ' 先に文字列の表示形式にしておく
        .Value = data
    End With
End If

CSVをExcelで開いたら「001」が「1」になっていた、なんて経験はありませんか?

Excelは、CSVを開いたりセルに貼り付けたりすると、先頭のゼロを消す・日付に変えるといった自動変換をかけます(Microsoft 365 と Excel 2024 は設定で止められます)

この関数は値をすべて文字列で返すので、配列に入った時点ではまだ変換前のままです
シートに書き戻すときは、上の例のように先に表示形式を文字列(「@」)にしておくのが一般的なやり方
ただ、Range.Value への代入でも同じ変換が起きるのかまでは確かめていません

もう1つの注意は、Excelのセル1つに入るのが32,767文字までなこと(Excelの仕様と制限)
後の検証で15万文字の値を読めたのはAccessでの話なので、そこまで長い値はシートには収まりません

Accessのテーブルに追加する例

Accessなら、読んだ配列を1行ずつテーブルに足していけます(テーブル名と列名は仮のものです)

Dim rows As Variant, r As Long, rs As DAO.Recordset
rows = CsvReadFile("C:\data\sample.csv", "UTF-8")
Set rs = CurrentDb.OpenRecordset("T_Import", dbOpenDynaset)
For r = 1 To UBound(rows)       ' 0 行目はヘッダとして読み飛ばす
    rs.AddNew
    rs!Code = rows(r)(0)
    rs!CustName = rows(r)(1)
    rs.Update
Next
rs.Close

この例では文字コードを "UTF-8" と指定して、0行目はヘッダーとして読み飛ばしています
DAO.Recordset を使うのはテーブルに書き込む側の話で、読み込む関数のほうは参照設定なしで動きます

検証した環境は、Windows 11 Pro と Microsoft 365 の Access(64bit)です
関数を動かした検証はAccessだけですが、Excel固有のオブジェクトは使っていないので、Excelでもそのまま動く想定です
上の3つの使い方の例(基本の読み方・Excel・Access)は、まだ動かしていません

Accessの実機で34ケースを検証した

コードは、要件を伝えて Claude Code に書いてもらいました
そのうえで、Accessを外から自動で動かして、関数の戻り値と Python 標準の csv モジュールで読んだ結果を1件ずつ突き合わせています

結果は34ケースすべて一致
内訳は、ファイルの読み込み26・1行の分割6・2次元配列への変換2です

  • 文字コードは Shift_JIS / UTF-8(BOMあり・なし)/ UTF-16LE を自動判定
  • 改行コードは CRLF / LF / CR だけ / CRLF と LF の混在
  • 囲みの中のカンマ・""・改行と、行末の """
  • 空行・TSV・空のファイル・15万文字の値
  • ①・髙(機種依存文字)・半角カナ
  • ソ・表・能(Shift_JIS で2バイト目が「\」になる字)

BOMつきのUTF-8で、先頭の値が "ID" のように囲まれていても正しく読めました
①や髙も化けなかったので、ADODB.Stream に "Shift_JIS" を指定すると、Windows の CP932 として読んでくれているようです

速さも測ってみました
10万行・9.3MBのCSV(UTF-8のBOMつき・100行に1回は複数行にまたがる値入り)を4回読んで、1.05〜1.25秒
手元のPCで、Option Compare Database を書いていないモジュールでの計測です

ジャベ雄

10万行でも1秒ちょっとなので、ふだん使いで待たされることはなさそうです

📚 VBAの独学に効く本PR
Excel マクロ&VBA やさしい教科書 Office 2024/2021 Microsoft 365対応

Excel マクロ&VBA やさしい教科書 Office 2024/2021 Microsoft 365対応

古川順平

てっとり早く確実にマスターできる Excel VBAの教科書 改訂新版

てっとり早く確実にマスターできる Excel VBAの教科書 改訂新版

大村あつし

Excel マクロ/VBA 今日から使える業務の自動化入門

Excel マクロ/VBA 今日から使える業務の自動化入門

富士通ラーニングメディア

私のおすすめからランダムで3冊を表示しています

1行ずつ読むとセル内改行で壊れる Line Input # を使わない理由

1行ずつ読むときにいちばん厄介なのが、囲みの中の改行(Excelでいうセル内改行)です
1行ずつ分割すると、その値が2つのレコードに割れてしまいます

たとえば住所の欄に改行が入っていると、CSVには「1,"東京都」と「千代田区",3」の2行で保存されます

見た目は2行でも、囲みの中の改行なので、データとしては3列・1レコードです

囲みの中で行が終わったら状態を次の行へ持ち越す

そこでこの関数は、行が囲みの中で終わったら、囲みの中にいることと作りかけの値を覚えたまま次の行を読みます
次の行の頭で元の改行を値に戻してから、続きをつなげる流れです

値に残る改行は、CRLF でも LF でもファイルにあったまま

全行を1つの文字列にまとめてから " の数の偶奇で判定するやり方でも、囲みの中の改行は扱えます
今回は「1行ずつ読む」作りにしたかったので、状態を持ち越す形を選びました

ジャベ雄

囲みの中の改行は、1行ずつ読む作りと一番ぶつかるところです

Line Input # はLFだけのファイルとUTF-8に弱かった

VBAでテキストを1行ずつ読むなら、まず思い浮かぶのは Line Input # ではないでしょうか?

実はこの関数では使っていません
試してみたところ、結果が崩れる場面が3つありました

読ませたファイルLine Input # の結果
LFだけで改行した3行のファイル全体が1行で返った
UTF-8で書いた「名前,値」「蜷榊燕,蛟、」に化けた
CRLFの3レコード(1つにセル内改行あり)4行に分かれた

Microsoftの公式ページを見ても、Line Input # の行の終わりとして挙がっているのは CR と CRLF の2つだけで、LFには触れていません
文字コードの記述もありません

そこで、ADODB.Stream の ReadText で1行ずつ読み、行の区切りをLFにして、CRLFは行末に残ったCRを外して扱うことにしました
LineSeparator の既定は CRLF なので、LFは自分で指定してます
先頭1MBにLFが無く、CRだけで改行しているファイルならCRで区切ります

ちなみに、CreateObject で呼んでいるぶん、adLF などのADOの定数はコードの先頭に自前で書いてあります

文字コードはBOM・UTF-8の検査・Shift_JISの順に判定

charset を省略すると、ファイルの先頭1MBを見て文字コードを決めます

  1. BOMがあれば、それに従う(UTF-8 / UTF-16)
  2. BOMが無ければ、UTF-8として正しいバイト列かを調べる
  3. 正しいUTF-8で、半角の英数字・記号以外の文字が入っていればUTF-8、それ以外はShift_JISとみなす

気をつけたいのは、判定に使うのが先頭の1MBだけなこと
先頭1MBが半角の英数字・記号だけのUTF-8は、Shift_JISと判定されます
ファイル全体がそうならどちらで読んでも結果は同じですが、1MBより後ろに日本語が出てくるCSVなら charset:="UTF-8" を指定してください

BOMの無いUTF-16も自動では見分けないので、UTF-16LEなら charset:="Unicode" を指定します

コードを書かずにCSVの文字化けだけ直したいなら、自作の文字コード変換ツールも使えます

Accessで使うならOption Compare Databaseに注意

実は、Option Compare Database のあるモジュールだと、InStr(行, ",") が全角のカンマ「,」にも当たりました
この関数を検証している途中、Accessの日本語環境で気づいたことです

標準モジュールを追加すると最初から書かれている

Accessで標準モジュールを追加すると、先頭に Option Compare Database が最初から入っています(手元の Microsoft 365 版で確認)
Accessで書いている方なら、見慣れた1行だと思います

AccessのVBEで追加した標準モジュールModule1、先頭にOption Compare DatabaseとOption Explicitの2行が最初から入っている

InStr は比較の方法(4つ目の引数)を省略すると、モジュールの Option Compare の設定に従います(InStr関数の公式ページ)
そして Option Compare Database は、データベースの並べ替え順序で比べるAccess専用の書き方です(Option Compare ステートメントの公式ページ)

全角と半角が同じ文字として扱われた

Option Compare Database のモジュールに式を書いて試すと、こうなりました

試した式結果
"," = ","True
"""" = """True
InStr(1, "a,b", ",")2(全角のカンマに当たった)
InStr(1, "a,b", ",", vbBinaryCompare)0

ほかにも、大文字と小文字・半角カナと全角カナ・ひらがなとカタカナが同じ扱い(True)でした
公式ページに書かれているのは並べ替え順序で比べるところまでで、全角と半角の扱いには触れていないので、あくまで私の環境での実測です

Option Compare Database のモジュールで比較の方法を省略すると、InStr は全角のカンマも区切りとして見つけてしまいます
データに全角のカンマが混ざっていたら…と考えると、ちょっと怖くないですか?

この関数は比較をすべてバイナリで書いた

そこでこの関数は、InStr に vbBinaryCompare を付けて、1文字の判定も AscW で文字コードを比べています
なので Option Compare Database を消さずに貼っても、結果は変わりません

実際に、モジュールの先頭にこの1行を足して同じテスト(当時の33ケース)を流したときも全件一致でした

自分で InStr を書くときも、vbBinaryCompare を付けておけば全角との取り違えを避けられます
ただし付けるときは、1つ目の引数(開始位置)を省略できないので気をつけてください

Excelでも Option Compare Text を書いたモジュールなら同じことが起きるかもしれませんが、こちらは試していません

ジャベ雄

全角カンマで割れても、エラーは出ないので気づきにくいです

崩れたCSVはExcelで開いたときとほぼ同じに読む

囲み文字の閉じ忘れのような崩れたCSVが来ても、この関数はエラーで止めずに読みます
崩れた入力の読み方は RFC 4180 にも定めが無いので、同じCSVをExcelで開いて比べ、値の分け方をそちらに合わせました

入力読んだ結果(Excelで開いた結果と同じ)
5"3(囲まれていない値の途中に ")5"3
"abc"def(閉じた囲みの後ろに文字)abcdef
"a" ,b(閉じた囲みの後ろにスペース)a (後ろにスペースが残る)と b
"q",z(スペースのあとに囲み) "q"(囲みとして扱わない)と z
"a", "b,c"(区切りのあとにスペース)a と "b と c"(囲みとして扱わず、カンマで割れる)
a,"bc のあと、閉じないまま2行続く2列目に残りが全部入る

試した崩れ方の中で、Excelと違ったのは1つだけです
閉じ忘れの " がファイルの最後まで続いたとき、Excelはセルの末尾に最後の改行を残しますが、この関数は最後の改行を落とします(ここはわざとです)

それと、値は文字列のまま返すので、Excelのように先頭のゼロや日付を勝手に変換することもありません
分け方は同じでも、ここはExcelで開いたときとの違い

閉じ忘れがあると、それ以降が1つの値に飲み込まれます(Excelも同じ)
そのときはイミディエイトウィンドウに「CsvReadFile: ○ 行目から始まる囲み文字が閉じられていません」と、始まった行を出します

エラーにも戻り値の印にもならないので、気づけるのはイミディエイトウィンドウだけです
取り込んだ件数や列の数も、一度見ておいてください

崩れていない普通のCSV(Shift_JISで、囲みの中のカンマ・""・改行あり)なら、分かれ方はExcelで開いた結果と同じでした

この関数でできないこと

  • 全角のカンマ「,」と全角の「"」は、区切りや囲みとして扱わない(普通の文字として読む)
  • 値はすべて文字列で返す(数値や日付への変換は呼び出す側で)
  • 区切り文字は1文字だけ、囲み文字は1文字か空文字
  • Windows専用(ADODB.Stream を使うため、Mac版のExcelでは動かない見込み)
  • 32bit版のOfficeは未確認(Declare を使っていないので影響は無いはず)
  • 数百MB級のファイルはメモリに注意(ファイル全体を ADODB.Stream に読み込み、結果も配列に持つため)
ジャベ雄

100%完璧ではなくても、崩れたときの読み方が決まっていれば使い回せます

CSVではなくJSONで受け取るデータなら、VBA-JSONで読み込む方法も参考にしてみてください

📚 VBAの独学に効く本PR
Excel マクロ&VBA やさしい教科書 Office 2024/2021 Microsoft 365対応

Excel マクロ&VBA やさしい教科書 Office 2024/2021 Microsoft 365対応

古川順平

てっとり早く確実にマスターできる Excel VBAの教科書 改訂新版

てっとり早く確実にマスターできる Excel VBAの教科書 改訂新版

大村あつし

Excel マクロ/VBA 今日から使える業務の自動化入門

Excel マクロ/VBA 今日から使える業務の自動化入門

富士通ラーニングメディア

私のおすすめからランダムで3冊を表示しています


WORK

ツール作成のご依頼を受けています

クラウドワークス・ココナラで受注200件以上
会社員の副業なので、対応は平日夜と土日です

頼めることと料金の目安を見る →

ココナラが初めての方は、紹介コード 78E62K で1,000円分のポイントがもらえます

はじめての方の会員登録(PR):クラウドワークス / ココナラ

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

VBAとPythonを中心にユーザー側でできるITを自己学習しているので備忘録半分、学習履歴を残して同じ道を辿る人の参考になればとブログを始めました

副業でスクレイピングツール作成を中心にできることを色々やっていますのでご相談いただけるとありがたいです!


クラウドワークスのページへ


ココナラのページへ

コメント

コメントする

目次