C#でCSV読む
DOBONをうろついていたら久しぶりに目から鱗が落ちたですよ
CSV形式のファイルをDataTableや配列等として取得する: .NET Tips: C#, VB.NET
「改行混じり」CSV、行中のダブルクオートが偶数になるまで行結合を繰り返すだけで読めるってよ
…。
はえ?
…。
ほ、本当だーッ!?
CSVって基本的にはセルをカンマで区切り、ROWはそのまんま改行で表現する書式。なんですが、意外と面倒くさいのはダブルクオーテーションで囲むと、区切り文字のカンマのみならず改行文字までセル内に含ませることが出来る仕様。どうしたものか。しかもダブルクオーテーション自体のエスケープにも対応しなくちゃいけない。しかし、しかしである。
ミソは
- 制御文字がカンマとダブルクオートしかない
- カンマはダブルクオーテーションで囲ってエスケープ
- ダブルクオーテーションは2連続でエスケープ
だからダブルクオートが偶数になるまで行結合するだけ*1、妙案…。ちなみに2連続ダブルクオートにも対応したスプリット用の正規表現が次のパターンだそう。
("(?:[^"]|"")*"|[^,]*),
(ただし行末にカンマを一つ追加しないと最後の要素が取得できない*2)
しょえー。読込だけならライブラリとか持ってくる必要もないのか。では早速、CSV形式のテキストストリームをシーケンシャルリードしちゃうクラス書いてみます。
using System; using System.Collections.Generic; public class CSVReader { /// <summary>先頭末尾のダブルクオーテーションを切除します。標準の動作です。</summary> public bool trimDQuot; /// <summary>エスケープされているダブルクオーテーション(二重になっているダブルクーテーション)を1つに戻します。標準の動作です。</summary> public bool formatEscapedDQuot; /// <summary>コンストラクタ</summary> /// <param name="reader">読込むCSVデータ。StringReaderかStreamReaderを想定。</param> public CSVReader(System.IO.TextReader reader) { this.reader = reader; trimDQuot = true; formatEscapedDQuot = true; } /// <summary>CSVから一行を読んで返します。</summary> /// <returns>行にデータがあれば、1行分のセルの配列をstring[]として返します。セルが一つもない空行の場合は空の配列が返ります。EOFに達するとNULLが返ります。ダブルクオーテーションの処理はtrimDQuotメンバとformatEscapedDQuotメンバを参照してください。</returns> public string[] ReadLine() { int ctQuot = 0; string mergedLine = null; while (-1 < reader.Peek()) { // ダブルクオーテーションの数が偶数になるまでReadLineをマージしていけば改行交じりを処理できる。 string getLine = reader.ReadLine(); // 連結 if (mergedLine == null) mergedLine = getLine; else mergedLine += (System.Environment.NewLine + getLine); // クオーテーションカウント ctQuot += (getLine.Length - getLine.Replace("\"", string.Empty).Length); if ((ctQuot % 2) == 0) // 偶数ブレイク判定 { ctQuot = 0; // カウンタ初期化(しなくても良いっちゃ良い) break; } } if (mergedLine == null) return null; else if (mergedLine.Length == 0) return new string[0]; List<string> split = new List<string>(); foreach (System.Text.RegularExpressions.Match match in regexSplitter.Matches(mergedLine + ',')) { string item = match.Value; split.Add(item.Substring(0, item.Length - 1)); // remove comma } if (trimDQuot) { for (int index = 0; index < split.Count; index++) { string item = split[index]; if ((0 < item.Length) && (item[0] == '"') && (item[item.Length - 1] == '"')) { split[index] = item.Substring(1, Math.Max(0, item.Length - 2)); } } } if (formatEscapedDQuot) { for (int index = 0; index < split.Count; index++) { split[index] = split[index].Replace("\"\"", "\""); } } return split.ToArray(); } private static readonly string splitterPattern = "(\"(?:[^\"]|\"\")*\"|[^,]*),"; private static readonly System.Text.RegularExpressions.Regex regexSplitter = new System.Text.RegularExpressions.Regex(splitterPattern, System.Text.RegularExpressions.RegexOptions.Multiline); private System.IO.TextReader reader; }
テスト用のCSVをつくりーの(できるだけえげつないの)
テストのコンソールプロジェクト作ってリソース追加しーの*3
書きーの
static void Main(string[] args) { CSVReader reader = new CSVReader(new System.IO.StringReader(Resources.CSVImportTest)); string[] getLine = null; while ((getLine = reader.ReadLine()) != null) { for (int index = 0; index < getLine.Length; index++) { // 改行分かりにくいから<br>に置換する。 getLine[index] = getLine[index].Replace(System.Environment.NewLine, "<br>"); } // アイテム同士をスラッシュで連結して表示する。 Console.WriteLine(string.Join("/", getLine)); } }
走らせる。
動いてるっぽい?改行コードとか、ちょっと不安なので使う際はお手元でも一通りテストしてくださいな*4
それでは皆様、良いお年を。
12/28 19:00追記
ソースコードの偶数カウンタctQuotですが、偶数達成時にゼロ初期化するのを忘れてました。「カウントアップした状態が偶数かどうか」だから理論的には初期化せずとも動くんですが気持ち悪いのでなおしました。
2015/04/14 追記
ダブルクオーテーションのトリム処理のところ、空のセルおよび最終セルがダブルクオーテーション一つだけでEOFしてる場合のチェックを追加…うーむgdgdで申し訳ないですハイ。