RSS

LINQは本当に強力だ (6) TextFieldContext

17 11月

抽象的な話ばかり続いたので、今回は、実用的な例を示そう。

.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);
	}
}

涙が出るほど簡単で、しかも速い。鳥肌が立つね 🙂

広告
 
コメントする

投稿者: : 2012/11/17 投稿先 .NET, LINQ

 

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

 
%d人のブロガーが「いいね」をつけました。