Excel 形式の CSV 形式読み込みの留意点を少し

  • 表の行に改行文字が含まれることを意識せよ
  • Culture を意識せよ、char 型禁止

表の行に改行文字が含まれることを意識せよ

a,"b
c",d
e,f,g

というデータは2行3列の

a b
c
d
e f g

という形式になる(このスタイルの表だとみにくいね・・・)

Culture を意識せよ、char 型禁止

文字列を特定の文字で分割する場合、Culture を意識しないととんでもないところで分割される場合があります。たとえば、cp874 で保存されたデータを InvariantCulture や ja-JP で読み込んでしまうと、意図しない @"\""" に遭遇する可能性があります。これは cp874 や cp932 といった MBCS が Leading-byte と Trailing-byte で構成されており、Trailing-byte に '\' を含むためですが、TextReader 等を利用して気楽に非 Unicode 文字列を読み込んでいる場合に引っかかりやすい問題なので注意しなければなりません。

読み込み終わった文字列を文字に分割するには TextElementEnumerator を使用します。これは CSV に限らず、文字列を文字単位で操作する際のルールだと思ってもよいと思われます。*1
char 型は ucs2 の1ユニットを示す型であって、Unicode の1文字を表現するには十分なサイズではありません。このため、組み合わせ文字やサロゲートペアといった2〜4ユニットを使用する文字を意識する必要があります。文字を単位とする分割は TextElementEnumerator に実装されているので、我々は TextElementEnumerator を利用すれば具体的な Unicode に関する知識は必要ありません。
『行』を保持する文字列から『列』を1つづつ取り出すには、

public IEnumerable<string> GetColumns(string singleLine)
{
  object quoteState = null;
  StringBuilder columnBuilder = new StringBuilder();

  TextElementEnumerator e = StringInfo.GetTextElementEnumerator(singleLine);
  while (e.MoveNext()
  {
    string c = e.GetTextElement();

    // カラムの先頭でのみ OpenBracket の検出を行う
    if ((columnBuilder.Length == 0) && IsOpenBracket(c, out quoteState))
    {
      continue;
    }

    if (quoteState != null)
    {
      // OpenBracket の内側でのみ、CloseBracket の検出を行う
      if (IsCloseBracket(c, quoteState))
      {
        quoteState = null;
        continue;
      }
    }
    else if (IsColumnSeparator(c))
    {
       // Bracket の外側でのみ、ColumnSeparator の検出を行う
       yield return columnBuilder.ToString();
       columnBuilder.Length = 0;
       continue;
    }

    columnBuilder.Append(c);
  }

  return columnBuilder.ToString();
}

のようなかんじになります。*2ただ、上記程度であれば Regex クラスを使ったほうがかなり高速になります。Regex クラスは RegexOptions.CultureInvariant を指定すると Culture に依存しなくなるので、組み合わせ文字を含む場合には RegexOptions.CultureInvariant を指定してはなりません。

まあ、Excel からデータ取り込むなら

素直に XML Sheet 形式で保存して XmlDocument に読み込んで Select("//Workbook/Worksheet[Name=Sheet1]/ss:Data") したほうがいい。

*1:Java にも似たようなクラスありましたね

*2:このコードはエスケープ処理を省略していますが…