銀河鉄道

【VBA】CSVに配列の中身を書き出す|ADODB.Stream

サムネイル
[VBA]CSVに書き出す
配列をCSVに
書き出したい

Excelに書き出すのとは違うの?

文字コードが必要になるよ

ADODB.Streamで書き出す

文字コードを制御したいときには ADODB.Stream を使う

Public Sub SaveUtf8WithBom(byval filePath As String, byval content As String)

    ' UTF-8で書き込む(まずはBOMなしで)
    Dim stTxt As Object: Set stTxt = CreateObject("ADODB.Stream")
    With stTxt
        .Type = 2  ' テキストモード
        .Charset = "utf-8"
        .Open
        .WriteText content
        .Position = 0
    End With

    ' バイナリファイルを作成
    Dim stBin As Object: Set stBin = CreateObject("ADODB.Stream")
    With stBin
        .Type = 1
        .Open

        ' バイナリファイルにBOMを追加する
        Dim bom(2) As Byte
        bom(0) = &HEF
        bom(1) = &HBB
        bom(2) = &HBF

        .Write bom

        ' テキストファイルをバイナリファイルにコピーする
        stTxt.CopyTo stBin

        ' バイナリファイルをファイルに保存する
        .SaveToFile filePath, 2

        .Close
    End With

    ' テキストファイルを閉じる
    stTxt.Close
End Sub

文字コードを制御する?

日本語の文字化けを防ぐこと

BOM付きのUTF-8を使う

BOMについてはこちら

VBAでBOM付きにする手順

  • テキストストリームにUTF-8で本文を書き込む
  • バイナリストリームを開き、先頭にBOM(EF BB BF)を書き込む
  • テキストストリームの内容をコピーして結合する
  • ファイルに保存する

そもそもADODB.Streamって何?

「データの入れ物」のこと

ADODB.Streamを開く

フタ付きの入れ物だから、まず開く必要がある

Dim st As Object
Set st = CreateObject("ADODB.Stream")

st.Type = 2          ' テキストモード
st.Charset = "utf-8" ' 文字コード設定

st.Open              ' ←ここで「ストリームを開く」
st.WriteText "Hello"

.Open すると、ストリームに次のことができる

  • 文字列を書き込む
  • ファイルから読み込む
  • 位置を移動する

使い方の例

'列:列記号で指定
private const COL_LASTROW_BASE = "A" ' 最終行の基準とする列記号

'ヘッダー行、データ行の開始行: 行番号で指定
private const ROW_HEADER = 1
private const ROW_DATA_START = 2

Public Sub ExportSheetToCSV()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets("対象シート")

    ' 出力列を定義
    dim fields() as string
    ReDim fields(0 To 4)
    fields(0) = "A"
    fields(1) = "C"
    fields(2) = "E"
    fields(3) = "F"
    fields(4) = "G"

    ' 最終行、最終列を取得
    Dim lastRow As Long, lastCol As Long
    With ws
        lastRow = .Range(COL_LASTROW_BASE & .Rows.Count).End(xlUp).Row
        lastCol = .Cells(ROW_HEADER, .Columns.Count).End(xlToLeft).Column
    End With

    ' CSV行データを格納する配列
    Dim csvLines() As String
    Dim rowData() As String
    Dim line As String

    Dim i As Long
    ' ヘッダー行のデータを格納する配列
    ReDim rowData(LBound(fields) To UBound(fields))
    For i = LBound(fields) To UBound(fields)
        rowData(i) = CSVEscape(ws.Cells(ROW_HEADER, fields(i)).Value)
    Next

    ' CSV行データを格納する配列
    ReDim csvLines(0 To lastRow - ROW_DATA_START + 1)

    ' 1行目にヘッダー行を出力
    line = Join(rowData, ",")
    csvLines(0) = line

    ' 行データの処理(各行を1つずつstringにする)
    Dim r As Long
    For r = ROW_DATA_START To lastRow

        For i = LBound(fields) To UBound(fields)
            rowData(i) = CSVEscape(ws.Cells(r, fields(i)).Value)
        Next i

        ' CSV1行にまとめる
        line = Join(rowData, ",")

        ' CSV配列に追加
        csvLines(r -ROW_DATA_START + 1) = line

    Next r

    ' CSV文字列
    Dim csvContent As String
    csvContent = Join(csvLines, vbCrLf)

    Dim csvFilePath As String
    csvFilePath = "ここに保存先パスを指定する"

    ' CSVファイルに保存
    SaveUtf8WithBom csvFilePath, csvContent
End Sub

'=== ダブルクォートを必要な時だけ付ける ===
Private Function CsvEscape(byval s As String) As String
    Dim needQuote As Boolean
    needQuote = (InStr(s, ",") > 0) Or (InStr(s, """") > 0) Or (InStr(s, vbCr) > 0) Or (InStr(s, vbLf) > 0)

    If needQuote Then
        CsvEscape = """" & Replace(s, """", """""") & """"
    Else
        CsvEscape = s
    End If
End Function

ダブルクォートを必要な時だけ付ける(CsvEscape)って?

特殊文字が入っているときに
逃げる(エスケープ)処理

ダブルクオートはなぜ必要か

こんなときにダブルクオートが必要

  • カンマ ( , ) が入っているとき
    • CSVはカンマ区切りだから、「山田,太郎」をそのまま書いたら「2列」と解釈されてしまう
    • 「"山田,太郎"」で「1セルの文字列」と認識される
  • 改行 (CR/LF) が入っているとき
    • 途中に改行があると、「ここで行が終わった」と誤解されてしまう
  • ダブルクォート ( ” ) 自体が含まれるとき
    • " → "" にエスケープして全体をクオートで囲む

こんなときはダブルクオートが不要

  • 普通の文字列(カンマ・改行・ダブルクォートが無い値)
    • 123」 や 「山田太郎」 のような文字列はクオート無しで安全

誤解を防ぐための””

ちなみに、いちど配列にするのがおすすめ

配列にする理由

CSV本文の組み立ては2通りある

1. 文字列を line & vbCrLf で「どんどん結合」していく方式

Dim sb As String
For r = 1 To lastRow
    sb = sb & line & vbCrLf
Next

特徴

  • 実装がシンプル。
  • 数百~数千行くらいなら、これで十分
  • 数万~数十万行になると 処理が極端に遅くなる
文字列結合が遅い理由

sb = sb & line のたびに
新しいバッファを確保→中身を丸ごとコピーが起きる

2. 配列にためて、最後に Join する方式

Dim arr() As String, idx As Long
ReDim arr(1 To lastRow)

For r = 1 To lastRow
    arr(idx) = line
    idx = idx + 1
Next

sb = Join(arr, vbCrLf)

特徴

  • 高速・省メモリ。文字列コピーが最小限。
  • 1万行以上では圧倒的に有利。
  • パフォーマンス改善効果が大きい
小規模でも配列にしておくと無難

配列でもオーバースペックにはならないため、損はない

参考

同じ処理をPythonでも書いてみよう

著者

author
月うさぎ

編集後記:
この記事の内容がベストではないかもしれません。

記事一覧