PR
私が使っているPCガジェット類
作業環境で実際に使っている・気になっている周辺機器などをまとめました
※ 一部のリンクは広告(アフィリエイト)を含みます
作業環境で実際に使っている・気になっている周辺機器などをまとめました
※ 一部のリンクは広告(アフィリエイト)を含みます
VBAでCSV読み込んでますか?
VBAでCSVを読み込むとき、もしダブルクォーテーションの囲みもなく、Split関数を使うだけで読み込めるきれいなCSVにしか出会ったことがないあなたは幸せ者です
一般的にはデータの中に区切り文字のカンマが紛れていたり、囲み文字になるはずのダブルクォーテーションが紛れていたり、文字コードがあれこれとか改行コードが含まれているなど考慮が必要なものです
今回はExcelとAccessの両VBAで同じコードを使いまわすために、CSVを1行ずつ読んで配列で返す関数を作りました
色々考慮してもデータ固有の問題もあるので100%完璧は難しくても、なるべく汎用的にちょっと味付けすれば対応できるように、が目標です
コード全文はそのまま貼れる形で載せていますのでデータにあわせて改修しながら使ってください
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(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じゃないと使い物になりません
結論から言うと、1行ずつ読みながら「いま囲み文字の中か」を判定して、囲みの中のカンマでは分割しない関数にすれば解決します
参照設定も特に要りません
Excelで開くときとほぼ同じ条件でデータを分割する仕様で、VBAの中で値を受け取って加工したいとき・Excelを介さずAccessで読みたいとき・先頭のゼロや日付の自動変換を通したくないときに有効です
| 関数 | すること | 返すもの |
|---|---|---|
| CsvReadFile | CSVファイルを1行ずつ読んで分割する | rows(行)(列) で取り出せる配列(0始まり)、0件なら UBound が -1 |
| CsvParseLine | 1レコード分の文字列を分割する | String の配列(0始まり) |
| CsvTo2DArray | CsvReadFile の結果を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文字) | "" を渡すと囲みを解釈しない |
| skipEmptyLines | True(空行を読み飛ばす) | 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 FunctionCsvReadFile にファイルのパスを渡したら、あとは 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のループ処理の記事もどうぞ
シートに貼るなら、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 IfCSVをExcelで開いたら「001」が「1」になっていた、なんて経験はありませんか?
Excelは、CSVを開いたりセルに貼り付けたりすると、先頭のゼロを消す・日付に変えるといった自動変換をかけます(Microsoft 365 と Excel 2024 は設定で止められます)
この関数は値をすべて文字列で返すので、配列に入った時点ではまだ変換前のままです
シートに書き戻すときは、上の例のように先に表示形式を文字列(「@」)にしておくのが一般的なやり方
ただ、Range.Value への代入でも同じ変換が起きるのかまでは確かめていません
もう1つの注意は、Excelのセル1つに入るのが32,767文字までなこと(Excelの仕様と制限)
後の検証で15万文字の値を読めたのは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)は、まだ動かしていません
コードは、要件を伝えて Claude Code に書いてもらいました
そのうえで、Accessを外から自動で動かして、関数の戻り値と Python 標準の csv モジュールで読んだ結果を1件ずつ突き合わせています
結果は34ケースすべて一致
内訳は、ファイルの読み込み26・1行の分割6・2次元配列への変換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秒ちょっとなので、ふだん使いで待たされることはなさそうです
1行ずつ読むときにいちばん厄介なのが、囲みの中の改行(Excelでいうセル内改行)です
1行ずつ分割すると、その値が2つのレコードに割れてしまいます
たとえば住所の欄に改行が入っていると、CSVには「1,"東京都」と「千代田区",3」の2行で保存されます
見た目は2行でも、囲みの中の改行なので、データとしては3列・1レコードです
そこでこの関数は、行が囲みの中で終わったら、囲みの中にいることと作りかけの値を覚えたまま次の行を読みます
次の行の頭で元の改行を値に戻してから、続きをつなげる流れです
値に残る改行は、CRLF でも LF でもファイルにあったまま
全行を1つの文字列にまとめてから " の数の偶奇で判定するやり方でも、囲みの中の改行は扱えます
今回は「1行ずつ読む」作りにしたかったので、状態を持ち越す形を選びました



囲みの中の改行は、1行ずつ読む作りと一番ぶつかるところです
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の定数はコードの先頭に自前で書いてあります
charset を省略すると、ファイルの先頭1MBを見て文字コードを決めます
気をつけたいのは、判定に使うのが先頭の1MBだけなこと
先頭1MBが半角の英数字・記号だけのUTF-8は、Shift_JISと判定されます
ファイル全体がそうならどちらで読んでも結果は同じですが、1MBより後ろに日本語が出てくるCSVなら charset:="UTF-8" を指定してください
BOMの無いUTF-16も自動では見分けないので、UTF-16LEなら charset:="Unicode" を指定します
コードを書かずにCSVの文字化けだけ直したいなら、自作の文字コード変換ツールも使えます
実は、Option Compare Database のあるモジュールだと、InStr(行, ",") が全角のカンマ「,」にも当たりました
この関数を検証している途中、Accessの日本語環境で気づいたことです
Accessで標準モジュールを追加すると、先頭に Option Compare Database が最初から入っています(手元の Microsoft 365 版で確認)
Accessで書いている方なら、見慣れた1行だと思います


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が来ても、この関数はエラーで止めずに読みます
崩れた入力の読み方は 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で開いた結果と同じでした



100%完璧ではなくても、崩れたときの読み方が決まっていれば使い回せます
CSVではなくJSONで受け取るデータなら、VBA-JSONで読み込む方法も参考にしてみてください
WORK
ツール作成のご依頼を受けています
クラウドワークス・ココナラで受注200件以上
会社員の副業なので、対応は平日夜と土日です
ココナラが初めての方は、紹介コード 78E62K で1,000円分のポイントがもらえます
VBAとPythonを中心にユーザー側でできるITを自己学習しているので備忘録半分、学習履歴を残して同じ道を辿る人の参考になればとブログを始めました
副業でスクレイピングツール作成を中心にできることを色々やっていますのでご相談いただけるとありがたいです!
クラウドワークスのページへ
ココナラのページへ
コメント