抽象的な話ばかり続いたので、今回は、実用的な例を示そう。
.NETでCSVファイルを読み取るとき、まさか自分でパースしたりしていないと思うが、知っていると便利なクラスが「VB.NET」のライブラリに存在する。TextFieldParserクラスだ。VB向けの実装の割には、Streamからの読み取りに対応しているなど、割としっかり作ってある。
今回はこのクラスをLINQで「楽に」使えるようにする。
public static class TextField { // 指定されたCSVファイルへのコンテキストを生成する public static IEnumerable<string[]> Context( string path, string separator = ",", Encoding encoding = null) { using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (TextFieldParser parser = new TextFieldParser(stream, encoding ?? Encoding.UTF8, true, false)) { parser.TextFieldType = FieldType.Delimited; parser.Delimiters = new[] { separator }; parser.HasFieldsEnclosedInQuotes = true; parser.TrimWhiteSpace = true; while (parser.EndOfData == false) { string[] fields = parser.ReadFields(); yield return fields; } } } } }
このクラスのアイデアは、LINQ to Entitiesだ。Contextメソッドで一旦「コンテキスト」を作ってしまえば、面倒な事を一切考えなくても読み取りが可能になる。これでロジックから解放だ。
郵便局 郵便番号データ(全国)を使う。
static void Main(string[] args) { // 郵便番号データのコンテキストを生成 IEnumerable<string[]> context = TextField.Context( @"KEN_ALL.CSV", ",", Encoding.GetEncoding(932)); // (クエリは遅延実行なので、以下の式ではまだ処理を開始していない) IEnumerable<string> results = // コンテキストからフィールド群を取得 from fields in context. AsParallel() // 並列化 // 郵便番号データの住所部分に「字」と「条」が含まれている行を抽出し、 where fields[8].Contains("字") || fields[8].Contains("条") // 郵便番号の降順でソートし、 orderby fields[2] descending // 文字列形式にフォーマットする select string.Format("〒{0} {1}{2}{3}", fields[2], fields[6], fields[7], fields[8]); // 取りあえず、結果をコンソールに出してみた(ここでGetEnumeratorが呼ばれて実行が開始される) foreach (string result in results) { Console.WriteLine(result); } }
データ量が少ないので、並列化の恩恵はあまりないが、AsParallelしてみた。AsParallelを付けたり外したりしてみて、タスクマネージャのCPUリソースの具合を確認して見ると良い。
前回、LINQのパイプライン実行について説明したが、上記のコードの良い点は、コンテキストからデータが流れるようにやってきて、クエリで加工されて結果が出力される(コンソールに表示される)というところだ。実は、orderby句(OrderByDescending拡張メソッド)は、内部でバッファリングを行っているので、件数に応じてメモリ使用量が増加してしまうが、orderbyが無ければゴミをGCが回収するため、メモリ使用量が増え続けてしまう事はない。
(orderbyはソートを行うのだが、ソートを行うためにはすべてのデータが揃わなければならない。いくらパイプライン実行でも、不可能なことはある。その代わり、AsParallelしていれば、スレッド数に応じてソートが高速化される。)
さあ、仕上げだ。目の前にCSVファイルの山があったとしよう。
static void Main(string[] args) { IEnumerable<string> results = from path in Directory.GetFiles("CSV_FILES", "*.csv", SearchOption.AllDirectories). AsParallel() from fields in TextField.Context(path, ",", Encoding.GetEncoding(932)) where fields[8].Contains("字") || fields[8].Contains("条") orderby fields[2] descending select string.Format("〒{0} {1}{2}{3}", fields[2], fields[6], fields[7], fields[8]); foreach (string result in results) { Console.WriteLine(result); } }
涙が出るほど簡単で、しかも速い。鳥肌が立つね :-)