• 追加された行はこの色です。
  • 削除された行はこの色です。
#title(.NETプログラミング研究 第65号)

#navi(.NETプログラミング研究)

#contents

*.NETプログラミング研究 第65号 [#q3c81f5f]

**.NET質問箱 [#a3768cfb]

「.NET質問箱」では、「どぼん!のプログラミング掲示板」に書き込まれた.NETプログラミングに関する投稿を基に、さらに考察を加え、Q&A形式にまとめて紹介します。

-[[どぼん!のプログラミング掲示板>http://dobon.net/vb/bbs.html]]
-[[どぼん!のプログラミング掲示板>https://dobon.net/vb/bbs.html]]

***CSV形式のファイルをDataTableや配列等として取得する [#k7018fd4]

#column(注意){{
この記事の最新版は「[[CSV形式のファイルをDataTableや配列等として取得する>http://dobon.net/vb/dotnet/file/readcsvfile.html]]」で公開しています。
この記事の最新版は「[[CSV形式のファイルをDataTableや配列等として取得する>https://dobon.net/vb/dotnet/file/readcsvfile.html]]」で公開しています。
}}

''【質問】''

CSV(Comma Separated Value)形式のファイルをDataTableや配列として読み込むことはできませんか?

''【回答】''

これには幾つかの方法が考えられます。ここでは3つの方法を紹介しますが、その前にCSVとはなにかについて確認しておきます。

CSV形式について、絶対的な決まりは存在していないようです。ただし、一般的なアプリケーションで使われている決まりについては、次のサイトで説明されています。

-[[CSV Comma Separated Value File Format (How To)>http://www.creativyst.com/Doc/Articles/CSV/CSV01.htm]]

要約しますと、次のようになります。

1.レコードは、LFまたはCRLFで区切られる。

2.フィールドは、カンマ(,)で区切られる。

3.区切りのカンマの前後のスペース(タブを含む)は無視される。

4.フィールドにカンマが含まれる場合、フィールドをダブルクォートで囲まなければならない。

5.フィールドにダブルクォート(")が含まれる場合、フィールドをダブルクォートで囲み、フィールド内のダブルクォートを2つの連続するダブルクォート(つまり、「""」)に置き換えなければならない。

6.フィールドが改行文字を含む場合、フィールドをダブルクォートで囲まなければならない。

7.フィールドの前後にスペースがある場合、フィールドをダブルクォートで囲まなければならない。

8.すべてのフィールドがダブルクォートで囲まれているかもしれない。

9.はじめのレコードは、ヘッダかもしれない。

ここでは、ここのような決まりに準じて、話を進めることにします。

CSV形式のファイルを配列として取得する方法としてよく使われるのが、CSVファイルから一行ずつ読み出し、その一行をString.Splitメソッドを使って「,」文字で分割するという方法です。しかしこの方法は、上記の規則の4,6を見れば、正しくないことが分かります。つまり、フィールドにカンマや改行文字が含まれている可能性があるのです。よってこのような方法は、フィールドにカンマや改行文字が含まれていないことが保障されたCSVにしか使用できません。そうでないCSVを扱うためには、より複雑な処理が必要になります。

さて、ここから本題に入ります。まずは、Jet ProviderやODBC Providerを使う方法を紹介します。これらを使って、CSVファイルを解析することができます。

以下にJet Providerを使った例を紹介します。ここでは解析されたCSVファイルの内容をDataTableに格納しています。CSVファイルの文字コードは、Shift JISである必要があります。なお、接続文字列の「HDR=No」を「HDR=Yes」とすることにより、一行目をヘッダとすることができます。

#code(vbnet){{
'CSVファイルのあるフォルダ
Dim csvDir As String = "C:\"
'CSVファイルの名前
Dim csvFileName As String = "test.csv"

'接続文字列
Dim conString As String = _
    "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" _
    + csvDir + ";Extended Properties=""text;HDR=No;FMT=Delimited"""
Dim con As New System.Data.OleDb.OleDbConnection(conString)

Dim commText As String = "SELECT * FROM [" + csvFileName + "]"
Dim da As New System.Data.OleDb.OleDbDataAdapter(commText, con)

'DataTableに格納する
Dim dt As New DataTable
da.Fill(dt)
}}

#code(csharp){{
//CSVファイルのあるフォルダ
string csvDir = @"C:\";
//CSVファイルの名前
string csvFileName = "test.csv";

//接続文字列
string conString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source="
    + csvDir + ";Extended Properties=\"text;HDR=No;FMT=Delimited\"";
System.Data.OleDb.OleDbConnection con =
    new System.Data.OleDb.OleDbConnection(conString);

string commText = "SELECT * FROM [" + csvFileName + "]";
System.Data.OleDb.OleDbDataAdapter da =
    new System.Data.OleDb.OleDbDataAdapter(commText, con);

//DataTableに格納する
DataTable dt = new DataTable();
da.Fill(dt);
}}

次はODBC Provider(Microsoft Text Driver)を使った例です。.NET Framework 1.1以降で使用できます。なおこの場合は、一行目がヘッダとして処理されます。

#code(vbnet){{
'CSVファイルのあるフォルダ
Dim csvDir As String = "C:\"
'CSVファイルの名前
Dim csvFileName As String = "test.csv"

'接続文字列
Dim conString As String = _
    "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=" _
    + csvDir + ";Extensions=asc,csv,tab,txt;"
Dim con As New System.Data.Odbc.OdbcConnection(conString)

Dim commText As String = "SELECT * FROM [" + csvFileName + "]"
Dim da As New System.Data.Odbc.OdbcDataAdapter(commText, con)

'DataTableに格納する
Dim dt As New DataTable
da.Fill(dt)
}}

#code(csharp){{
//CSVファイルのあるフォルダ
string csvDir = @"C:\";
//CSVファイルの名前
string csvFileName = "test.csv";

//接続文字列
string conString = "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq="
    + csvDir + ";Extensions=asc,csv,tab,txt;";
System.Data.Odbc.OdbcConnection con =
    new System.Data.Odbc.OdbcConnection(conString);

string commText = "SELECT * FROM [" + csvFileName + "]";
System.Data.Odbc.OdbcDataAdapter da =
    new System.Data.Odbc.OdbcDataAdapter(commText, con);

//DataTableに格納する
DataTable dt = new DataTable();
da.Fill(dt);
}}

このような方法では、正しくCSVファイルが解析されるようにするには、Schema.iniを用意しておく必要があります。そうしないと、正しく解釈されない可能性が十分にあります。

-[[Schema.ini File (Text File Driver)>http://msdn.microsoft.com/library/en-us/odbc/htm/odbcjetschema_ini_file.asp]]

Schema.iniファイルの作成法については、次のサイトが参考になります。

-[[Importing CSV Data and saving it in database - The Code Project>http://www.codeproject.com/cs/database/FinalCSVReader.asp]]
-[[[AC97]VBAから Schema.ini ファイルを作成する方法>http://support.microsoft.com/kb/155512/ja]]

参考:

-[[ConnectionStrings.com>http://www.connectionstrings.com/]]

次に正規表現を使った方法を紹介します。「Perlメモ」の「CSV形式の行から値のリストを取り出す」や「値に改行コードを含む CSV形式を扱う」が参考になります。

-[[Perlメモ>http://www.din.or.jp/~ohzaki/perl.htm]]

これによると、CSVの一つの行(一つのレコード)からフィールドを取り出すには、レコードの最後にカンマをつけてから、

 ("(?:[^"]|"")*"|[^,]*),

というパターンを使用します。

また、フィールドに改行コードを含む場合にCSV形式の文字列から一つのレコードを取り出すには、まず一行を取り出してから、その文字列内の「"」の数を数え、奇数であれば次の一行を追加します。これを「"」の数が偶数になるまで繰り返し、偶数になったところで、一つのレコードが取り出せたものとします。

フィールドを取り出すためのパターンは、他にもいろいろ考えられます。例えば、一つのレコードからフィールドを取り出すためのパターンとしては、

 ,(?=([^\"]*"[^"]*")*(?![^"]*"))

や

 (?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)

などが以下のサイトで紹介されています。

-[[CSV Regex Pattern>http://blogs.worldnomads.com.au/matthewb/archive/2004/01/18/197.aspx]]
-[[RegEx for CSV>http://geekswithblogs.net/mwatson/archive/2004/09/04/10658.aspx]]

「Perlメモ」による方法を参考にしたサンプルを以下に紹介します。ここでは、先のCSV規則の3が適切に処理されるように、「Perlメモ」のパターンに手を加えています。また、一行取り出すためにも、正規表現を使っています。取り出したフィールドは、レコードごとにArrayListに格納し、これらをさらにArrayListに格納しています(実用的ではありませんが、あくまでCSVの解析のサンプルということで、ご理解ください)。CSVにはヘッダが無く、すべてのフィールドを文字列として取得します。CSV形式のファイルを解析する場合は、その内容をString型に読み込んでからメソッドを呼び出してください。

#code(vbnet){{
'/ <summary>
'/ CSVをArrayListに変換
'/ </summary>
'/ <param name="csvText">CSVの内容が入ったString</param>
'/ <returns>変換結果のArrayList</returns>
Public Shared Function CsvToArrayList1(ByVal csvText As String) _
    As System.Collections.ArrayList
    Dim csvRecords As New System.Collections.ArrayList

    '前後の改行を削除しておく
    csvText = csvText.Trim( _
        New Char() {ControlChars.Cr, ControlChars.Lf})

    '一行取り出すための正規表現
    Dim regLine As New System.Text.RegularExpressions.Regex( _
        "^.*(?:\n|$)", _
        System.Text.RegularExpressions.RegexOptions.Multiline)

    '1行のCSVから各フィールドを取得するための正規表現
    Dim regCsv As New System.Text.RegularExpressions.Regex( _
        "\s*(""(?:[^""]|"""")*""|[^,]*)\s*,", _
        System.Text.RegularExpressions.RegexOptions.None)

    Dim mLine As System.Text.RegularExpressions.Match = _
        regLine.Match(csvText)
    While mLine.Success
        '一行取り出す
        Dim line As String = mLine.Value
        '改行記号が"で囲まれているか調べる
        While CountString(line, """") Mod 2 = 1
            mLine = mLine.NextMatch()
            If Not mLine.Success Then
                Throw New ApplicationException("不正なCSV")
            End If
            line += mLine.Value
        End While
        '行の最後の改行記号を削除
        line = line.TrimEnd( _
            New Char() {ControlChars.Cr, ControlChars.Lf})
        '最後に「,」をつける
        line += ","

        '1つの行からフィールドを取り出す
        Dim csvFields As New System.Collections.ArrayList
        Dim m As System.Text.RegularExpressions.Match = _
            regCsv.Match(line)
        While m.Success
            Dim field As String = m.Groups(1).Value
            '前後の空白を削除
            field = field.Trim()
            '"で囲まれている時
            If field.StartsWith("""") And field.EndsWith("""") Then
                '前後の"を取る
                field = field.Substring(1, field.Length - 2)
                '「""」を「"」にする
                field = field.Replace("""""", """")
            End If
            csvFields.Add(field)
            m = m.NextMatch()
        End While

        csvFields.TrimToSize()
        csvRecords.Add(csvFields)

        mLine = mLine.NextMatch()
    End While

    csvRecords.TrimToSize()
    Return csvRecords
End Function

'/ <summary>
'/ 指定された文字列内にある文字列が幾つあるか数える
'/ </summary>
'/ <param name="strInput">strFindが幾つあるか数える文字列</param>
'/ <param name="strFind">数える文字列</param>
'/ <returns>strInput内にstrFindが幾つあったか</returns>
Public Shared Function CountString( _
    ByVal strInput As String, _
    ByVal strFind As String) As Integer
    Dim foundCount As Integer = 0
    Dim sPos As Integer = strInput.IndexOf(strFind)
    While sPos > -1
        foundCount += 1
        sPos = strInput.IndexOf(strFind, sPos + 1)
    End While

    Return foundCount
End Function
}}

#code(csharp){{
/// <summary>
/// CSVをArrayListに変換
/// </summary>
/// <param name="csvText">CSVの内容が入ったString</param>
/// <returns>変換結果のArrayList</returns>
public static System.Collections.ArrayList CsvToArrayList1(string csvText)
{
    System.Collections.ArrayList csvRecords =
        new System.Collections.ArrayList();

    //前後の改行を削除しておく
    csvText = csvText.Trim(new char[] {'\r', '\n'});

    //一行取り出すための正規表現
    System.Text.RegularExpressions.Regex regLine =
        new System.Text.RegularExpressions.Regex(
        "^.*(?:\\n|$)",
        System.Text.RegularExpressions.RegexOptions.Multiline);

    //1行のCSVから各フィールドを取得するための正規表現
    System.Text.RegularExpressions.Regex regCsv =
        new System.Text.RegularExpressions.Regex(
        "\\s*(\"(?:[^\"]|\"\")*\"|[^,]*)\\s*,",
        System.Text.RegularExpressions.RegexOptions.None);

    System.Text.RegularExpressions.Match mLine = regLine.Match(csvText);
    while (mLine.Success)
    {
        //一行取り出す
        string line = mLine.Value;
        //改行記号が"で囲まれているか調べる
        while ((CountString(line, "\"") % 2) == 1)
        {
            mLine = mLine.NextMatch();
            if (!mLine.Success)
            {
                throw new ApplicationException("不正なCSV");
            }
            line += mLine.Value;
        }
        //行の最後の改行記号を削除
        line = line.TrimEnd(new char[] {'\r', '\n'});
        //最後に「,」をつける
        line += ",";

        //1つの行からフィールドを取り出す
        System.Collections.ArrayList csvFields =
            new System.Collections.ArrayList();
        System.Text.RegularExpressions.Match m = regCsv.Match(line);
        while (m.Success)
        {
            string field = m.Groups[1].Value;
            //前後の空白を削除
            field = field.Trim();
            //"で囲まれている時
            if (field.StartsWith("\"") && field.EndsWith("\""))
            {
                //前後の"を取る
                field = field.Substring(1, field.Length - 2);
                //「""」を「"」にする
                field = field.Replace("\"\"", "\"");
            }
            csvFields.Add(field);
            m = m.NextMatch();
        }

        csvFields.TrimToSize();
        csvRecords.Add(csvFields);

        mLine = mLine.NextMatch();
    }

    csvRecords.TrimToSize();
    return csvRecords;
}

/// <summary>
/// 指定された文字列内にある文字列が幾つあるか数える
/// </summary>
/// <param name="strInput">strFindが幾つあるか数える文字列</param>
/// <param name="strFind">数える文字列</param>
/// <returns>strInput内にstrFindが幾つあったか</returns>
public static int CountString(string strInput, string strFind)
{
    int foundCount = 0;
    int sPos = strInput.IndexOf(strFind);
    while (sPos > -1)
    {
        foundCount++;
        sPos = strInput.IndexOf(strFind, sPos + 1);
    }

    return foundCount;
}
}}

最後に紹介するのは、文字列を独自に解析する方法です。面倒ですが、これが一番速いかもしれません。

以下にその例を示します。使い方は先のCsvToArrayList1メソッドと同じです。

(テストが十分でないため、間違いがあるかもしれません。不具合を発見された方は、ぜひご報告ください。)

#code(vbnet){{
'/ <summary>
'/ CSVをArrayListに変換
'/ </summary>
'/ <param name="csvText">CSVの内容が入ったString</param>
'/ <returns>変換結果のArrayList</returns>
Public Shared Function CsvToArrayList2(ByVal csvText As String) _
    As System.Collections.ArrayList
    '前後の改行を削除しておく
    csvText = csvText.Trim( _
        New Char() {ControlChars.Cr, ControlChars.Lf})

    Dim csvRecords As New System.Collections.ArrayList
    Dim csvFields As New System.Collections.ArrayList

    Dim csvTextLength As Integer = csvText.Length
    Dim startPos As Integer = 0
    Dim endPos As Integer = 0
    Dim field As String = ""

    While True
        '空白を飛ばす
        While startPos < csvTextLength _
            AndAlso (csvText.Chars(startPos) = " "c _
            OrElse csvText.Chars(startPos) = ControlChars.Tab)
            startPos += 1
        End While

        'データの最後の位置を取得
        If startPos < csvTextLength _
            AndAlso csvText.Chars(startPos) = ControlChars.Quote Then
            '"で囲まれているとき
            '最後の"を探す
            endPos = startPos
            While True
                endPos = csvText.IndexOf(ControlChars.Quote, endPos + 1)
                If endPos < 0 Then
                    Throw New ApplicationException("""が不正")
                End If
                '"が2つ続かない時は終了
                If endPos + 1 = csvTextLength OrElse _
                    csvText.Chars((endPos + 1)) <> ControlChars.Quote Then
                    Exit While
                End If
                '"が2つ続く
                endPos += 1
            End While

            '一つのフィールドを取り出す
            field = csvText.Substring(startPos, endPos - startPos + 1)
            '""を"にする
            field = field.Substring(1, field.Length - 2). _
                Replace("""""", """")

            endPos += 1
            '空白を飛ばす
            While endPos < csvTextLength AndAlso _
                csvText.Chars(endPos) <> ","c AndAlso _
                csvText.Chars(endPos) <> ControlChars.Lf
                endPos += 1
            End While
        Else
            '"で囲まれていない
            'カンマか改行の位置
            endPos = startPos
            While endPos < csvTextLength AndAlso _
                csvText.Chars(endPos) <> ","c AndAlso _
                csvText.Chars(endPos) <> ControlChars.Lf
                endPos += 1
            End While

            '一つのフィールドを取り出す
            field = csvText.Substring(startPos, endPos - startPos)
            '後の空白を削除
            field = field.TrimEnd()
        End If

        'フィールドの追加
        csvFields.Add(field)

        '行の終了か調べる
        If endPos >= csvTextLength OrElse _
            csvText.Chars(endPos) = ControlChars.Lf Then
            '行の終了
            'レコードの追加
            csvFields.TrimToSize()
            csvRecords.Add(csvFields)
            csvFields = New System.Collections.ArrayList( _
                csvFields.Count)

            If endPos >= csvTextLength Then
                '終了
                Exit While
            End If
        End If

        '次のデータの開始位置
        startPos = endPos + 1
    End While

    csvRecords.TrimToSize()
    Return csvRecords
End Function
}}

#code(csharp){{
/// <summary>
/// CSVをArrayListに変換
/// </summary>
/// <param name="csvText">CSVの内容が入ったString</param>
/// <returns>変換結果のArrayList</returns>
public static System.Collections.ArrayList CsvToArrayList2(string csvText)
{
    //前後の改行を削除しておく
    csvText = csvText.Trim(new char[] {'\r', '\n'});

    System.Collections.ArrayList csvRecords =
        new System.Collections.ArrayList();
    System.Collections.ArrayList csvFields =
        new System.Collections.ArrayList();

    int csvTextLength = csvText.Length;
    int startPos = 0, endPos = 0;
    string field = "";

    while (true)
    {
        //空白を飛ばす
        while (startPos < csvTextLength &&
            (csvText[startPos] == ' ' || csvText[startPos] == '\t'))
        {
            startPos++;
        }

        //データの最後の位置を取得
        if (startPos < csvTextLength && csvText[startPos] == '"')
        {
            //"で囲まれているとき
            //最後の"を探す
            endPos = startPos;
            while (true)
            {
                endPos = csvText.IndexOf('"', endPos + 1);
                if (endPos < 0)
                {
                    throw new ApplicationException("\"が不正");
                }
                //"が2つ続かない時は終了
                if (endPos + 1 == csvTextLength || csvText[endPos + 1] != '"')
                {
                    break;
                }
                //"が2つ続く
                endPos++;
            }

            //一つのフィールドを取り出す
            field = csvText.Substring(startPos, endPos - startPos + 1);
            //""を"にする
            field = field.Substring(1, field.Length - 2).Replace("\"\"", "\"");

            endPos++;
            //空白を飛ばす
            while (endPos < csvTextLength &&
                csvText[endPos] != ',' && csvText[endPos] != '\n')
            {
                endPos++;
            }
        }
        else
        {
            //"で囲まれていない
            //カンマか改行の位置
            endPos = startPos;
            while (endPos < csvTextLength &&
                csvText[endPos] != ',' && csvText[endPos] != '\n')
            {
                endPos++;
            }

            //一つのフィールドを取り出す
            field = csvText.Substring(startPos, endPos - startPos);
            //後の空白を削除
            field = field.TrimEnd();
        }

        //フィールドの追加
        csvFields.Add(field);

        //行の終了か調べる
        if (endPos >= csvTextLength || csvText[endPos] == '\n')
        {
            //行の終了
            //レコードの追加
            csvFields.TrimToSize();
            csvRecords.Add(csvFields);
            csvFields = new System.Collections.ArrayList(
                csvFields.Count);

            if (endPos >= csvTextLength)
            {
                //終了
                break;
            }
        }

        //次のデータの開始位置
        startPos = endPos + 1;
    }

    csvRecords.TrimToSize();
    return csvRecords;
}
}}

このようなコードを自分で書かなくても、すでに優秀なクラスが多く存在します。最後に、その内幾つかを以下に紹介しておきます。(上の2つが代表的なものです。)

-[[A Fast CSV Reader>http://www.codeproject.com/cs/database/CsvReader.asp]]
-[[XmlCsvReader Implementation>http://msdn.microsoft.com/library/en-us/dnxmlnet/html/xmlcsvreader.asp]]
-[[A portable and efficient generic parser for flat files>http://www.codeproject.com/cs/database/GenericParser.asp]]
-[[ASC2XXX - Two classes for parsing delimited text files | (正規表現を使用しているようです)>http://www.codeproject.com/cs/database/asc2xxx.asp]]

○この記事の基になった掲示板のスレッド

-[[CSVファイルの総行数を取得したいです。 | 投稿者(敬称略) こつ, 小野@どっとねっとふぁん, こど。, 壱丸3, じゃんぬねっと>http://dobon.net/vb/bbs/log3-16/9785.html]]
-[[CSV箕荷 | 投稿者(敬称略) kenta, 岡田 之仁, sas>http://dobon.net/vb/bbs/log3-7/4050.html]]
-[[CSVファイルをデータグリットで表示 | 投稿者(敬称略) A, nepia>http://dobon.net/vb/bbs/log3-8/4876.html]]
-[[CSVファイルについて | 投稿者(敬称略) Boo, 岡田 之仁>http://dobon.net/vb/bbs/log3-12/6599.html]]
-[[CSVデータをODBCで削除するには? | 投稿者(敬称略) おすぎ, 深山>http://dobon.net/vb/bbs/log3-13/7393.html]]
-[[テキストファイルを実行ファイルに取り込んで扱う方法 | 投稿者(敬称略) だまさい, Mike>http://dobon.net/vb/bbs/log3-3/1430.html]]
-[[ArrayListの使い方 | 投稿者(敬称略) kyoro, Blue, 中博俊>http://dobon.net/vb/bbs/log3-18/10598.html]]
-[[多次元配列(二次元配列)の初期化について | 投稿者(敬称略) HOGE, java.lang.Nullpo>http://dobon.net/vb/bbs/log3-9/5298.html]]
-[[Split | 投稿者(敬称略) SOMY, まどか, なおこ(・∀・)>http://dobon.net/vb/bbs/log3-21/13151.html]]
-[[CSVファイルの総行数を取得したいです。 | 投稿者(敬称略) こつ, 小野@どっとねっとふぁん, こど。, 壱丸3, じゃんぬねっと>https://dobon.net/vb/bbs/log3-16/9785.html]]
-[[CSV箕荷 | 投稿者(敬称略) kenta, 岡田 之仁, sas>https://dobon.net/vb/bbs/log3-7/4050.html]]
-[[CSVファイルをデータグリットで表示 | 投稿者(敬称略) A, nepia>https://dobon.net/vb/bbs/log3-8/4876.html]]
-[[CSVファイルについて | 投稿者(敬称略) Boo, 岡田 之仁>https://dobon.net/vb/bbs/log3-12/6599.html]]
-[[CSVデータをODBCで削除するには? | 投稿者(敬称略) おすぎ, 深山>https://dobon.net/vb/bbs/log3-13/7393.html]]
-[[テキストファイルを実行ファイルに取り込んで扱う方法 | 投稿者(敬称略) だまさい, Mike>https://dobon.net/vb/bbs/log3-3/1430.html]]
-[[ArrayListの使い方 | 投稿者(敬称略) kyoro, Blue, 中博俊>https://dobon.net/vb/bbs/log3-18/10598.html]]
-[[多次元配列(二次元配列)の初期化について | 投稿者(敬称略) HOGE, java.lang.Nullpo>https://dobon.net/vb/bbs/log3-9/5298.html]]
-[[Split | 投稿者(敬称略) SOMY, まどか, なおこ(・∀・)>https://dobon.net/vb/bbs/log3-21/13151.html]]

***DataTableや配列等をCSV形式のファイルとして保存する [#f946f51f]

#column(注意){{
この記事の最新版は「[[DataTableや配列等をCSV形式のファイルとして保存する>http://dobon.net/vb/dotnet/file/writecsvfile.html]]」で公開しています。
この記事の最新版は「[[DataTableや配列等をCSV形式のファイルとして保存する>https://dobon.net/vb/dotnet/file/writecsvfile.html]]」で公開しています。
}}

''【質問】''

DataTableや配列をCSV形式のファイルとして保存するにはどのようにすればよいでしょうか?

【解答】
ここでもCSV形式の規則は、先ほど紹介したものと同じとして、話を進めます。つまり、4,6の規則に従い、フィールドにカンマ、改行文字が含まれる場合は、ダブルクォートで囲みます。また5の規則に従い、フィールドにダブルクォートが含まれる場合は、これを2つのダブルクォートに置換して、ダブルクォートで囲みます。さらに規則7に従い、フィールドの前後にスペースがある場合も、ダブルクォートで囲みます。

このような方針により、DataTableをCSV形式のファイルに保存する例を以下に示します。フィールドの型を考慮せず、単純にToStringメソッドで文字列にして保存しています。ヘッダも書き込んでいますが、ヘッダの書き込みと、レコードの書き込みが全く同じコードになっており、コードとしてはあまり良くありません。あくまでサンプルということで、ご了承ください。

またこの例では、いちいち一つ一つのフィールドを調べてダブルクォートで囲むか調べていますが、これが面倒であれば、すべてのフィールドをダブルクォートで囲むようにしても結構です。

#code(vbnet){{
'CSVで保存するDataTable
Dim dt As DataTable = CType(DataGrid1.DataSource, DataTable)
'保存先のCSVファイルのパス
Dim csvPath As String = "C:\test1.csv"
'CSVファイルに書き込むときに使うEncoding
Dim enc As System.Text.Encoding = _
    System.Text.Encoding.GetEncoding("Shift_JIS")

'開く
Dim sr As New System.IO.StreamWriter(csvPath, False, enc)

Dim colCount As Integer = dt.Columns.Count
Dim lastColIndex As Integer = colCount - 1

'ヘッダを書き込む
Dim i As Integer
For i = 0 To colCount - 1
    'ヘッダの取得
    Dim field As String = dt.Columns(i).Caption
    '"で囲む必要があるか調べる
    If field.IndexOf(ControlChars.Quote) > -1 OrElse _
        field.IndexOf(","c) > -1 OrElse _
        field.IndexOf(ControlChars.Cr) > -1 OrElse _
        field.IndexOf(ControlChars.Lf) > -1 OrElse _
        field.StartsWith(" ") OrElse _
        field.StartsWith(ControlChars.Tab) OrElse _
        field.EndsWith(" ") OrElse _
        field.EndsWith(ControlChars.Tab) Then
        If field.IndexOf(ControlChars.Quote) > -1 Then
            '"を""とする
            field = field.Replace("""", """""")
        End If
        field = """" + field + """"
    End If
    'フィールドを書き込む
    sr.Write(field)
    'カンマを書き込む
    If lastColIndex > i Then
        sr.Write(","c)
    End If
Next i
'改行する
sr.Write(ControlChars.Cr + ControlChars.Lf)

'レコードを書き込む
Dim row As DataRow
For Each row In dt.Rows
    For i = 0 To colCount - 1
        'フィールドの取得
        Dim field As String = row(i).ToString()
        '"で囲む必要があるか調べる
        If field.IndexOf(ControlChars.Quote) > -1 OrElse _
            field.IndexOf(","c) > -1 OrElse _
            field.IndexOf(ControlChars.Cr) > -1 OrElse _
            field.IndexOf(ControlChars.Lf) > -1 OrElse _
            field.StartsWith(" ") OrElse _
            field.StartsWith(ControlChars.Tab) OrElse _
            field.EndsWith(" ") OrElse _
            field.EndsWith(ControlChars.Tab) Then
            If field.IndexOf(ControlChars.Quote) > -1 Then
                '"を""とする
                field = field.Replace("""", """""")
            End If
            field = """" + field + """"
        End If
        'フィールドを書き込む
        sr.Write(field)
        'カンマを書き込む
        If lastColIndex > i Then
            sr.Write(","c)
        End If
    Next i
    '改行する
    sr.Write(ControlChars.Cr + ControlChars.Lf)
Next row

'閉じる
sr.Close()
}}

#code(csharp){{
//CSVで保存するDataTable
DataTable dt = (DataTable) dataGrid1.DataSource;
//保存先のCSVファイルのパス
string csvPath = "C:\\test1.csv";
//CSVファイルに書き込むときに使うEncoding
System.Text.Encoding enc =
    System.Text.Encoding.GetEncoding("Shift_JIS");

//開く
System.IO.StreamWriter sr =
    new System.IO.StreamWriter(csvPath, false, enc);

int colCount = dt.Columns.Count;
int lastColIndex = colCount - 1;

//ヘッダを書き込む
for (int i = 0; i < colCount; i++)
{
    //ヘッダの取得
    string field = dt.Columns[i].Caption;
    //"で囲む必要があるか調べる
    if (field.IndexOf('"') > -1 ||
        field.IndexOf(',') > -1 ||
        field.IndexOf('\r') > -1 ||
        field.IndexOf('\n') > -1 ||
        field.StartsWith(" ") || field.StartsWith("\t") ||
        field.EndsWith(" ") || field.EndsWith("\t"))
    {
        if (field.IndexOf('"') > -1)
        {
            //"を""とする
            field = field.Replace("\"", "\"\"");
        }
        field = "\"" + field + "\"";
    }
    //フィールドを書き込む
    sr.Write(field);
    //カンマを書き込む
    if (lastColIndex > i)
    {
        sr.Write(',');
    }
}
//改行する
sr.Write("\r\n");

//レコードを書き込む
foreach (DataRow row in dt.Rows)
{
    for (int i = 0; i < colCount; i++)
    {
        //フィールドの取得
        string field = row[i].ToString();
        //"で囲む必要があるか調べる
        if (field.IndexOf('"') > -1 ||
            field.IndexOf(',') > -1 ||
            field.IndexOf('\r') > -1 ||
            field.IndexOf('\n') > -1 ||
            field.StartsWith(" ") || field.StartsWith("\t") ||
            field.EndsWith(" ") || field.EndsWith("\t"))
        {
            if (field.IndexOf('"') > -1)
            {
                //"を""とする
                field = field.Replace("\"", "\"\"");
            }
            field = "\"" + field + "\"";
        }
        //フィールドを書き込む
        sr.Write(field);
        //カンマを書き込む
        if (lastColIndex > i)
        {
            sr.Write(',');
        }
    }
    //改行する
    sr.Write("\r\n");
}

//閉じる
sr.Close();
}}

このような方法以外に、掲示板では、XSLTを使ってCSVに変換する方法が紹介されています。この方法については、記事の最後にある掲示板の過去ログへのリンク(「DatasetのデータをCSV保存する」)や、次のリンク先をご覧ください。

-[[XSLT to transform Excel XML spreadsheet to CSV or HTML table - The Code Project - SOAP and XML>http://www.codeproject.com/soap/xml_spreadsheet_to_csv.asp]]
-[[XSLT to Convert Dataset XML to CSV>http://groups.google.co.jp/group/microsoft.public.dotnet.framework.adonet/msg/8fa7f8b03000fe26?hl=ja&]]

私個人の意見としましては、このような方法では上記に示したCSVの規則に従うファイルを出力するのが難しく、柔軟性にも乏しいため、お勧めはできません。

○この記事の基になった掲示板のスレッド

-[[csvファイルの読み書き | 投稿者(敬称略) KIRIRI, なおこ(・∀・), trapemiya, じゃんぬねっと>http://dobon.net/vb/bbs/log3-21/12803.html]]
-[[DatasetのデータをCSV保存する | 投稿者(敬称略) ほげほげ, 中 博俊, えムナウ>http://dobon.net/vb/bbs/log3-14/8300.html]]
-[[ADO.NETからの出力結果のカラム名を取り出すには? | 投稿者(敬称略) あみっど>http://dobon.net/vb/bbs/log2/780.html]]
-[[csvファイルの読み書き | 投稿者(敬称略) KIRIRI, なおこ(・∀・), trapemiya, じゃんぬねっと>https://dobon.net/vb/bbs/log3-21/12803.html]]
-[[DatasetのデータをCSV保存する | 投稿者(敬称略) ほげほげ, 中 博俊, えムナウ>https://dobon.net/vb/bbs/log3-14/8300.html]]
-[[ADO.NETからの出力結果のカラム名を取り出すには? | 投稿者(敬称略) あみっど>https://dobon.net/vb/bbs/log2/780.html]]

**コメント [#nd32bb27]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2006-02-19 (日) 18:00:00,DOBON!,2010-03-22 (月) 03:08:38,DOBON!)


[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]