C#
CsvReader
using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
public class CsvReader : IDisposable
{
private const int ReadBufferSize = 256;
private char[] ReadBuffer;
private int ReadCount;
private int ReadBufferIndex;
private StringBuilder FieldBuffer;
private char PrevChar;
private TextReader SrcReader;
public StringCollection Fields { get; private set; }
public long RecordIndex { get; private set; }
public CsvReader(TextReader Reader)
{
if (Reader == null)
{
throw new ArgumentNullException("Reader");
}
ReadBuffer = new char[ReadBufferSize];
ReadCount = 0;
ReadBufferIndex = 0;
FieldBuffer = new StringBuilder();
PrevChar = '\0';
Fields = new StringCollection();
RecordIndex = -1;
SrcReader = Reader;
}
public void Dispose()
{
if (ReadBuffer != null)
{
ReadBuffer = null;
}
ReadCount = 0;
ReadBufferIndex = 0;
if (FieldBuffer != null)
{
FieldBuffer.Length = 0;
FieldBuffer = null;
}
PrevChar = '\0';
if (Fields != null)
{
Fields.Clear();
Fields = null;
}
RecordIndex = -1;
if (SrcReader != null)
{
SrcReader.Dispose();
SrcReader = null;
}
}
public bool ReadNext()
{
bool IsFileEnd = false;
bool IsRecordEnd = false;
bool HasAnyValue = false;
Fields.Clear();
while (!IsRecordEnd)
{
bool IsFieldEnd = false;
bool IsQuoted = false;
bool IsQuotedField = false;
FieldBuffer.Length = 0;
while (!IsFieldEnd)
{
if (ReadBufferIndex >= ReadCount)
{
ReadCount = SrcReader.ReadBlock(ReadBuffer, 0, ReadBufferSize);
ReadBufferIndex = 0;
if (ReadCount == 0)
{
IsFieldEnd = true;
IsRecordEnd = true;
IsFileEnd = true;
if (IsQuoted)
{
FieldBuffer.Replace("\"", "\"\"");
FieldBuffer.Insert(0, '\"');
}
break;
}
}
char CurChar = ReadBuffer[ReadBufferIndex++];
switch (CurChar)
{
case '\r':
if (IsQuoted)
{
FieldBuffer.Append(CurChar);
}
else
{
IsFieldEnd = true;
IsRecordEnd = true;
}
break;
case '\n':
if (IsQuoted)
{
FieldBuffer.Append(CurChar);
}
else if (PrevChar != '\r')
{
IsFieldEnd = true;
IsRecordEnd = true;
}
break;
case '\"':
HasAnyValue = true;
if (IsQuoted)
{
IsQuoted = false;
}
else if (IsQuotedField)
{
FieldBuffer.Append(CurChar);
IsQuoted = true;
}
else if (FieldBuffer.Length == 0)
{
IsQuoted = true;
IsQuotedField = true;
}
else
{
FieldBuffer.Append(CurChar);
}
break;
case ',':
HasAnyValue = true;
if (IsQuoted)
{
FieldBuffer.Append(CurChar);
}
else
{
IsFieldEnd = true;
}
break;
default:
HasAnyValue = true;
if (!IsQuoted && IsQuotedField)
{
IsQuotedField = false;
FieldBuffer.Replace("\"", "\"\"");
FieldBuffer.Insert(0, '\"');
FieldBuffer.Append('\"');
}
FieldBuffer.Append(CurChar);
break;
}
PrevChar = CurChar;
}
Fields.Add(FieldBuffer.ToString());
}
if (!IsFileEnd || HasAnyValue)
{
RecordIndex++;
return true;
}
PrevChar = '\0';
Fields.Clear();
RecordIndex = -1;
return false;
}
}
CSVの一般的なルール
CSVに規格のようなものはなかったが、いつの間にかRFC 4180にまとめられていた。ただ、仕様ではないらしい。CSVには方言しかないので、それらに共通するルールをまとめたものだと思う。
簡単に書くと、だいたいこんな感じ。
- ファイルは、改行でレコードに区切られる。
- レコードは、カンマでフィールドに区切られる。
- レコードに含まれるフィールドの数は、すべてのレコードで同じでなければならない。
- フィールドを二重引用符で囲んでも良い。
二重引用符で囲まないフィールドに、二重引用符を含めてはいけない。
二重引用符で囲むフィールドには、二重引用符、カンマ、改行を含めても良く、その場合、含める二重引用符の直前にひとつの二重引用符を挿入する。
- 先頭のレコードをヘッダとすることがある。
実装の留意事項
CSVは現役
CSVは相当古い形式だし、XMLなど新しいフォーマットもあるので、とっくにすたれてるだろうと思いきや、いまでもときどき使うことがある。
比較的小さなデータならXMLのほうが便利だが、XMLはファイルをすべて読み取ってからでないと次の処理ができないため、大きなデータを扱うには向かない。
CSVなら1レコードずつデータを読み取りながら処理できるので、大きなデータを扱うときにはCSVを選択することが多い。誰でも知っている一般的な形式であることも理由のひとつ。
例えばデータ移行のとき。データベース同士を直接つなげられれば簡単だが、そのようなことはセキュリティ的に許されないだろう。データベースからテーブルの内容をCSVで取り出し、別のデータベースに登録するのが普通だと思うが、そのファイルはMB単位とか、あるいはそれ以上の大きさになり、テキストエディタで開くのさえ難しいことがある。
その他
- CSVを読み取るプログラムは、使いまわせるし、役に立つので、ひとつ作っておいたほうが良い。
- ファイルの内容を一括で読み取ってから、改行でレコードに分割し、さらにカンマでフィールドに分割するようなやり方では、CSVを正しく読み取るのは難しいと思う。予めそうできるように取り決めておかない限り、うまくいかない。
- 二重引用符で囲まれたフィールドは文字列であり、そうでないフィールドはintなどの値であるといったルールもある。それは独自のルール、方言のひとつであって、「1フィールド目はintに変換してください」というようなことと同じだと思う。
CSVはテキストなので、すべてのフィールドは文字列であると考えたほうが理解しやすい。
サンプルコード
public static int[] MeasureCsvFieldLengths(string CsvFilePath, Encoding CharSet)
{
int[] FieldLengths = null;
using (FileStream InputStream = File.Open(CsvFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (StreamReader SrcReader = new StreamReader(InputStream, CharSet))
{
using (CsvReader Reader = new CsvReader(SrcReader))
{
if (Reader.ReadNext())
{
FieldLengths = new int[Reader.Fields.Count];
for (int i = 0; i < Reader.Fields.Count; i++)
{
FieldLengths[i] = Reader.Fields[i].Length;
}
while (Reader.ReadNext())
{
if (Reader.Fields.Count != FieldLengths.Length)
{
throw new Exception("フィールド数が異なります。レコード=" + Reader.RecordIndex.ToString());
}
for (int i = 0; i < Reader.Fields.Count; i++)
{
if (FieldLengths[i] < Reader.Fields[i].Length)
{
FieldLengths[i] = Reader.Fields[i].Length;
}
}
}
}
}
}
}
return FieldLengths;
}
public static void CsvToDatabase(string CsvFilePath, Encoding CharSet, string ConnectionString, string NewTableName)
{
int[] FieldLengths = MeasureCsvFieldLengths(CsvFilePath, CharSet);
if (FieldLengths == null)
{
return;
}
int FieldCount = FieldLengths.Length;
string[] ColumnNames = new string[FieldCount];
SqlParameter[] Parameters = new SqlParameter[FieldCount + 1];
StringBuilder Builder = new StringBuilder();
Builder.Append("CREATE TABLE ");
Builder.Append(NewTableName);
Builder.AppendLine(" (");
Builder.AppendLine(" N BIGINT NOT NULL,");
Parameters[0] = new SqlParameter("@N", SqlDbType.BigInt);
for (int i = 0; i < FieldCount; i++)
{
ColumnNames[i] = "C" + i.ToString();
Builder.Append(" ");
Builder.Append(ColumnNames[i]);
Builder.Append(" NVARCHAR(");
Builder.Append(FieldLengths[i]);
Builder.AppendLine(") NOT NULL,");
Parameters[i + 1] = new SqlParameter("@" + ColumnNames[i], SqlDbType.NVarChar, FieldLengths[i]);
}
Builder.AppendLine(" PRIMARY KEY (N))");
string CreateCommandText = Builder.ToString();
Builder.Length = 0;
Builder.Append("INSERT INTO ");
Builder.Append(NewTableName);
Builder.Append(" (N");
for (int i = 0; i < FieldCount; i++)
{
Builder.Append(", ");
Builder.Append(ColumnNames[i]);
}
Builder.Append(") VALUES (");
Builder.Append(Parameters[0].ParameterName);
for (int i = 1; i <= FieldCount; i++)
{
Builder.Append(", ");
Builder.Append(Parameters[i].ParameterName);
}
Builder.Append(")");
string InsertCommandText = Builder.ToString();
using (SqlConnection Connection = new SqlConnection(ConnectionString))
{
Connection.Open();
using (SqlTransaction Transaction = Connection.BeginTransaction())
{
using (SqlCommand Command = new SqlCommand(CreateCommandText, Connection, Transaction))
{
Command.ExecuteNonQuery();
}
using (FileStream InputStream = File.Open(CsvFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (StreamReader SrcReader = new StreamReader(InputStream, CharSet))
{
using (CsvReader Reader = new CsvReader(SrcReader))
{
while (Reader.ReadNext())
{
Parameters[0].Value = Reader.RecordIndex;
for (int i = 0; i < FieldCount; i++)
{
Parameters[i + 1].Value = Reader.Fields[i];
}
using (SqlCommand Command = new SqlCommand(InsertCommandText, Connection, Transaction))
{
Command.Parameters.AddRange(Parameters);
Command.ExecuteNonQuery();
Command.Parameters.Clear();
}
}
}
}
}
Transaction.Commit();
}
}
}
(2020/12/21 初稿)
(2020/12/22 更新)
(2021/01/15 更新)