Excel VBAによるCSVインポート機能の自作(フィールド内の改行も取り込む)

標準のインポート機能の問題点など

ExcelでCSVをインポートするときの話。
システム開発で証跡を残すときに、DBデータをCSVに落としてExcelに貼り付けることが多い。
また、レイアウトを横長ではなく縦長にしたりすることも、少なからずある。
これをExcel標準機能だけでこなそうとすると、結構面倒だ。
それだけでなく、お節介な機能が働いてしまい、勝手にデータが変換されてしまう。さらに厄介なことに、このお節介機能を制御することができない。
もっと言いたいことがある。フィールドに改行コードが含まれている場合、標準のインポート機能ではそこを行端と判断してしまい、取り込み後のレイアウトが崩れてしまう。

自作インポート機能のイメージ


そこで右クリックメニューからCSVファイルを指定しただけで、データを即座に貼り付ける自作機能をVBAで作ってみた。

ここでは、1行のデータとして以下のような形式を想定している。
 a,"b,c",d,"e,f,CRLFg",hLF
CRLFはwin系の改行コード、LFはUNIX系の改行コード。
CRLFはフィールド内の改行コードとして使用されている。
LFは行端を示す改行コードとして使用されている。
フィールドにカンマや改行コードが含まれる場合は、ダブルクォーテーションで括られている。

今回の自作機能は動作が重く、大量データを取り込もうとすると固まる。 数十行くらいなら、おそらく許容範囲の処理時間だと思う。

実装(右クリックメニュー)

マクロブックをアドインとして保存する まず、右クリックメニューの追加。 VBEの「Microsoft Excel Objects」内の「ThisWorkbook」に以下を記述する。 これにより、このアドインを追加しておけば、次回Excel起動時からは右クリックメニューの一番下に追加表示される。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Private Sub Workbook_Open()
    Application.CommandBars("Cell").Reset
    Set newb = Application.CommandBars("Cell").Controls.Add()
    With newb
        .Caption = "csvインポート(横)"
        .OnAction = "CsvImportHorizontal"
        .BeginGroup = True
    End With
    Set newb = Application.CommandBars("Cell").Controls.Add()
    With newb
        .Caption = "csvインポート(縦)"
        .OnAction = "CsvImportVertical"
    End With
End Sub

実装(インポート本体)

次にインポート機能の本体部分。 一応これで概ね動作すると思うが、誤動作した場合はご容赦を。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
Option Explicit

Const VERTICAL = 1
Const HORIZONTAL = 2

Const SP = "~~~~~" ' セパレータ
Const EOL = "@@@@@" ' 行端文字
Const DQ = """" ' ダブルクォーテーション

Sub CsvImportHorizontal()
    Call CsvImport(HORIZONTAL)
End Sub

Sub CsvImportVertical()
    Call CsvImport(VERTICAL)
End Sub

Sub CsvImport(hvFlg)
    Dim csvFile, adoSt, buf, lineArr, fieldArr, tpl, i, j, r, c

    On Error GoTo errH

    ' ダウンロードフォルダのCSVファイルを選択
    ChDir CreateObject("Wscript.Shell").SpecialFolders("MyDocuments") & "\..\Downloads\"
    csvFile = Application.GetOpenFilename("CSVファイル(*.csv), *.csv")
    If VarType(csvFile) = vbBoolean Then Exit Sub

    ' ADODBストリームを生成してCSVファイルの内容をバッファに読み込み
    Set adoSt = CreateObject("ADODB.Stream")
    With adoSt
        .Charset = "UTF-8"
        .Type = 2
        .LineSeparator = 10
        .Open
        .LoadFromFile (csvFile)
        buf = .Readtext
        .Close
    End With

    ' バッファ内のセパレータをカンマから別文字に変更する
    buf = rep(buf)

    ' バッファを行単位の配列にする
    buf = Replace(buf, DQ, "")
    buf = Replace(buf, vbCrLf, vbCr)
    buf = Replace(buf, vbLf, EOL)
    buf = Replace(buf, vbCr, vbCrLf)
    lineArr = Split(buf, EOL)
    ' 最終行が空行の場合は除外する
    If lineArr(UBound(lineArr)) = "" Then
        ReDim Preserve lineArr(UBound(lineArr) - 1)
    End If

    ' 行単位の配列をセパレータで分離して2次元配列を作成する
    ReDim fieldArr(UBound(lineArr), UBound(Split(lineArr(0), SP)))
    For i = 0 To UBound(lineArr)
        tpl = Split(lineArr(i), SP)
        For j = 0 To UBound(tpl)
            fieldArr(i, j) = tpl(j)
        Next
    Next

    ' csvインポート(縦)の場合
    If hvFlg = VERTICAL Then
        ' フィールド単位の配列の行要素と列要素を入れ替え
        fieldArr = WorksheetFunction.Transpose(fieldArr)

        ' 貼り付け範囲を取得
        r = UBound(fieldArr, 1)
        c = UBound(fieldArr, 2)

    ' csvインポート(横)の場合
    Else
        ' 貼り付け範囲を取得
        r = UBound(fieldArr, 1) + 1
        c = UBound(fieldArr, 2) + 1

    End If

    ' 貼り付け範囲の書式を文字列に設定
    Range(ActiveCell, ActiveCell.Offset(r - 1, c - 1)).NumberFormatLocal = "@"

    ' フィールド単位の配列をアクティブセルを基点にしてシートに貼り付け
    ActiveCell.Resize(r, c).Value = fieldArr

    Set adoSt = Nothing
    Exit Sub
errH:
    Set adoSt = Nothing
    MsgBox "エラーが発生しました" & vbCrLf & "エラー番号:" & Err.Number & vbCrLf & "エラーの種類:" & Err.Description
End Sub

' セパレータを別文字に置換
Function rep(line)
    Dim regex
    Set regex = CreateObject("vbscript.regexp")
    With regex
        .ignorecase = True
        .Global = True
        .Pattern = ",(?=([^" & DQ & "]*" & DQ & "[^" & DQ & "]*" & DQ & ")*(?![^" & DQ & "]*" & DQ & "))"
        rep = .Replace(line, SP)
    End With
    Set regex = Nothing
End Function

処理の流れを説明しておく。
1.CSVファイルを選択する。
2.ADOでCSV内のデータをバッファに取り込む。(ADOを使うのはUTF-8に対応するため)
3.バッファ内のカンマセパレータを別文字Aに置換。
4.バッファ内のフィールド内の改行コードをいったん別文字Bに置換。
  次に行端を示す改行コードを別文字Cに置換して、Bを元の改行コードに戻す。
5.バッファをセパレータAと行端を示すBで分割し、2次元配列を生成。
6.縦形式のインポートの場合は、2次元配列の行と列を入れ替える。
7.2次元配列を右クリックしたセルを基点に貼り付ける。

3.は正規表現を使ってカンマセパレータを置換しているが、ここが肝だ。(ファンクションrep)
これさえできれば、あとはそんなに難しくない。
ここで説明すると長くなるので、別記事「CSVでフィールド内にカンマがあるときの話」で説明した。
4.も少しややこしいが、ここで説明しておく。

1行のデータが以下だとする。
 a,"b,c",d,"e,f,CRLFg",hLF
ダブルクォーテーション内にカンマや改行コードが含まれているが、一つのフィールドデータだ。 上の4でやっている置換の流れはこうだ。(別文字A=~、別文字B=CR、別文字C=@とする)

★3の処理後から
a~"b,c"~d~"e,f,CRLFg"~hLF

↓ (CRLFをCRに置換)

a~"b,c"~d~"e,f,CRg"~hLF

↓ (LFを@に置換)

a~"b,c"~d~"e,f,CRg"~h@

↓ (CRをCRLFに戻す)

a~"b,c"~d~"e,f,CRLFg"~h@

ここで使用した「~」「CR」「@」などの別文字は、フィールドデータとして使用されないものであれば、なんでも良い。なお実際のコードではダブルクォートを削除しており、また別文字がフィールドデータと確実に被らないように「~~~~~」などとしている。

この置換の目的は、フィールド内の改行コードCRLFのLF部分と、行端を示すLFを区別するためだ。 この置換をしないと、フィールド内の改行コードが行端とみなされてしまって、エラーの原因となる。 また、いきなり行端のLFを別文字に置換しようとしても、同時にフィールド内のCRLFのLF部分も置換されてしまい、エラーの原因となる。

おわりに

もしデータ形式としてフィールド内の改行コードがCRLFではなく行端と同じLFが使われている場合は、このPGは正しく動作しない。 この場合は、正規表現を使うなどして行端のLFのみを置換する必要があるだろうが、別の機会で考えたい。

関連ページ