write ahead log

ロールフォワード用

VB.NETでCSVを読み込む・書き込む

簡単そうでエスケープ(,入力が必要な際のダブルクオーテーションによる例外化)処理やデリミタ変更(TSV)が以外と面倒なCSV処理.

標準ライブラリでやってみました.

書いてからEncoding指定もパラメータ化すれば良かったと思いましたが, そもそもクラス化してライブラリにした方がいいでしょうね.

読み込み

ファイル名指定

TextFieldParserを使うとファイル名も指定出来て楽.

けど, 後述するStream渡しの方が単機能になって個人的には好み.

''' <summary>
''' CSVファイル名を渡して解析し, 結果の2次元のリストを返す
''' </summary>
''' <param name="fileName">解析するCSVファイル名</param>
''' <returns>CSVの行列を含む2次元のリスト</returns>
Private Function LoadCSVFile(ByVal fileName As String) As List(Of List(Of String))
    '=========================================
    '結果返答用のリストを準備
    '=========================================
    Dim ret As New List(Of List(Of String))

    '=========================================
    'ファイル解析用にTextFieldParserを用意
    '=========================================
    'ファイル名から指定する場合
    Dim parser As New FileIO.TextFieldParser(fileName,
                             System.Text.Encoding.GetEncoding("Shift_JIS"))

    '=========================================
    'Parserの設定
    '=========================================
    '区切り文字形式指定
    parser.TextFieldType = FileIO.FieldType.Delimited
    'デリミタを指定(CSVなら, TSVならvbTab)
    parser.SetDelimiters(",")
    'ダブルクオーテーションでエスケープするフィールドがあるならTrue
    parser.HasFieldsEnclosedInQuotes = True
    '区切り文字前後の空白スペースを除去するならTrue
    parser.TrimWhiteSpace = True

    '=========================================
    'Parse開始
    '=========================================
    'ファイル終端まで繰り返す
    While Not parser.EndOfData
        '結果を入れるリスト(1行分)
        Dim ret_row As New List(Of String)

        '1行読み出す => 各列の配列になって取得できる
        Dim row As String() = parser.ReadFields()

        '各列を読み出し
        For Each field As String In row
            '各列分リストへ入れる
            ret_row.Add(field)
        Next
        '出来た1行分をリストへ入れる
        ret.Add(ret_row)
    End While

    Return ret
End Function

IO.Streamを指定

ファイル名ではなくIO.Streamを指定してやる.

こっちの方が外部IOが無い分, シンプルで良いと思う.

''' <summary>
''' CSVのIO.Streamを渡して解析し, 結果の2次元のリストを返す
''' </summary>
''' <param name="csvStream">解析するCSVのIOStream</param>
''' <returns>CSVの行列を含む2次元のリスト</returns>
Private Function LoadCSVFromStream(ByVal csvStream As IO.Stream) As List(Of List(Of String))
    '=========================================
    '結果返答用のリストを準備
    '=========================================
    Dim ret As New List(Of List(Of String))

    '=========================================
    'ファイル解析用にTextFieldParserを用意
    '=========================================
    Dim encoding As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift_JIS")
    Dim parser As New FileIO.TextFieldParser(csvStream, encoding)

    '=========================================
    'Parserの設定
    '=========================================
    '区切り文字形式指定
    parser.TextFieldType = FileIO.FieldType.Delimited
    'デリミタを指定(CSVなら, TSVならvbTab)
    parser.SetDelimiters(",")
    'ダブルクオーテーションでエスケープするフィールドがあるならTrue
    parser.HasFieldsEnclosedInQuotes = True
    '区切り文字前後の空白スペースを除去するならTrue
    parser.TrimWhiteSpace = True

    '=========================================
    'Parse開始
    '=========================================
    'ファイル終端まで繰り返す
    While Not parser.EndOfData
        '結果を入れるリスト(1行分)
        Dim ret_row As New List(Of String)

        '1行読み出す => 各列の配列になって取得できる
        Dim row As String() = parser.ReadFields()

        '各列を読み出し
        For Each field As String In row
            '各列分リストへ入れる
            ret_row.Add(field)
        Next
        '出来た1行分をリストへ入れる
        ret.Add(ret_row)
    End While

    Return ret
End Function

StringをIO.Streamへ変換してやれば文字列渡しでも使えるし.

''' <summary>
''' CSVのStringを渡して解析し, 結果の2次元のリストを返す
''' </summary>
''' <param name="csvStr">解析するCSVのString</param>
''' <returns>CSVの行列を含む2次元のリスト</returns>
Private Function LoadCSVFromString(ByVal csvStr As String) As List(Of List(Of String))
    'Encoding指定し, StringをStreamへ変換
    Dim encoding As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift_JIS")
    Dim stream As New IO.MemoryStream(encoding.GetBytes(csvStr))

    'Streamを2次元のリストにして返す
    Return LoadCSVFromStream(stream)
End Function

書き込み

読み込みに比べれば随分楽です.

''' <summary>
''' Streamに対してCSVListの内容をCSV形式で書き込みます
''' </summary>
''' <param name="stream">書き込み先Stream</param>
''' <param name="CSVList">書き込む内容の行/列の2次元List</param>
''' <param name="delimiter">区切り文字</param>
''' <returns>書き込み先Stream</returns>
Private Function WriteCSV(ByVal stream As IO.StreamWriter, CSVList As List(Of List(Of String)), ByVal delimiter As String) As IO.StreamWriter
    For i As Integer = 0 To CSVList.Count - 1
        Dim row As List(Of String) = CSVList(i)

        For k As Integer = 0 To row.Count - 1
            '列の値
            Dim col As String = row(k)

            'デリミタが含まれていればエスケープ
            If col.Contains(delimiter) = True Then
                'ダブルクオーテーションが含まれているか確認
                ' => 2重化してエスケープ
                If col.Contains("""") = True Then
                    col = col.Replace("""", """""")
                End If
                '両端にダブルクオーテーションを入れてエスケープ
                col = """" & col & """"
            End If

            '列の値を書き込み
            stream.Write(col)

            '最終列でなければデリミタ挿入
            If k <> row.Count - 1 Then
                stream.Write(delimiter)
            End If
        Next
        '最終行でなければ改行を書き込み
        If i <> CSVList.Count - 1 Then
            stream.Write(vbCrLf)
        End If
    Next

    Return stream
End Function

使う時はこんな感じ.

''' <summary>
''' 試しに書き込み
''' </summary>
Private Sub testCSVWrite()
    '=========================================
    'テスト用のデータ作成
    '=========================================
    Dim list As New List(Of List(Of String))

    Dim r1 As New List(Of String)
    r1.Add("abc")
    r1.Add("def")
    list.Add(r1)

    Dim r2 As New List(Of String)
    r2.Add("""abc""")
    r2.Add("def")
    list.Add(r2)

    Dim r3 As New List(Of String)
    r3.Add("test,string")
    r3.Add("""zxd,")
    list.Add(r3)

    '=========================================
    '書き込み
    '=========================================
    'エンコーディング指定
    Dim encoding As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift_JIS")
    '保存先パス
    Dim path As String = System.IO.Path.Combine(
        System.Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
        "test.csv")
    '書き込み用のStreamWriterを準備
    '第一引数でパス指定
    Dim sw As New IO.StreamWriter(path, False, encoding)
    '書き込み
    WriteCSV(sw, list, ",")
    '閉じる
    sw.Close()
End Sub