コンテンツへスキップ

CSVをエクセルファイルで読み書きする

CSVファイルはデータ出力を伴う様々なシステムで扱われています。

システム知識の浅いユーザーが手動でCSVファイルを触ると、大抵の場合でデータを破損させてしまいます。その主な原因は、エクセルが文字コードや改行を意図せずに変更してしまうことがあるからです。そのくせ、CSVファイルをダブルクリックするとエクセルで開いてしまうので厄介です。

Cassava Editorなど、CSVファイルを崩さずに編集できるソフトはありますが、エクセルは関数が便利なので「エクセルに読み込んでシートに表示」「シートをCSVに出力」をワンクリックで実行できるマクロを作りました。

読み込むCSVファイルの単一セル内にカンマや改行が含まれていても大丈夫なようにしています。

Option Explicit

Sub clearSheet()

    '############################# 変数定義 #############################
    
    Dim varChoice       As Variant      'ユーザー選択
    Dim ws              As Worksheet
    
    
    '############################# 既存データ削除確認 #############################
    
    varChoice = MsgBox("Existing data on 'Data' sheet will be deleted. Please make a backup of this file if necessary. Do you wish to continue?", vbYesNo)
    If varChoice = vbNo Then End 'Noを選んだらマクロ終了
    
    
    '############################# Dataシート初期化 #############################
    
    initializeSheet ("Data")

    '完了メッセージ
    MsgBox "Done!"


End Sub


'「Export to CSV」ボタンを押したときの処理
Sub CsvExport()
    
    '############################# 変数定義 #############################
    
    Dim r               As Integer  '行番号
    Dim c               As Integer  '列番号
    Dim intMaxRow       As Integer  '最大行
    Dim intMaxColumn    As Integer  '最大列
    Dim strPath         As String   'CSVファイルパス
    Dim strLine         As String   '加工済み行データ
    Dim ColumnID        As String   '項目ID
    Dim varRowsArray    As Variant  '列配列
    Dim Stream          As Object   'ストリームオブジェクト
    Dim ByteData()      As Byte     'バイトデータ
    
    
    '############################# A1にデータが無ければ終了 #############################
    
    If Worksheets("Data").Cells(1, 1).Value = "" Then
        MsgBox "No data in Row 1 Column A."
        End
    End If

    
    '############################# 保存ファイル名取得 #############################
        
    strPath = Application.GetSaveAsFilename(InitialFileName:="EmployeeImportData.csv", FileFilter:="CSV UTF-8,*.csv")
    If strPath = "False" Then End 'キャンセルが押されたらマクロ終了
    
    
    '############################# Stream準備 #############################
    
    Set Stream = CreateObject("Adodb.Stream")
    With Stream
        .Charset = "UTF-8"      '文字エンコード
        .Type = adTypeText      'テキストデータタイプ
        .LineSeparator = 10     '行区切り文字をadLFに設定
        .Open                   'ストリームを起動
    End With
    
    
    '############################# 列、行取得 #############################
    
    '列数取得
    intMaxColumn = Worksheets("Data").Cells(1, Columns.Count).End(xlToLeft).Column
    
    '行数取得 (データのない列がある可能性を考慮)
    ReDim varRowsArray(intMaxColumn)
    intMaxRow = 0
    Dim i As Long
    For i = 1 To intMaxColumn
        varRowsArray(i) = Worksheets("Data").Cells(Rows.Count, i).End(xlUp).Row
        If intMaxRow < varRowsArray(i) Then
            intMaxRow = varRowsArray(i)
        End If
    Next
    
    
    '############################# Stream書き込み #############################
    
    '1行目からデータの最終行まで繰り返す
    For r = 1 To intMaxRow
        'strLine初期化 & 1列目読み込み
        strLine = """" & Worksheets("Data").Cells(r, 1).Value & """"
        
        '2列目から最終列までデータを読み込む
        For c = 2 To intMaxColumn
            strLine = strLine & "," & """" & Worksheets("Data").Cells(r, c).Value & """"
        Next c
        
        'StrLineに格納した行データを改行付きでストリームに書き込む
        Stream.WriteText strLine, adWriteLine
    Next r
    
    'BOMなしにするための準備
    With Stream
        .Position = 0 'ストリーム内の位置
        .Type = adTypeBinary 'バイナリデータタイプ
        .Position = 3 'ストリーム内の位置
        ByteData = .Read
    End With
   
    'ファイルの保存
    With Stream
        .Close
        .Open
        .Write ByteData 'BOMなしにする
        .SaveToFile strPath, 2 'ファイル存在していても上書き
        .Close
    End With
    
    '完了メッセージ
    MsgBox "Done!"

End Sub


'「Read CSV」ボタンを押したときの処理
Sub CsvReadButton()

    '############################# 変数定義 #############################
    
    Dim strPath     As String       'CSVファイルパス
    Dim varCSV      As Variant      'CSVを読み込んだ配列
    Dim varChoice   As Variant      'ユーザー選択
    Dim ws          As Worksheet
    
    
    '############################# 既存データ削除確認 #############################
    
    varChoice = MsgBox("Existing data on 'Data' sheet will be deleted. Please make a backup of this file if necessary. Do you wish to continue?", vbYesNo)
    If varChoice = vbNo Then End 'Noを選んだらマクロ終了
    
    
    '############################# CSVファイルパス取得 #############################

    strPath = Application.GetOpenFilename(FileFilter:="CSV UTF-8,*.csv", Title:="Select CSV File")
    If strPath = "False" Then End
    
    
    '############################# 高速化用処理 #############################
    
    With Application
        .Cursor = xlWait                    'マウスポインターを砂時計にする
        .ScreenUpdating = False             '処理中はエクセルシートへの変更を画面に反映しない
        .Calculation = xlCalculationManual  'セルの関数が処理されるのを防ぐ
    End With
    
    
    '############################# Dataシート初期化 #############################
    
    initializeSheet ("Data")


    '############################# CSVファイルを配列に読み取り #############################
    
    '配列に読み込み
    varCSV = readCSVtoTemp(strPath)
    
    'ワークシートに張り付け
    Worksheets("Data").Range("A1").Resize(UBound(varCSV, 1) + 1, UBound(varCSV, 2) + 1) = varCSV
    
    
    '############################# 後処理 #############################
    
    With Application
        .Cursor = xlDefault                     'マウスポインターを標準に戻す
        .ScreenUpdating = True                  'エクセルシートへの変更を画面に反映する
        .Calculation = xlCalculationAutomatic   'セルの関数が処理されるようにする
    End With

    'アクティブシートの変更
    Worksheets("Data").Activate

    MsgBox "完了しました"

End Sub



'【関数】シートの初期化
Function initializeSheet(ByVal strSheetName As String)

    Dim strFlag         As String       '存在フラグ
    Dim ws              As Worksheet
    
    '該当シートがあれば中身をクリアする
    For Each ws In Worksheets
        If ws.Name = strSheetName Then
            ws.Activate
            ws.Cells.Clear
            ws.Cells.EntireColumn.ColumnWidth = ws.StandardWidth
            strFlag = "Sheet exists"
        End If
    Next ws
    
    '該当シートがなければ作成する
    If strFlag <> "Sheet exists" Then
        Set ws = Worksheets.Add(After:=ActiveSheet)
        ws.Name = strSheetName
    End If

End Function


'【関数】カンマと改行を区切り文字で代入
Function splitByCommaAndLine(ByVal str As String) As Variant

    Dim i               As Long     '文字番号カウンタ
    Dim strTemp         As String   '判定文字
    Dim quotCount       As Long     '"カウンタ
       
    'vbCrLfをvbLfに置き換える
    If InStr(str, vbCrLf) > 0 Then
        str = Replace(str, vbCrLf, vbLf)
    End If
       
    '1文字ずつ区切るべきカンマか判別する
    For i = 1 To Len(str)
        'strから現在の1文字を切り出す
        strTemp = Mid(str, i, 1)
        '代入すべき文字indexを収集する
        If strTemp = """" Then
            quotCount = quotCount + 1
        ElseIf strTemp = "," Then
            If quotCount Mod 2 = 0 Then Mid(str, i, 1) = "†"
        ElseIf i = Len(str) Then
            If strTemp = vbLf Or strTemp = vbCr Then GoTo EndIndent
        ElseIf strTemp = vbLf Or strTemp = vbCr Then
            If quotCount Mod 2 = 0 Then Mid(str, i, 1) = "‡"
        End If
    Next
    
    'データを改行(‡)で区切る
    splitByCommaAndLine = Split(str, "‡")
    strTemp = ""
    Exit Function
    
'最後が改行コードの場合は改行コードを除く
EndIndent:
    splitByCommaAndLine = Split(Left(str, Len(str) - 1), "‡")
    strTemp = ""
    Exit Function

End Function


'【関数】CSVの読み込み(戻り値は配列)
Function readCSVtoTemp(ByVal FilePath As String) As Variant

    '変数の定義
    Dim Stream          As Object       'ストリームのオブジェクト(CSVをインポート/エクスポートするライブラリ)
    Dim csvAll          As String       'csvの全データ
    Dim csvLine         As Variant      'csvの行データ(カンマで区切る前)
    Dim csvLineSplit    As Variant      'csvの行データ(カンマで区切った後)
    Dim csvResult       As Variant      'csvの補正後全データ

    'CSVファイルの読み込み
    Set Stream = CreateObject("Adodb.Stream")
    With Stream
        .Type = adTypeBinary        'バイナリで読み込む(高速化)
        .Open                       'ストリームを起動
        .LoadFromFile FilePath      'csvのパス指定
        .Position = 0               'ポジション指定
        .Type = adTypeText          'テキストモードに変更
        .Charset = "UTF-8"          '文字エンコード
        csvAll = .ReadText          'ファイルを読み込む
        .Close                      'ストリームの終了
    End With
    Set Stream = Nothing

    '1行ごとにデータを区切る
    csvLine = splitByCommaAndLine(csvAll)
    csvAll = ""

    '2次元配列の行&列数設定
    Dim RowMaxIndex As Long
    RowMaxIndex = UBound(csvLine)
    Dim ColumnMaxIndex As Long
    ColumnMaxIndex = UBound(Split(csvLine(0), "†"))
    ReDim csvResult(RowMaxIndex, ColumnMaxIndex)

    '1行目から最終行まで繰り返し
    Dim i As Long
    Dim j As Long
    For i = LBound(csvLine) To UBound(csvLine)
        'カンマでデータを区切る
        csvLineSplit = Split(csvLine(i), "†")
        'ダブルクォーテーションなしで結果配列にコピーする
        For j = LBound(csvLineSplit) To UBound(csvLineSplit)
            csvResult(i, j) = Replace(csvLineSplit(j), """", "")
        Next
    Next
    Erase csvLine
    Erase csvLineSplit
    
    'ワークシートに張り付け
    'Worksheets("temp").Range("A1").Resize(RowMaxIndex + 1, ColumnMaxIndex + 1) = csvResult
    
    readCSVtoTemp = csvResult
    Erase csvResult
    
End Function