CSV(Comma Separated Value)形式のファイルをDataTableや配列として読み込む方法を幾つか紹介します。ここでは3つの方法を紹介しますが、その前にCSVとはなにかについて確認しておきます。
CSV形式について、絶対的な決まりは存在していないようです。ただし、一般的なアプリケーションで使われている決まりについては、「CSV Comma Separated Value File Format (How To)」で説明されています。
要約しますと、次のようになります。
ここでは、ここのような決まりに準じて、話を進めることにします。
補足:2005年10月にCSVの仕様書RFC4180が出されました。しかしこの仕様書は後追いで、CategoryがInformationalです。現状では先に紹介した決まりの方が多くのアプリケーションに適応しており、ここでは先に紹介した仕様に沿って説明します。
CSV形式のファイルを配列として取得する方法としてよく使われるのが、CSVファイルから一行ずつ読み出し、その一行をString.Splitメソッドを使って「,」文字で分割するという方法です。しかしこの方法は、上記の規則の4,6を見れば、正しくないことが分かります。つまり、フィールドにカンマや改行文字が含まれている可能性があるのです。よってこのような方法は、フィールドにカンマや改行文字が含まれていないことが保障されたCSVにしか使用できません。そうでないCSVを扱うためには、より複雑な処理が必要になります。
ここから本題に入ります。まずは、Jet ProviderやODBC Providerを使う方法を紹介します。これらを使って、CSVファイルを解析することができます。
以下にJet Providerを使った例を紹介します。ここでは解析されたCSVファイルの内容をDataTableに格納しています。CSVファイルの文字コードは、Shift JISである必要があります。なお、接続文字列の「HDR=No」を「HDR=Yes」とすることにより、一行目をヘッダとすることができます。
'CSVファイルのあるフォルダ Dim csvDir As String = "C:\test\" '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)
//CSVファイルのあるフォルダ string csvDir = @"C:\test\"; //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以降で使用できます。なおこの場合は、一行目がヘッダとして処理されます。
'CSVファイルのあるフォルダ Dim csvDir As String = "C:\test\" '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)
//CSVファイルのあるフォルダ string csvDir = @"C:\test\"; //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 Fileを用意しておく必要があります。そうしないと、正しく解釈されない可能性が十分にあります。
Schema.iniファイルの作成法については、「Importing CSV Data and saving it in database」や「[AC97]VBAから Schema.ini ファイルを作成する方法」が参考になります。
次に正規表現を使った方法を紹介します。「Perlメモ」の「CSV形式の行から値のリストを取り出す」や「値に改行コードを含むCSV形式を扱う」が参考になります。 これによると、CSVの一つの行(一つのレコード)からフィールドを取り出すには、レコードの最後にカンマをつけてから、
("(?:[^"]|"")*"|[^,]*),
というパターンを使用します。
また、フィールドに改行コードを含む場合にCSV形式の文字列から一つのレコードを取り出すには、まず一行を取り出してから、その文字列内の「"」の数を数え、奇数であれば次の一行を追加します。これを「"」の数が偶数になるまで繰り返し、偶数になったところで、一つのレコードが取り出せたものとします。
フィールドを取り出すためのパターンは、他にもいろいろ考えられます。例えば、一つのレコードからフィールドを取り出すためのパターンとしては、
,(?=([^\"]*"[^"]*")*(?![^"]*"))
や
(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)
などが「RegEx for CSV」等で紹介されています。
「Perlメモ」による方法を参考にしたサンプルを以下に紹介します。ここでは、先のCSV規則の3が適切に処理されるように、「Perlメモ」のパターンに手を加えています。また、一行取り出すためにも、正規表現を使っています。取り出したフィールドは、レコードごとにArrayListに格納し、これらをさらにArrayListに格納しています(実用的ではありませんが、あくまでCSVの解析のサンプルということで、ご理解ください)。CSVにはヘッダが無く、すべてのフィールドを文字列として取得します。CSV形式のファイルを解析する場合は、その内容をString型に読み込んでからメソッドを呼び出してください。
''' <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
/// <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メソッドと同じです。
補足:テストが十分でないため、間違いがあるかもしれません。不具合を発見された方は、ぜひご報告ください。
''' <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
/// <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つが代表的なものです。)
.NET Framework 2.0からは、VB.NET用のクラスとして、TextFieldParserクラスが追加されました。これを使えば、CSVファイルの解析も楽になります。
以下に例を示します。文字コードを指定しているEncodingに関しては、「文字コードを指定してテキストファイルを読み込む」を参考にしてください。C#では参照に「Microsoft.VisualBasic.dll」を追加する必要があります。
Dim csvRecords As New System.Collections.ArrayList() 'CSVファイル名 Dim csvFileName As String = "C:\test.csv" 'Shift JISで読み込む Dim tfp As New FileIO.TextFieldParser(csvFileName, _ System.Text.Encoding.GetEncoding(932)) 'フィールドが文字で区切られているとする 'デフォルトでDelimitedなので、必要なし tfp.TextFieldType = FileIO.FieldType.Delimited '区切り文字を,とする tfp.Delimiters = New String() {","} 'フィールドを"で囲み、改行文字、区切り文字を含めることができるか 'デフォルトでtrueなので、必要なし tfp.HasFieldsEnclosedInQuotes = True 'フィールドの前後からスペースを削除する 'デフォルトでtrueなので、必要なし tfp.TrimWhiteSpace = True While Not tfp.EndOfData 'フィールドを読み込む Dim fields As String() = tfp.ReadFields() '保存 csvRecords.Add(fields) End While '後始末 tfp.Close()
System.Collections.ArrayList csvRecords = new System.Collections.ArrayList(); //CSVファイル名 string csvFileName = "C:\\test.csv"; //Shift JISで読み込む Microsoft.VisualBasic.FileIO.TextFieldParser tfp = new Microsoft.VisualBasic.FileIO.TextFieldParser( csvFileName, System.Text.Encoding.GetEncoding(932)); //フィールドが文字で区切られているとする //デフォルトでDelimitedなので、必要なし tfp.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited; //区切り文字を,とする tfp.Delimiters = new string[] { "," }; //フィールドを"で囲み、改行文字、区切り文字を含めることができるか //デフォルトでtrueなので、必要なし tfp.HasFieldsEnclosedInQuotes = true; //フィールドの前後からスペースを削除する //デフォルトでtrueなので、必要なし tfp.TrimWhiteSpace = true; while (!tfp.EndOfData) { //フィールドを読み込む string[] fields = tfp.ReadFields(); //保存 csvRecords.Add(fields); } //後始末 tfp.Close();
TextFieldParser.TrimWhiteSpaceプロパティはフィールドの前後のスペースを削除するものですが、ダブルクォートで囲まれていた場合でも、前後のスペースを削除します。また、TrimWhiteSpaceの削除するスペースは、スペース文字の他に、タブや改行文字も含まれるようです。このような点から、TextFieldParserクラスを使って先に示したルールに基づいた解析は、無理のようです。(RFC4180に基づくならば、TrimWhiteSpaceをfalseにすればよいかもしれませんが。)
(この記事は、「.NETプログラミング研究」で紹介したものを基にしています。)