CSVを読込み別のCSVに書き込み

タグの編集
投稿者 PAPA  (社会人) 投稿日時 2012/11/26 14:03:05
VB2010初心者です。
vb.net2010でCSVファイルを変換して新しいCSVファイルを作りたいのですが、どのようにすれば良いかヒントを教えていただけないでしょうか?

佐藤,国語,70
高橋,国語,80
鈴木,国語,90
佐藤,数学,75
高橋,数学,85
鈴木,数学,90
佐藤,英語,50
高橋,英語,55
鈴木,英語,60


この様なCSVファイルを変換して

名前,国語,数学,英語
佐藤,70,75,80
高橋,80,85,55
鈴木,90,90,60

という表のようなCSVファイルを作りたいのです。配列を使って行うのでしょうか
ヒントだけでも良いのでよろしくお願いいたします。
投稿者 ヴァン  (社会人) 投稿日時 2012/11/26 15:11:32
こんにちは。

・教科は3教科だけなのか?
・人によって無い教科があるのか?
・教科の並びに条件はあるのか?

この辺りの条件はどうなのでしょうか?
投稿者 PAPA  (社会人) 投稿日時 2012/11/26 15:29:51
初めまして初心者のPAPAです。早速、ご連絡頂きありがとうございます。
条件ですが、下記の様になります。

・教科は3教科だけなのか?      ⇒ 3教科以上ある場合があります。
・人によって無い教科があるのか? ⇒ あります。
・教科の並びに条件はあるのか?  ⇒  ありません。固定です。

条件等、詳しく記載してなくてすみません。

宜しくお願い致します。
投稿者 ミミガー  (社会人) 投稿日時 2012/11/26 15:40:45
これはVB2010でやるよりは、Excelのマクロ(VBA)を使用した方が手っ取り早いと思いますが...

少なくとも、CSVをDataTableのような形式で読み込む(それが難しいなら配列データとして読み込む)ことができないと、次工程(名前で科目別点数の表を作成)に進むことができませんが、その点はどうなのでしょうか?
投稿者 PAPA  (社会人) 投稿日時 2012/11/26 15:51:32
初めまして初心者のPAPAです。早速、ご連絡頂きありがとうございます。

Excelのマクロ(VBA)ではなくて、VB2010で検討しています。
CSVをStreamReaderで読み込んで配列データとして行いたいと思ってます。

宜しくお願い致します。
投稿者 YuO  (社会人) 投稿日時 2012/11/26 18:15:38
ミミガーさんが書かれている,DataTbleを使うのが一番簡単だと思いますが……。

一行読んで,
・存在しない教科 (列名) であれば,列を追加する
・存在しない名前 (行の名前列を検索) であれば,行を追加する
・一致する行と列の場所に得点を設定する
を繰り返して最終的なデータを作り,それを元にCSVを書くことになります。

Dictionary(Of String, Dictionary(Of String, Integer))
でも作れなくはないと思いますが……。
# 教科の扱いが面倒になるので……。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 18:27:01
いろいろな書き方があるとは思いますが、Linq を使った例をひとつ。

・行の並び順は自由です。英数国でも数国英でも構いませんし、科目順でも名前順でも OK です。
・該当する科目が記入されていなかった場合、その科目は0点となります。
・人と科目が重複する組合せがあった場合、その人の点数は各行の合算値となります。

Imports System.IO
Imports System.Text
Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim srcFile As String = "C:\temp\1.csv"
        Dim dstFile As String = "C:\temp\2.csv"

        Dim sjis As Encoding = Encoding.GetEncoding("Shift_JIS")

        '評価の一覧として読み込む 
        Dim q = From row In File.ReadLines(srcFile, sjis)
          Where row <> "" Let cols = row.Split(",")
          Select row = New With {.姓 = cols(0), .科目 = cols(1), .点数 = CInt(cols(2))}
          Group By row.姓 Into
            国語 = Sum(If(row.科目 = "国語", row.点数, 0)),
            数学 = Sum(If(row.科目 = "数学", row.点数, 0)),
            英語 = Sum(If(row.科目 = "英語", row.点数, 0))

        'ヘッダ付きcsv形式に変換 
        Dim csv = {"名前,国語,数学,英語"}.Union(q.Select(Function(row) _
          String.Format("{0},{1},{2},{3}", row.姓, row.国語, row.数学, row.英語)))

        'ファイルに保存する 
        File.WriteAllLines(dstFile, csv, sjis)
    End Sub
End Class
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 18:52:24
DataTable 案です。

先の例との差別化のため、同じ科目を複数回受けた場合は、
後で受けた値が有効となるようにしてみました。
該当する科目を受講していない場合、0ではなく空欄になります。

先の回答では File.ReadLines メソッドを使っていますが、
今回は  File.ReadAllLines メソッドを利用しています。

Imports System.IO
Imports System.Text
Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim srcFile As String = "C:\temp\1.csv"
        Dim sjis As Encoding = Encoding.GetEncoding("Shift_JIS")

        '元のcsvを読み込む 
        Dim src = From row In File.ReadAllLines(srcFile, sjis)
          Where row <> "" Let cols = row.Split(",")
          Select New With {.姓 = cols(0), .科目 = cols(1), .点数 = CInt(cols(2))}

        '必要な科目分の列を持ったDataTableを用意 
        Dim tbl As New DataTable()
        tbl.PrimaryKey = {tbl.Columns.Add("姓")}
        For Each col In From row In src Select row.科目 Distinct
            tbl.Columns.Add(col, GetType(Integer))
        Next

        'DataTableに格納 
        For Each r In src
            Dim row = If(tbl.Rows.Find(r.姓), tbl.Rows.Add(r.姓))
            row(r.科目) = r.点数
        Next

        'DataGridView に表示 
        DataGridView1.DataSource = tbl
    End Sub
End Class
投稿者 PAPA  (社会人) 投稿日時 2012/11/26 19:38:02
初めまして、魔界の仮面弁士 様。

やりたい事が、バッチリ出来ました。本当に有難う御座いました。

もう一つ、ご質問ですが、DataGridViewに表示した後に、DataGridViewの内容をCSVに書き込む
方法を教えて頂けると嬉しいです。

お忙しい中、すみませんが宜しくお願い致します。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 20:20:59
> DataGridViewに表示した後に、DataGridViewの内容をCSVに書き込む

こんな感じでしょうか。

なお、DataTable を DataGridView にバインドしている場合は、
DataGridView のセル内容を列挙するのではなく、
DataTable の中身を取り出すようにした方が良いでしょう。

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim dstFile As String = "C:\temp\2.csv"
    Dim sjis As Encoding = Encoding.GetEncoding("Shift_JIS")

    Using stm As New StreamWriter(dstFile, False, sjis)
        'ヘッダ行の書き込み 
        Dim header = From col In DataGridView1.Columns.Cast(Of DataGridViewColumn)() _
          Select col.HeaderText
        stm.WriteLine(String.Join(",", header))

        'データ行の書き込み 
        For Each row In From r In DataGridView1.Rows.Cast(Of DataGridViewRow)() _
          Where Not r.IsNewRow
            Dim contents = From cell In row.Cells.Cast(Of DataGridViewCell)() Select cell.Value
            stm.WriteLine(String.Join(",", contents))
        Next

        stm.Close()
    End Using
End Sub
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 21:08:41
>> やりたい事が、バッチリ出来ました。本当に有難う御座いました。
『CSVをStreamReaderで読み込んで配列データとして行いたいと思ってます。』
という条件は満たせていないですけれどね。


> なお、DataTable を DataGridView にバインドしている場合は、
> DataGridView のセル内容を列挙するのではなく、
> DataTable の中身を取り出すようにした方が良いでしょう。

DataGridView を用いず、 DataTable の中身を csv に保存してみました。

今回は File.WriteAllText と File.AppendAllLines を使ってみました。

この方法だと、ファイルを作成した後、追記モードで開きなおす形になるので、
本当は StreamWriter を使う方法や、File.WriteAll 系メソッド一回で書く方が
良いとは思いますが、こういう書き方もあるということで。


Private tbl As DataTable   'ここにデータを読み込んでおくこと 

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim dstFile As String = "C:\temp\2.csv"
    Dim sjis As Encoding = Encoding.GetEncoding("Shift_JIS")

    'ヘッダ行の書き込み 
    File.WriteAllText(dstFile, _
      String.Join(","c, tbl.Columns.Cast(Of DataColumn) _
      .Select(Function(col) col.ColumnName)), sjis)

    'データ行の書き込み 
    File.AppendAllLines(dstFile, _
      tbl.AsEnumerable().Select(Function(r) _
      String.Join(","c, r.ItemArray)), sjis)
End Sub
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 22:07:34
> いろいろな書き方があるとは思いますが、Linq を使った例をひとつ。

今度は、Linq を使わずに、ループで処理する手法で書いてみました。

『CSVをStreamReaderで読み込んで配列データとして行いたいと思ってます。』
という要望に逆らって、あえて今回は System.IO.StreamReader ではなく、
Microsoft.VisualBasic.FileIO.TextFieldParser で読み込ませています。

Imports System.IO
Imports System.Text
Imports Microsoft.VisualBasic.FileIO

Public Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim srcFile As String = "C:\temp\1.csv"
        Dim dstFile As String = "C:\temp\2.csv"
        Dim sjis As Encoding = Encoding.GetEncoding("Shift_JIS")

        Dim tbl As New DataTable() With {.PrimaryKey = {.Columns.Add("名前")}}
        Dim cols = tbl.Columns
        Dim rows = tbl.Rows

        'csv を読み込んで DataTable を作成。 
        Using reader As New TextFieldParser(srcFile, sjis)
            reader.SetDelimiters(",")
            Do Until reader.EndOfData
                Dim fields() As String = reader.ReadFields()
                If fields.Length <> 3 Then
                    Continue Do
                End If
                Dim row = If(rows.Find(fields(0)), rows.Add(fields(0)))
                If Not cols.Contains(fields(1)) Then
                    cols.Add(New DataColumn() With { _
                        .ColumnName = fields(1), _
                        .DataType = GetType(Integer), _
                        .AllowDBNull = False, _
                        .DefaultValue = 0 _
                    })
                End If
                row.SetField(fields(1), CInt(fields(2)))
            Loop
        End Using

        '確認用。今回の変換処理とは無関係。 
        DataGridView1.ReadOnly = True
        DataGridView1.DataSource = tbl

        '読み取った DataTable を csv 保存。 
        Using writer As New StreamWriter(dstFile, False, sjis)
            'ヘッダー部 
            For c As Integer = 0 To cols.Count - 2
                writer.Write(cols(c).ColumnName)
                writer.Write(","c)
            Next
            writer.Write(cols(cols.Count - 1).ColumnName)

            'データ本体部 
            For Each row As DataRow In rows
                For Each col As DataColumn In cols
                    If col.ColumnName = "名前" Then
                        writer.WriteLine()
                        writer.Write(row(col))
                        Continue For
                    End If
                    writer.Write(","c)
                    writer.Write(row(col))
                Next
            Next
            writer.Close()
        End Using
    End Sub
End Class



なお、先に書いたサンプルで出力される csv ファイルのフォーマットは
「レコード末尾に CrLf を付与」という仕様でしたが、今回のサンプルでは
「レコード間が CrLf で区切られる」という仕様に変更してみました。

(出力した csv ファイルの最後に改行があるか無いかの違いです)
投稿者 PAPA  (社会人) 投稿日時 2012/11/26 22:29:51
魔界の仮面弁士 様。

色々なパターン有難うございました。
この掲示板に投稿する前に2週間トライして全くできませんでした。
魔界の仮面弁士様に教えて頂き、本当に感謝しております。

お忙しい中、本当に有難う御座いました。

これからVBをもっと勉強します。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/11/26 23:15:22
> 今度は、Linq を使わずに、ループで処理する手法で書いてみました。

JET/ACE の Text-IISAM 経由で、クロス集計クエリー(TRANSFORM ステートメント)を利用する例。

Imports System.Data.OleDb
Public Class Form1
    Private tbl As DataTable
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim csvFolder As String = "C:\temp\"
        Dim srcFile As String = "1.csv"

        Dim sb As New OleDbConnectionStringBuilder()
        'sb.Provider = "Microsoft.JET.OLEDB.4.0" 
        sb.Provider = "Microsoft.ACE.OLEDB.12.0"
        sb.DataSource = csvFolder
        sb("Extended Properties") = "Text;HDR=No;FMT=Delimited;"

        Using conn As New OleDbConnection(sb.ConnectionString)
            conn.Open()

            Dim s As String = "TRANSFORM SUM(F3) " _
                            & "SELECT F1 AS `名前` " _
                            & "FROM `" & srcFile & "` " _
                            & "GROUP BY F1 " _
                            & "PIVOT F2"
            's &= " IN ('国語','数学','英語')" 

            tbl = New DataTable(srcFile.Replace("."c, "#"c))
            Using adp As New OleDbDataAdapter(s, conn)
                adp.Fill(tbl)
            End Using

            conn.Close()
        End Using

        '確認用。 
        DataGridView1.ReadOnly = True
        DataGridView1.DataSource = tbl
    End Sub
End Class
投稿者 PAPA  (社会人) 投稿日時 2012/11/30 23:57:20
お返事が遅くなりすみません。
魔界の仮面弁士様、ミミガー様、Yuo様、色々とアドバイスを頂き有難うございました。

また、初歩的なご質問をするかも知れませんが、その際には宜しくお願い致します。