VBAの Line Input # でCSVを1行ずつ読むと、問題が2種類起きることがあります。

  • ループが1回で終わり、ファイル全体が1行として読み込まれる
  • 行数が想定より多くなり、途中から列がずれる

どちらも「改行の扱い」が原因ですが、仕組みが違うため直し方も違います。この記事では、まずファイルの改行がどうなっているかを確認し、その結果に合った読み込み方を選ぶ手順を説明します。コードはWindows版Excelを前提にしています。

2つの症状の原因

症状 主な原因
ファイル全体が1行として読まれる 改行コードがLF(Chr(10))だけになっている
行数が多すぎる、途中から列がずれる 引用符で囲まれた項目の中に改行(セル内改行)があり、そこで行が分かれている

Line Input # は、CR(Chr(13))またはCRLF(Chr(13) + Chr(10))が出てくるまでを1行として読みます。LFだけの改行は、行の区切りとして扱われません。Microsoftの Line Input # ステートメントのリファレンスで確認できます。

ほかのシステムやWebサービスから出力されたCSVは、改行がLFだけのことがあります。この場合、ファイル全体が1行として読み込まれます。読み込んだ文字列の中にはLFが残っているので、セルに書き込むとセル内で改行されて見えることもあります。

手順1:改行コードの種類と数を調べる

エディタで開けば、おおまかな改行コードが分かります。Windows 11のメモ帳ならステータスバーに「Windows (CRLF)」「Unix (LF)」などと表示されます。Visual Studio Codeなら右下に「CRLF」「LF」と表示されます。

ただし、エディタの表示は「主な改行コード」しか示しません。1つのファイルにCRLFとLFが混ざっているかどうかまでは分からないので、VBAで種類ごとの数を数えます。

Sub CountLineBreaks()
    Dim path As String
    Dim f As Integer
    Dim b() As Byte
    Dim i As Long, last As Long
    Dim nCrLf As Long, nLf As Long, nCr As Long

    path = "C:\work\data.csv"   ' 調べるファイル

    f = FreeFile
    Open path For Binary Access Read As #f
    If LOF(f) = 0 Then
        Close #f
        MsgBox "ファイルが空です"
        Exit Sub
    End If
    ReDim b(0 To LOF(f) - 1)
    Get #f, , b
    Close #f

    last = UBound(b)
    i = 0
    Do While i <= last
        If b(i) = 13 Then
            If i < last Then
                If b(i + 1) = 10 Then
                    nCrLf = nCrLf + 1
                    i = i + 1
                Else
                    nCr = nCr + 1
                End If
            Else
                nCr = nCr + 1
            End If
        ElseIf b(i) = 10 Then
            nLf = nLf + 1
        End If
        i = i + 1
    Loop

    MsgBox "CRLF: " & nCrLf & vbCrLf & _
           "LFのみ: " & nLf & vbCrLf & _
           "CRのみ: " & nCr
End Sub

このコードはバイト単位で数えています。Shift_JISでもUTF-8でも、0x0D(CR)と0x0A(LF)は改行以外の文字の一部としては出てこないので、どちらの文字コードのファイルにも使えます。

結果は、データの件数(見出し行を含めた行数)と比べて読みます。

  • CRLFの数が行数とほぼ同じで、LFのみが0:改行はCRLFでそろっている
  • LFのみの数が行数とほぼ同じで、CRLFが0:LF改行のファイル。全体が1行になる原因はこれ
  • CRLFが行数とほぼ同じで、LFのみも少しある:行の区切りはCRLFで、項目の中にLFのセル内改行がある可能性が高い
  • CRLFもLFも行数より多い:行の区切りと同じ記号が項目の中でも使われている可能性がある

手順2:セル内改行があるか確かめる

セル内改行があるかどうかは、引用符(")の数で判断できます。

CSVでは、改行を含む項目はダブルクォーテーションで囲まれます。改行で区切った1行の中に " が奇数個しかなければ、その行は「引用符の途中で改行されている」ことになり、次の行と合わせて1件のデータです。

エディタで見る場合は、行の先頭が想定と違う行(たとえば、ID列の数字で始まるはずなのに文章の続きで始まっている行)を探します。その直前の行の末尾が、引用符で囲まれた項目の途中になっていないか確認してください。

手順3:結果に合わせて読み込み方を選ぶ

ファイルの状態 読み込み方
CRLFのみ、セル内改行なし Line Input # のままでよい
CRLFで行を区切り、セル内改行はLF Line Input # で行は分かれない。項目の中にLFが残るので、使い道に合うか確認する
LFのみ、セル内改行なし 全体を読み込んでLFで分ける(下記A)
行の区切りと同じ改行が項目の中にもある 引用符を考慮して1件ずつ区切る(下記B)

A. LF改行のファイルを読む

Shift_JISのファイルは、バイトのまま読み込んで文字列に変換してから、LFで分けます。StrConv の vbUnicode はシステムの文字コード(日本語版Windowsなら通常Shift_JIS)で変換します。

Function ReadAllAnsi(ByVal path As String) As String
    Dim f As Integer, b() As Byte
    f = FreeFile
    Open path For Binary Access Read As #f
    If LOF(f) > 0 Then
        ReDim b(0 To LOF(f) - 1)
        Get #f, , b
        ReadAllAnsi = StrConv(b, vbUnicode)
    End If
    Close #f
End Function

Sub ReadLfCsv()
    Dim buf As String, lines() As String, i As Long
    buf = ReadAllAnsi("C:\work\data.csv")
    buf = Replace(buf, vbCrLf, vbLf)   ' CRLFが混ざっていてもLFにそろえる
    lines = Split(buf, vbLf)
    For i = 0 To UBound(lines)
        If Len(lines(i)) > 0 Then
            Debug.Print lines(i)   ' ここで1行ずつ処理する
        End If
    Next i
End Sub

UTF-8のファイルは、ADODB.Streamで行の区切りをLFに指定すると1行ずつ読めます。

Sub ReadUtf8LfCsv()
    Dim st As Object, rec As String
    Set st = CreateObject("ADODB.Stream")
    st.Type = 2              ' テキスト
    st.Charset = "UTF-8"
    st.LineSeparator = 10    ' LF
    st.Open
    st.LoadFromFile "C:\work\data.csv"
    Do Until st.EOS
        rec = st.ReadText(-2)    ' 1行読む
        If Right$(rec, 1) = vbCr Then rec = Left$(rec, Len(rec) - 1)
        If Len(rec) > 0 Then
            Debug.Print rec      ' ここで1行ずつ処理する
        End If
    Loop
    st.Close
End Sub

CRLFが混ざっていると行末にCRが残るので、取り除く処理を入れています。BOM付きのファイルを読んだ場合は、1行目の先頭に見えない文字が残っていないかも確認してください。

B. セル内改行があるファイルを読む

改行で単純に分ける方法では、セル内改行のところでもデータが分かれてしまいます。ファイル全体を文字列として読み込み、引用符の外にある改行だけで区切ります。

Function SplitRecords(ByVal s As String) As Collection
    Dim recs As New Collection
    Dim i As Long, startPos As Long, ch As String
    Dim inQuote As Boolean

    startPos = 1
    i = 1
    Do While i <= Len(s)
        ch = Mid$(s, i, 1)
        If ch = """" Then
            inQuote = Not inQuote
            i = i + 1
        ElseIf (ch = vbCr Or ch = vbLf) And Not inQuote Then
            recs.Add Mid$(s, startPos, i - startPos)
            If ch = vbCr And Mid$(s, i + 1, 1) = vbLf Then i = i + 1
            i = i + 1
            startPos = i
        Else
            i = i + 1
        End If
    Loop
    If startPos <= Len(s) Then recs.Add Mid$(s, startPos)

    Set SplitRecords = recs
End Function

文字列は、Shift_JISなら上の ReadAllAnsi で、UTF-8ならADODB.Streamの ReadText(-1)(全体を読む)で読み込んでから渡します。項目の中の ""(引用符の書き方)は2回切り替わって元に戻るので、引用符の外か中かの判定は崩れません。CRLF、LF、CRのどの改行で区切られたファイルでも同じように処理できます。

戻り値の1件ごとに、セル内改行がそのまま残っています。Excelのセル内改行はLFなので、セルに書き込むなら Replace(rec, vbCrLf, vbLf) でそろえておくと表示が安定します。

なお、この関数はデータを「1件ずつ」に分けるだけです。1件の中をカンマで項目に分けるときも、引用符の中のカンマを考慮する必要があります。これは別の問題として扱ってください。また、1文字ずつ調べるので、数十MBを超えるようなファイルでは時間がかかります。

読み込んだ後に確認すること

  • 読み込んだ件数が、出力元のシステムが示す件数やエディタで見た件数と合っている
  • すべての行の項目数が、見出し行の項目数と同じ
  • 最後の行が抜けていない(末尾に改行がないファイルでも読めている)
  • 1行目の先頭の列名に、BOMなどの見えない文字が残っていない
  • セル内改行が項目の中に残っていて、書き込み先でその扱いでよい

件数と項目数が合わない行が見つかったら、その行と前後の行をエディタで開き、手順2のとおり引用符の数を確認してください。

おすすめの記事