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のとおり引用符の数を確認してください。
