黒龍's Blog

明日から役立つ無駄知識をあなたに(仮)

ホーム 連絡をする 同期する ( RSS 2.0 ) Login
投稿数  167  : 記事  0  : コメント  836  : トラックバック  25

ニュース

わんくま同盟に参加させていただきました。
どうぞよろしくお願いします。

自己紹介

コミュニティ

  • わんくま同盟
    わんくま同盟

書庫

2012年4月7日 #

いまどきはHello WorldならぬツイッターのTL表示がサンプルとしてよく見かけるように思います。お勉強の題材でも色々触っててふと思ったのですがStreamingのある今って@に反応するようなBotがタイムラグなしに実現できるんじゃなかろうか?と。というわけでやってみました。

まずは認証のための登録をhttp://twitter.com/appsで済ませておきます。(作るアプリのタイプによっては不要です)

で認証部分ですがReactiveOAuthを使わせていただきました。(@neueさん感謝です)

まぁゴテゴテかいてもあれなのでソースをば

static void Main(string[] args)
{
    var client = new OAuthClient(consumerKey, consumerSecret, accessToken)
    {
        Url = "https://userstream.twitter.com/2/user.json"
    };
    var us = client.GetResponseLines()
        .Where(s => !string.IsNullOrWhiteSpace(s)) // filter invalid data
        .Select(s => // 文字列JSONからXElementへ変換
            {
                using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.Default.GetBytes(s), XmlDictionaryReaderQuotas.Max))
                    return XElement.Load(jsonReader);
            })
        .Where(d => d.Element("entities") != null 
            && d.Element("entities").Elements("user_mentions").Count(_ => _.Element("item") != null && _.Element("item").Element("screen_name").Value == "LinqFe") > 0)
        //.ObserveOnDispatcher()
        .Subscribe(
            d => ParseAndAction(d),
            ex => Console.Error.WriteLine(ReadWebException(ex)));

    Console.ReadKey();
    us.Dispose();
}

使い方のサンプルそのままな感じですが一応開設するとLinqTojson(JsonReaderWriterFactory経由によるLinqToXML)な感じで読み取ってWhereのところでLinqFe宛のツイートをフィルタリングして中身を処理するParseAndAction()にXMLを渡す。ただそれだけです。

Whereの中身がうるさい感じですがこういったWebのものはデシリアライズしてクラスに起しちゃうよりXMLで文字列で読み取ってるのが正解。。とおもいます。無関係な定義の変更なんかに煩わされずに済むので。いるとこだけ定義したクラスへのデシリアライズでもいいんですがさっと思いついた機能をさっと足すぐらいのほうが楽でいいです。

で、作ってみたのがこちら。おうむ返しのBotでもいいんですがこないだからいじってたHotPepperからの検索をしてくれるBotにしてみました。実行結果はこちらになります。

DosPromptResult

ReactiveOAuth、Rxのお手軽さとStreaming APIの強力さがよくわかる題材でした。こういったエージェントっぽい反応をするBotは以前はラグがあっていまいちだったんですが今はStreamingでリアルタイムに反応できるので面白いと思います。

posted @ 18:43 | Feedback (4)

2012年2月8日 #

大事なことなのに放置してたのでエントリ。Rxでのいいところの一つにDisposeでのキャンセルがあると思います。とはいえ私もあまり詳しくないのでメモ代わりですがRxなクエリの最終段階の購読登録であるSubscribeメソッドですが戻りがIDisposableになってます。で、こいつのDisposeを呼ぶことでイベントハンドラから外れて以後のイベントは飛んでこなくなります。だいたい非同期にしたいからRx使うんで遅い(or その他)理由でキャンセルしたいケースがかなりあるかと思います。そんな時にDisposeってことで以前のコードにつけてみました。

で試してみるとおぉ~止まる。また取得して~おぉ~止まる。でもってまた取得して~~~?ん?取得しない??

なんか固まってるっぽい。てかこの挙動HTTPの通信上限っぽい動き^^;

イベントハンドラからは外れているもののそもそもの受信待ちの取得ができなくなってDisposeされてない感じに思えます。んむむ。。。どうやればいいのかさっぱりわからん。色々調べていくといろんなやり方がありそうだけどもneueさんところにあったWebRequestをAbortするパターンってのが良さげでした。ただソースが追いにくい。。。Rxの習熟度が低い私からみるとまるで魔法のようなコード。ちょっとしっかり覚えなきゃだなぁ。

posted @ 0:49 | Feedback (4)

2012年2月7日 #

知ってる人からしたら当たり前なのかもですが不思議に思ったのでメモ。

C++/CLIですがアンマネージとの変換はいろいろ手法があります。まぁ文字列型もいろいろあるのでその辺は仕方ないんですが割と低レベルというかCレベルのものと組み合わせる時はconst char*になると思います。で、このconst char*とのやり取りですが.net からであればIntPtr ptr = Marshal::StringToHGlobalAnsi(text); あたりがメジャーかと思います。Marshal::FreeHGlobal(ptr);さえ忘れなければよくあるAnsi(sjis)な文字列との変換はオッケー。

で、タイトルに書いたmarshal_as<>ですがこいつも2008から入ってきたのと他の変換と同じに書けるのもあってモリモリ使ってました。が、こいつですが内部でWideCharToMultiByteを使っていることもあって現在のThreadのコードページの影響をうけます。どういうことかというとSystem:String^な文字列をSJISにしたいなぁって時にふつうのアプリからなら問題ないです。が、ローカライズで英語リソース読むようにCultureを変更するような場合だとSJISじゃなくてコードページ1252みたいなことになっちゃいます。

確かにローカライズで扱う文字も変わるとはいえアプリの本質も通信する先もSJISなのは変わらないので余計なお世話かなぁと感じた次第です。なのでSJISな奴との通信がある場合の変換はmarshal_asじゃなくStringToHGlobalAnsiあたりを使っとくほうが間違いがないと思います。上記挙動って覚えてればいいですがすっかり忘れる気がするので。

以上。あまり役に立たないかもな情報でした。

posted @ 22:58 | Feedback (0)

2012年1月30日 #

しつこくも続いてます。

やっぱりJOSNのほうがデータ小さいしJSON対応にすっかーと思ってJsonReaderWriterFactoryを挟んでみたらあっさりと成功。いわゆるLinq to JSONってやつですな。気をよくしてSLとWP7もやりますかね~って感じで書き換えていったまではいいのですが。。。

ない。。。

WP7にJsonReaderWriterFactoryがねェ!!まじかよ~と嘆きつつもないものは仕方ないのでDataContractJsonSerializerでシリアライズする方向に。JsonReaderWriterFactoryは幸せなんですがマルチな展開を考えるとDataContractJsonSerializer一択って感じですね。。。とはいえWeb相手だとガッツリ定義できないことも多いのでなかなか悩ましい。。。とはいえDataContractJsonSerializer自体はかなりいい感じです。さっくりとデシリアライズできたのでなにも困らなかったです。JsonReaderWriterFactoryを使ってのLinqToJSONはT4とかで生かすといい感じになるんじゃないでしょうか。

今回とくに書くネタもないんですが検索部分を若干修正しました。複数回指定できるようなパラメータがあるんですが今までパラメータをWhere句から拾ってDictionaryに突っ込んでたので同一のキーが定義できなかったのでList<KeyValuePair<string, string>>な感じに変えるとともにドメイン(定義域)を持ったやつをクラスにくくり出しました。まぁふつうはEnumにするところなんですが最終的な文字列への落とし込みとExpressionからの拾い上げがめんどいのでEnumじゃないです。どういうものかというと。。。

/// <summary>
/// 検索範囲
/// </summary>
public struct Range
{
    private string _value;
    internal string Value { get { return _value; } }
    public static Range m300 { get { return new Range("1"); } }
    public static Range m500 { get { return new Range("2"); } }
    public static Range km1 { get { return new Range("3"); } }
    public static Range km2 { get { return new Range("4"); } }
    public static Range km3 { get { return new Range("5"); } }
    private Range(string value) 
    {
        this._value = value;
    }
    public override string ToString()
    {
        return _value;
    }
    public static bool operator ==(Range lhs, Range rhs)
    {
        return lhs.Value == rhs.Value;
    }
    public static bool operator !=(Range lhs, Range rhs)
    {
        return !(lhs == rhs);
    }
}

こんな感じで文字列Enumっぽいものとしてよく作られるんじゃないかなぁと想像。こうすると文字列だと間違うところが間違えないし比較対象も限定できるので。(EqualsとかGetHashはいらないので手抜き^^;)

実際動かしてみるとRxのメリットはかなり大きいですね。何より止まらずにデータが振ってくるのはかなり素敵な感じです。

さぁ。準備はできたのでSilverlightのお勉強ができるぞっと。

posted @ 23:13 | Feedback (6)

2012年1月21日 #

前回まででおよその形は出来上がりました。まぁいまどきのアプリってSilverlightとかだわなぁってことでSL、WP7から使えるようにライブラリをこさえてみようと思ったんですがうまくいかない。

WebClientで同期でとっちゃってるんでそりゃ駄目だわな。非同期に書き換えるとして。。。IEnumerableで返せん。。。Linq構文あきらめてただのWebClientの非同期サンプルにする?それは負けだろう。

…Rxだな。。。

とはいえRXで非同期をなんてサンプルはどこにでもある話。やるなら噂で聞いたIQbservableかなぁと漠然と思いながらneueさんのRxのライブラリ紹介とかを見ながらお勉強。どうせなら同じソースで動かしたいよねぇってことでRxを落としてくる。が、ここで問題発生。どうもWP7にはIQbservableがなさそうな予感。。。色々悩んだ結果以前の部分でIEnumerableを返してたのでTの部分を非同期リクエストの戻りであるIObservableを内包した形で返すことに。合わせてResult周りの型を変更。GourmetResult : ResultBase>な感じに。

ではプロジェクトのほうを整えることに。Coreに切りだしていた部分はそのまま使うのでリンクで追加。あ、そうそうWP7にはExpressionVisitorがないんですがMSDNのサイトにソースが全部あるので追加。プロバイダ周りは書き換えるのでコピーで追加しました。全体像してはこんな感じ。

ソリューションエクスプローラ

PagingQueryProviderを書き換えていくことにします。肝としてはWebClientで動機リクエストしてたところをIObservable化するところってことですが@ITにずばりな感じの入門記事があったのでそれを参考に。

第1回 Reactive Extensionsの概要と利用方法

WebClientにDownloadStringAsyncってのが追加されてIObservableで返ってきますのでその後パースしてってところは前と同じ。

var req = WebRequest.Create(executeUrl);

var o = req.DownloadStringAsync().Select(_ =>
    {
        var element = XDocument.Parse(_);
        var ns = element.Root.GetDefaultNamespace();

        //エラーチェック
        var error = element.Descendants(ns + "error").FirstOrDefault();
        if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);

        //クエリー条件にマッチする、検索結果の全件数
        this.limit = int.Parse(element.Descendants(ns + "results_available").FirstOrDefault().Value);
        //このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
        this.returnedCount = int.Parse(element.Descendants(ns + "results_returned").FirstOrDefault().Value);
        //検索結果の開始位置
        var resultStart = int.Parse(element.Descendants(ns + "results_start").FirstOrDefault().Value);

        var collection = element.Descendants(ns + name);

        //取得件数が指定されているなら補正する
        if (takeCount.HasValue)
        {
            var endLimit = takeCount.Value + start;
            this.limit = Math.Min(endLimit, this.limit);
        }
        return collection.Select(x => innerBuilder(x)).ToArray();
    });

こんな感じです。パース後にDescendantsするとXElement[]になるんですが途中部分でサクッと変換はやりにくいので要素一個のファクトリメソッドを受け取るようにしておきました。(FuncなinnnerBuilderってやつです)これでIObservableって形になるのでこれを前回の結果のIEnumerableを内包した結果で返します。

継続部分をとる必要がありますが以前はIEnumerableだったので普通にConcatでした。今回はIObservableとIEnumerable>をつないで返すことに。これはEnumerable.Repeat(o, 1).Concat(GetResultCore(url))でおけ。

でとりだしの部分ですがIObservableとIEnumerable、配列が混在なのでちょっとややこしいですが分けて考えれば大丈夫。

var context = new HotpepperContext();
var query = (
    from result in context.Gourmet
    where result.keyword == this.textBox1.Text
    select result).Take(100).First().Results.Concat();

var subscribe = (
    from results in query
    from shop in results
    select shop)
    .ObserveOnDispatcher()
    .Subscribe(_ =>
        {
            count++;
            this.textBlock1.Text = count.ToString();
            this.listBox1.Items.Add(_);
        });

こうなります。前半は以前と同様IQeryableを使ってるところ。IEnumerableを含んだ(Resultsがそれ)結果がひとつ返るのでFirstしてResultsをConcat。この操作でIEnumerable>がIObservableに平滑化されます。このまま続けて書いてもいいのですがここまではIQueryable、これからはIObservableという違いとT[]で返るのでSelectManyがわかりやすいように分けてクエリ構文で記載してみました。

WP7結果

感触ですがかなり簡単に取れるのでいい感じ。もろもろまとめて公開予定ですがひとまずPageQueryProviderのソースだけでも乗っけときますね。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;
using System.Reactive.Linq;
using System.IO;

namespace LinqToHotpepper
{
    /// 
    /// ファクトリクラス
    /// 
    public static class PagingQueryProvider
    {
        /// 
        /// ファクトリメソッド
        /// 
        /// 発行されたAPIキー
        /// リクエストURL
        /// 抽出対象のXML要素名
        /// 結果セットのファクトリ
        /// 結果のファクトリ
        public static PagingQueryProvider Create(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder)
        {
            return new PagingQueryProvider(apiKey, baseUrl, name, builder, innerBuilder, null);
        }
        /// 
        /// ファクトリメソッド
        /// 
        /// 発行されたAPIキー
        /// リクエストURL
        /// 抽出対象のXML要素名
        /// 結果のファクトリ
        /// 結果セットのファクトリ
        /// 要素から抽出を行うFinder
        public static PagingQueryProvider Create(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder, EqualsFinder finder)
        {
            return new PagingQueryProvider(apiKey, baseUrl, name, builder, innerBuilder, finder);
        }
    }
    public class PagingQueryProvider : QueryProviderBase
    {
        /// 発行されたAPIキー
        private string key;
        /// リクエストURL
        private string baseUrl;
        /// 要素から抽出を行うFinder
        private EqualsFinder finder;
        /// 抽出対象のXML要素名
        private string name;
        /// 結果セットのファクトリ
        private Func<IEnumerable<IObservable>, T1> resultBuilder;
        /// 結果セットのファクトリ
        private Func<XElement, T2> innerBuilder;
        /// 開始件数
        private int start;
        /// 取得中件数
        private int index;
        /// 取得終了件数
        private int limit;
        /// 取得時の戻り件数(初回リクエスト時の戻り件数)
        private int returnedCount;
        /// パラメータで指定された取得件数
        private int? takeCount;
        /// デフォルトの取得件数
        private const int DEFAULT_COUNT = 30;

        /// 
        /// コンストラクタ
        /// 
        /// 発行されたAPIキー
        /// リクエストURL
        /// 抽出対象のXML要素名
        /// 結果のファクトリ
        /// 要素から抽出を行うFinder
        public PagingQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<IObservable>, T1> builder, Func<XElement, T2> innerBuilder, EqualsFinder finder)
        {
            this.start = 0;
            this.key = apiKey;
            this.baseUrl = baseUrl;
            this.name = name;
            this.resultBuilder = builder;
            this.innerBuilder = innerBuilder;
            this.finder = finder;
        }

        protected override IEnumerable GetResult(Expression expression)
        {
            InnermostFinder innerFinder = new InnermostFinder();
            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);
            if (finder != null)
            {
                MethodCallExpression whereExpression = innerFinder.GetInnermostWhere(expression);
                if (whereExpression != null)
                {
                    LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
                    // Send the lambda expression through the partial evaluator.
                    lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
                    finder.Expression = lambdaExpression.Body;

                    foreach (var item in finder.Results)
                    {
                        sb.AppendFormat("&{0}={1}", item.Key, item.Value);
                    }
                }
            }

            MethodCallExpression skipExpression = innerFinder.GetInnermostSkip(expression);
            if (skipExpression != null)
            {
                this.start = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(skipExpression.Arguments[1]));
            }

            MethodCallExpression takeExpression = innerFinder.GetInnermostTake(expression);
            if (takeExpression != null)
            {
                this.takeCount = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(takeExpression.Arguments[1]));
            }

            var url = sb.ToString();

            var executeUrl =  string.Format("{0}&start={1}",url, start + 1);

            if (takeCount.HasValue) executeUrl = string.Format("{0}&count={1}", executeUrl, takeCount.Value);

            var req = WebRequest.Create(executeUrl);

            var o = req.DownloadStringAsync().Select(_ =>
                {
                    var element = XDocument.Parse(_);
                    var ns = element.Root.GetDefaultNamespace();

                    //エラーチェック
                    var error = element.Descendants(ns + "error").FirstOrDefault();
                    if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);

                    //クエリー条件にマッチする、検索結果の全件数
                    this.limit = int.Parse(element.Descendants(ns + "results_available").FirstOrDefault().Value);
                    //このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
                    this.returnedCount = int.Parse(element.Descendants(ns + "results_returned").FirstOrDefault().Value);
                    //検索結果の開始位置
                    var resultStart = int.Parse(element.Descendants(ns + "results_start").FirstOrDefault().Value);

                    var collection = element.Descendants(ns + name);

                    //取得件数が指定されているなら補正する
                    if (takeCount.HasValue)
                    {
                        var endLimit = takeCount.Value + start;
                        this.limit = Math.Min(endLimit, this.limit);
                    }
                    return collection.Select(x => innerBuilder(x)).ToArray();
                });
            yield return resultBuilder(Enumerable.Repeat(o, 1).Concat(GetResultCore(url)));
        }
        /// 
        /// 繰り返し部分の取得実体
        /// 
        /// 
        /// 
        private IEnumerable<IObservable> GetResultCore(string url)
        {
            //次回取得位置を初期化
            index = start + returnedCount;
            //取得ループ開始
            while (true)
            {
                //欲しい件数が取れているなら終了
                if (limit < index + 1) yield break;
                var executeUrl = string.Format("{0}&start={1}", url, start + 1);

                //残りカウントを取得
                var leftCount = limit - (index + returnedCount);
                //取得件数より小さければ取得件数を減らす
                if (leftCount < returnedCount) executeUrl = string.Format("{0}&count={1}", executeUrl, leftCount);

                var req = WebRequest.Create(executeUrl);

                var o = req.DownloadStringAsync().Select(_ =>
                    {
                        var element = XDocument.Parse(_);
                        var ns = element.Root.GetDefaultNamespace();

                        //エラーチェック
                        var error = element.Descendants(ns + "error").FirstOrDefault();
                        if (error != null) throw new InvalidQueryException(new Error(ns, error).Message);

                        //クエリー条件にマッチする、検索結果の全件数
                        var total = element.Descendants(ns + "results_available").FirstOrDefault();
                        // このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
                        var returned = element.Descendants(ns + "results_returned").FirstOrDefault();
                        //結果がなければ終了
                        if (total == null || total.Value == "0" || returned == null || returned.Value == "0") Observable.Empty<XElement[]>();

                        var collection = element.Descendants(ns + name);

                        index += int.Parse(returned.Value);

                        return collection.Select(x => innerBuilder(x)).ToArray();
                    });
                yield return o;
            }
        }
    }
}

public static class WebRequestExtensions
{
    public static IObservable<string> DownloadStringAsync(this WebRequest request)
    {
        return Observable.Defer(() => Observable.FromAsyncPattern<WebResponse>(
                request.BeginGetResponse, request.EndGetResponse)()
            .Select(res =>
            {
                using (var stream = res.GetResponseStream())
                using (var sr = new StreamReader(stream))
                {
                    return sr.ReadToEnd();
                }
            }));
    }
}

コーディング量はそうないもののいい感じにWP7対応できました。

※ひとまず今までの一式。WP7位置情報とか簡単に取れて面白いかも。

posted @ 18:57 | Feedback (11)

2012年1月10日 #

前回(Linq to 色々その3.汎用的な呼び出しのベースを作成する。)、ひとまずクエリを投げて取得する汎用的なものは作成できました。今回はページング対応のものを完成させることにします。ページングで汎用的なものを作ればグルメサーチも店名サーチも両方使えますね!

今回ページングするにあたってSkipとTakeは必要そうなのでInnermostWhereFinderをInnermostFinderとリネームして中身にSkip、Takeの取得を追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    internal class InnermostFinder : ExpressionVisitor
    {
        private MethodCallExpression innermostWhereExpression;
        private MethodCallExpression innermostSkipExpression;
        private MethodCallExpression innermostTakeExpression;

        public MethodCallExpression GetInnermostWhere(Expression expression)
        {
            Visit(expression);
            return innermostWhereExpression;
        }

        public MethodCallExpression GetInnermostSkip(Expression expression)
        {
            Visit(expression);
            return innermostSkipExpression;
        }
        public MethodCallExpression GetInnermostTake(Expression expression)
        {
            Visit(expression);
            return innermostTakeExpression;
        }
        protected override Expression VisitMethodCall(MethodCallExpression expression)
        {
            if (expression.Method.Name == "Where")
                innermostWhereExpression = expression;

            if (expression.Method.Name == "Skip")
                innermostSkipExpression = expression;
            
            if (expression.Method.Name == "Take")
                innermostTakeExpression = expression;

            Visit(expression.Arguments[0]);
            return expression;
        }
    }
}
Modifierも併せて修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public static class ExpressionTreeModifier
    {
        public static ExpressionTreeModifier<T> Create<T>(IQueryable<T> query)
        {
            return new ExpressionTreeModifier<T>(query);
        }
    }
    public class ExpressionTreeModifier<T> : ExpressionVisitor
    {
        private IQueryable<T> queryable;

        internal ExpressionTreeModifier(IQueryable<T> query)
        {
            this.queryable = query;
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name == "Where")
                return Visit(node.Arguments[0]);

            if (node.Method.Name == "Skip")
                return Visit(node.Arguments[0]);
            
            if (node.Method.Name == "Take")
                return Visit(node.Arguments[0]);
            
            return base.VisitMethodCall(node);
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            // Replace the constant Query<T> arg with the queryable Place collection.
            if (c.Type == typeof(Query<T>))
                return Expression.Constant(this.queryable);
            else
                return c;
        }
    }
}

Whereとおんなじ感じで追加。で、Whereの抽出部分を以前はQueryProviderBase<T>の中でやってたんですがSkipはページングしない場合は必要ないのでWhereの解釈部分も派生クラス側でやるように修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public abstract class QueryProviderBase<T> : IQueryProvider
    {
        public IQueryable CreateQuery(Expression expression)
        {
            Type elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (System.Reflection.TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        // Queryable's collection-returning standard query operators call this method.
        public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
        {
            return new Query<TResult>(this, expression);
        }

        public object Execute(Expression expression)
        {
            return this.Execute<object>(expression, false);
        }

        // Queryable's "single value" standard query operators call this method.
        // It is also called from QueryableGourmetData.GetEnumerator().
        public TResult Execute<TResult>(Expression expression)
        {
            bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");

            return this.Execute<TResult>(expression, IsEnumerable);
        }

        public TResult Execute<TResult>(Expression expression, bool IsEnumerable)
        {
            // The expression must represent a query over the data source.
            if (!IsQueryOverDataSource(expression))
                throw new InvalidProgramException("No query over the data source was specified.");

            IQueryable<T> result = GetResult(expression).AsQueryable<T>();

            var treeCopier = ExpressionTreeModifier.Create(result);
            Expression newExpressionTree = treeCopier.Visit(expression);

            // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
            if (IsEnumerable)
                return (TResult)result.Provider.CreateQuery(newExpressionTree);
            else
                return (TResult)result.Provider.Execute(newExpressionTree);
        }

        private bool IsQueryOverDataSource(Expression expression)
        {
            // If expression represents an unqueried IQueryable data source instance,
            // expression is of type ConstantExpression, not MethodCallExpression.
            return (expression is MethodCallExpression);
        }

        protected abstract IEnumerable<T> GetResult(Expression expression);
    }
}

あとはページング対応のProviderを実装。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;

namespace LinqToHotpepper
{
    /// <summary>
    /// ファクトリクラス
    /// </summary>
    public static class PagingQueryProvider
    {
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        public static PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder)
        {
            return new PagingQueryProvider<T>(apiKey, baseUrl, name, builder, null);
        }
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public static PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            return new PagingQueryProvider<T>(apiKey, baseUrl, name, builder, finder);
        }
    }
    public class PagingQueryProvider<T> : QueryProviderBase<T>
    {
        /// <summary>発行されたAPIキー</summary>
        private string key;
        /// <summary>リクエストURL</summary>
        private string baseUrl;
        /// <summary>要素から抽出を行うFinder</summary>
        private EqualsFinder<T> finder;
        /// <summary>抽出対象のXML要素名</summary>
        private string name;
        /// <summary>結果セットのファクトリ</summary>
        private Func<IEnumerable<XElement>, T> resultBuilder;
        /// <summary>開始件数</summary>
        private int start;
        /// <summary>取得中件数</summary>
        private int index;
        /// <summary>パラメータで指定された取得件数</summary>
        private int? takeCount;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public PagingQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            this.start = 0;
            this.key = apiKey;
            this.baseUrl = baseUrl;
            this.name = name;
            this.resultBuilder = builder;
            this.finder = finder;
        }

        protected override IEnumerable<T> GetResult(Expression expression)
        {
            InnermostFinder innerFinder = new InnermostFinder();
            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);
            if (finder != null)
            {
                MethodCallExpression whereExpression = innerFinder.GetInnermostWhere(expression);
                if (whereExpression != null)
                {
                    LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
                    // Send the lambda expression through the partial evaluator.
                    lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
                    finder.Expression = lambdaExpression.Body;

                    foreach (var item in finder.Results)
                    {
                        sb.AppendFormat("&{0}={1}", item.Key, item.Value);
                    }
                }
            }

            MethodCallExpression skipExpression = innerFinder.GetInnermostSkip(expression);
            if (skipExpression != null)
            {
                this.start = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(skipExpression.Arguments[1]));
            }

            MethodCallExpression takeExpression = innerFinder.GetInnermostTake(expression);
            if (takeExpression != null)
            {
                this.takeCount = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(takeExpression.Arguments[1]));
            }

            var url = sb.ToString();

            var executeUrl = url + string.Format("&start={0}&count=30", start + 1);
            var element = XDocument.Load(executeUrl);

            //エラーチェック
            var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
            if (error != null) throw new InvalidQueryException(new Error(error).Message);

            //クエリー条件にマッチする、検索結果の全件数
            var total = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault().Value);
            //このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
            var returned = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault().Value);
            //検索結果の開始位置
            var resultStart = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_start").FirstOrDefault().Value);

            var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);

            var limit =total;

            if (takeCount.HasValue) limit = takeCount.Value + start;

            //残りがあるなら残りのリクエストをシーケンスにくっつけておく
            if (resultStart + returned < limit)
            {
                //次の呼び出しのためにカウンタを進めておく
                index = start + returned;
                collection = collection.Concat(this.GetResultCore(url));
            }
            if (takeCount.HasValue)
            {
                collection.Take(takeCount.Value);
            }
            yield return resultBuilder(collection);
        }
        /// <summary>
        /// 繰り返し部分の取得実体
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private IEnumerable<XElement> GetResultCore(string url)
        {
            //取得ループ開始
            while (true)
            {
                var executeUrl = url + string.Format("&start={0}&count=30", index + 1);

                var element = XDocument.Load(executeUrl);

                //エラーチェック
                var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
                if (error != null) throw new InvalidQueryException(new Error(error).Message);

                //クエリー条件にマッチする、検索結果の全件数
                var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
                // このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
                var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
                //結果がなければ終了
                if (total == null || total.Value == "0" || returned == null || returned.Value == "0") yield break;

                var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);

                var limit = int.Parse(total.Value);

                if (takeCount.HasValue) limit = takeCount.Value + start;

                foreach (var item in collection)
                {
                    index++;
                    if(index > limit) yield break;
                    yield return item;
                }
            }
        }
    }
}
ひとまずこれで完成。

他のWebServiceもこれらをベースにすればLinq化するのも楽かもしれません。まぁWebはXML or JSONで十分って気がしないでもないですが^^;
posted @ 1:37 | Feedback (3)

2012年1月9日 #

前回(Linq to 色々その2)、前々回(Linq to 色々)に引き続きIQueryableの残りを作っていきます。

Where句を抽出してWeb Serviceのパラメータとするところまでは概ねうまくいっていたのですが気になる現象がみられました。

それはWhere指定した条件が最終的な列挙にも使用されているという現象です。(なので結果セットを作るところでkeywordとかを詰めなおしていた)

これはExpressionTreeからWhereを評価後に該当するWhere句を取っ払ってしまえばよく、そのためのExpressionTreeModifier<T>というクラスが存在していますので中身にWhere句部分を取っ払うようにしてみましょう。

Where句というのはWhereの抽出で行っていた中身にあるように実質はWhereメソッドの呼び出しになります。(Argument[0]が後続のSelect等のクエリ、Argument[1]がWhereの条件部分)なのでここをWhereの抽出時同様にVisitMethodCallをオーバライドするようにしましょう。こうすれば結果セットに対してのWhereはかからなくなるので期待通り結果のみつめればよくなります。

protected override Expression VisitMethodCall(MethodCallExpression node)
{
    if (node.Method.Name == "Where")
        return Visit(node.Arguments[0]);
    return base.VisitMethodCall(node);
}

で、いよいよ大物のグルメサーチAPIと行きたいところですがマスタ類も結局は必要になるので先にかたずけるとしましょう。前回ページング対応のものは作ったのですが同じ形でAPI毎に作るというのも面倒なのでほとんどのマスタ系呼び出しに共通するページングなしのQueryProviderを作成しましょう。

やりたいこととしては

  • URLを切り替えれるようにする
  • 結果のXMLから抽出するタグ名を指定できるように
  • 検索条件があれば指定できるように

あたりの事が出来ればひとまずよさそうです。というわけで。。。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;

namespace LinqToHotpepper
{
    /// <summary>
    /// ファクトリクラス
    /// </summary>
    public static class SimpleQueryProvider
    {
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        public static SimpleQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder)
        {
            return new SimpleQueryProvider<T>(apiKey, baseUrl, name, builder, null);
        }
        /// <summary>
        /// ファクトリメソッド
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public static SimpleQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            return new SimpleQueryProvider<T>(apiKey, baseUrl, name, builder, finder);
        }
    }
    public class SimpleQueryProvider<T> : QueryProviderBase<T>
    {
        /// <summary>発行されたAPIキー</summary>
        private string key;
        /// <summary>リクエストURL</summary>
        private string baseUrl;
        /// <summary>要素から抽出を行うFinder</summary>
        private EqualsFinder<T> finder;
        /// <summary>抽出対象のXML要素名</summary>
        private string name;
        /// <summary>結果セットのファクトリ</summary>
        private Func<IEnumerable<XElement>, T> resultBuilder;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        /// <param name="finder">要素から抽出を行うFinder</param>
        public SimpleQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
        {
            this.key = apiKey;
            this.baseUrl = baseUrl;
            this.name = name;
            this.resultBuilder = builder;
            this.finder = finder;
        }
        /// <summary>
        /// パラメータチェック
        /// </summary>
        /// <param name="finder"></param>
        /// <returns></returns>
        private bool IsValidParameter(EqualsFinder<ShopResult> finder)
        {
            return true;
        }

        protected override IEnumerable<T> GetResult(Expression expression)
        {
            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);
            if (finder != null)
            {
                finder.Expression = expression;
                foreach (var item in finder.Results)
                {
                    sb.AppendFormat("&{0}={1}", item.Key, item.Value);                    
                }
            }

            var element = XDocument.Load(sb.ToString());

            //エラーチェック
            var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
            if (error != null) throw new InvalidQueryException(new Error(error).Message);

            var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);

            yield return resultBuilder(collection);
        }
    }
}

SimpleQueryProvider<T>ですがQueryProviderBase<T> (abstractだったのでQueryProviderからQueryProviderBaseに名称変えました)をシンプルに実装しただけですね。同名のSimpleQueryProviderクラスはSimpleQueryProvider<T>に対するファクトリです。ジェネリッククラスのコンストラクタはいちいち<T>を書かないとダメなんですがジェネリックメソッドは引数からの型推論が効くのでジェネリックメソッドにはだいたい作っちゃうことが多いです。

こうすることでnew Query<ResultBase<CodeName>>(new SimpleQueryProvider<ResultBase<CodeName>>(...

なんて書き方がQuery.Create(SimpleQueryProvider.Create(...ですんじゃうので。で、呼び出し方はそのまんまですが

var foodQuery = Query.Create(SimpleQueryProvider.Create(
            <APIキー>,
            "http://webservice.recruit.co.jp/hotpepper/food/v1/",
            "food",
             x => new FoodResult()
             {
                 Results = x.Select(_ => new Food(_))
             },
             EqualsFinder.Creater((FoodResult _) => _.code
                , _ => _.keyword,
                _ => _.food_category)));
var query = from result in foodQuery
            from food in result.Results
            where result.keyword == "料理"
            select food;

foreach (var item in query)
{
    System.Diagnostics.Debug.WriteLine(item.Name);
}

みたいな感じにすることでOK。ページング対応の部分も必要なので前回のShopInfoQueryProviderをもとに今回のような汎用的な検索のページングバージョンを作成しましょう。

長くなったので続く。

posted @ 1:32 | Feedback (8)

2012年1月5日 #

前回(Linq to 色々)の続きです。

あれだけではLinqっぽさがまるで出てないので今回はExpressionの解釈も進めましょう。QueryProviderBaseの中でInnermostWhereFinderを使って最も内側のWhere句を拾うところまではすでに入っているのであとはWhereの解釈になります。元記事ではLocationFinderなるExpressionVisitorを継承したクラスがその役目を負っていました。これも型特化しているのでジェネリックに書き換えてみました。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public static class EqualsFinder
    { 
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="exp">走査対象のExpressionTree</param>
        /// <param name="name">プロパティ名</param>
        /// <returns></returns>
        public static EqualsFinder<T> Creater<T>(Expression exp, params string[] name)
        {
            return new EqualsFinder<T>(exp, name);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="P"></typeparam>
        /// <param name="exp">走査対象のExpressionTree</param>
        /// <param name="properties"></param>
        /// <returns></returns>
        public static EqualsFinder<T> Creater<T, P>(Expression exp, params Expression<Func<T, P>>[] properties)
        {
            string[] names = new string[properties.Length];
            for (int i = 0; i < properties.Length; i++)
            {
                names[i] = ((MemberExpression)properties[i].Body).Member.Name;
             
            }
            return new EqualsFinder<T>(exp, names);
        }
    }

    public class EqualsFinder<T> : ExpressionVisitor
    {
        private Expression expression;
        private Dictionary<string,string> results;
        private string[] name;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="exp">走査対象のExpressionTree</param>
        /// <param name="name">プロパティ名</param>
        public EqualsFinder(Expression exp, string[] name)
        {
            this.expression = exp;
            this.name = name;
        }

        /// <summary>
        /// 検索した結果
        /// </summary>
        public Dictionary<string, string> Results
        {
            get
            {
                if (results == null)
                {
                    results = new Dictionary<string, string>();
                    this.Visit(this.expression);
                }
                return this.results;
            }
        }

        protected override Expression VisitBinary(BinaryExpression be)
        {
            if (be.NodeType == ExpressionType.Equal)
            {
                foreach (var item in name)
                {
                    if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(T), item))
                    {
                        results[item] = ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(T), item);
                        return be;
                    }
                }
                return base.VisitBinary(be);
            }
            else
                return base.VisitBinary(be);
        }
    }
}
あとで使いやすいように特定の型のプロパティを使用してDictionaryに突っ込んでいく形としました。今回はちょっと複雑めのAPIとして店名サーチAPIを使用します。ホットペッパーAPIリファレンス
呼び出しで帰ってくる型やらを定義します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace LinqToHotpepper
{
    /// <summary>
    /// WebService呼び出しに使用する結果型。(再内のWhereでの条件がリクエストに使用される)
    /// </summary>
    public class ShopResult
    {
        public string keyword { get; set; }
        public string tel { get; set; }
        public IEnumerable<ShopInfo> Results { get; set; }
    }
    public class ShopInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string NameKana { get; set; }
        public string Address { get; set; }
        public Genre Genre { get; set; }
        public Urls Urls { get; set; }
        public string Desc { get; set; }
        public ShopInfo(XElement element)
        {
            this.Id = (string)element.Element(element.GetDefaultNamespace() + "id");
            this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
            this.NameKana = (string)element.Element(element.GetDefaultNamespace() + "name_kana");
            this.Address = (string)element.Element(element.GetDefaultNamespace() + "address");
            this.Genre = new Genre(element.Element(element.GetDefaultNamespace() + "genre"));
            this.Urls = new Urls(element.Element(element.GetDefaultNamespace() + "urls"));
            this.Desc = (string)element.Element(element.GetDefaultNamespace() + "desc");
        }
    }
    public class Genre
    {
        public string Name { get; set; }
        public Genre(XElement element)
        {
            this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
        }
    }
    public class Urls
    {
        public string Pc { get; set; }
        public string Mobile { get; set; }
        public string Qr { get; set; }
        public string Sp { get; set; }

        public Urls(XElement element)
        {
            this.Pc = (string)element.Element(element.GetDefaultNamespace() + "pc");
            this.Mobile = (string)element.Element(element.GetDefaultNamespace() + "mobile");
            this.Qr = (string)element.Element(element.GetDefaultNamespace() + "qr");
            this.Sp = (string)element.Element(element.GetDefaultNamespace() + "sp");
        }
    }
}

APIのリファレンスでもわかるように<results>の中に件数やらがあった後に<shop>として繰り返し格納されます。ちょっと特殊なのがShopResult型。Web Serviceに投げるパラメータをクエリのWhereの条件として指定できるためには結果セットである必要があるのですが結果のなかに普通に結果と一体になっていると取得後の絞り込みとリクエストのパラメータと区別ができなくなるためIEnumerable<T>として結果セットを含む型を返すようにしました。取り出しはおなじみSelectManyです。

クエリ構文で書くと

var context =  new HotpepperContext(<APIキー>);

var x =
    from result in context.ShopInfo
    from shop in result.Results
    where result.keyword == "オーガニック 大阪"
    select shop;

な感じですがfromが並ぶのってわかりにくいかもですね。。。

では実際に取得を行うProviderの中身を見ていきましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;

namespace LinqToHotpepper
{
    public class ShopInfoQueryProvider : QueryProvider<ShopResult>
    {
        /// <summary>発行されたAPIキー</summary>
        private string key;
        /// <summary>開始件数</summary>
        private int start;
        /// <summary>読み出し件数(最小1、最大30)</summary>
        private int count;
        /// <summary>条件抽出を行うFinder</summary>
        private EqualsFinder<ShopResult> finder;

        /// <summary>リクエストURL</summary>
        private const string baseUrl = "http://webservice.recruit.co.jp/hotpepper/shop/v1/";
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        public ShopInfoQueryProvider(string apiKey)
        {
            key = apiKey;
        }

        /// <summary>
        /// パラメータチェック
        /// </summary>
        /// <param name="finder"></param>
        /// <returns></returns>
        private bool IsValidParameter(EqualsFinder<ShopResult> finder)
        {
            return true;
        }

        protected override IEnumerable<ShopResult> GetResult(Expression expression)
        {
            // Get the place name(s) to query the Web service with.
            this.finder = EqualsFinder.Creater(expression, (ShopResult o) => o.keyword);

            //条件をチェック
            if (!IsValidParameter(this.finder))
                throw new InvalidQueryException("You must specify at least one place name in your query.");

            this.start = 1;
            this.count = 30;

            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);
            if (finder.Results.ContainsKey("tel")) sb.AppendFormat("&tel={0}", finder.Results["tel"]);
            if (finder.Results.ContainsKey("keyword")) sb.AppendFormat("&keyword={0}", finder.Results["keyword"]);

            var url = sb.ToString();

            var element = XDocument.Load(url + string.Format("&start={0}", start) + string.Format("&count={0}", count));

            //エラーチェック
            var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
            if (error != null) throw new InvalidQueryException(new Error(error).Message);

            //クエリー条件にマッチする、検索結果の全件数
            var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
            //このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
            var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
            //検索結果の開始位置
            var resultStart = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_start").FirstOrDefault();

            var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}shop")
                .Select(x => new ShopInfo(x));
            //残りがあるなら残りのリクエストをシーケンスにくっつけておく
            if (int.Parse(resultStart.Value) + int.Parse(returned.Value) > int.Parse(total.Value))
            {
                //次の呼び出しのためにカウンタを進めておく
                start += count;
                collection = collection.Concat(this.GetResultCore(url));
            }
            //結果を返す
            yield return new ShopResult()
            {
                tel = finder.Results.ContainsKey("tel") ? finder.Results["tel"] : string.Empty,
                keyword = finder.Results.ContainsKey("keyword") ? finder.Results["keyword"] : string.Empty,
                Results = collection,
            };
        }
        /// <summary>
        /// 繰り返し部分の取得実体
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private IEnumerable<ShopInfo> GetResultCore(string url)
        {
            //取得ループ開始
            while (true)
            {
                var element = XDocument.Load(url + string.Format("&start={0}", start) + string.Format("&count={0}", count));

                //エラーチェック
                var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
                if (error != null) throw new InvalidQueryException(new Error(error).Message);

                //クエリー条件にマッチする、検索結果の全件数
                var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
                // このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
                var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
                //結果がなければ終了
                if (total == null || total.Value == "0" || returned == null || returned.Value == "0") yield break;

                var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}shop")
                    .Select(x => new ShopInfo(x));

                foreach (var item in collection)
                {
                    yield return item;
                }
                start += count;
            }
        }
    }
}

リファレンスにあるように件数やらが返ったあとにshopが繰り返し来る形になります。また、パラメータにも取得開始行数、取得行数などがありページングしながら全部をIEnumerable越しに返すという部分を作りこみたいのでレスポンスを返し切ったら続きを取得というように書いてみます。(今回のAPIでは30件以上の問い合わせはそもそもエラーになってしまうのですが^^;;)

まずはリクエストを投げて件数などを見ながらShopInfoのイテレータに後続部分をConcatしながら返すという形で書いてみました。(今回はベタに1件目からとってますがこのあたりExpressionを解釈してSkip、Takeあたりを取れれば無駄のないリクエストができそうです)

で、実際の取得ですがこんな感じ

var shopInfo = new Query<ShopResult>(new ShopInfoQueryProvider(<APIキー>));
var query =
    from result in shopInfo
    from shop in result.Results
    where result.keyword == "オーガニック 大阪"
    select shop;
foreach (var item in query)
{
    System.Diagnostics.Debug.WriteLine(item.Name);
}

ちょっとはLinqっぽくなってきましたね~。とはいえ今回のAPIはMAX30件という微妙な位置づけAPIなので本命はグルメサーチAPIなんですがパラメータが半端ないので次回に持ち越します。

posted @ 1:03 | Feedback (5)

2012年1月4日 #

あけましたね。おめでとうございます。

今年も色々と技術や製品が出てくると思いますがおいてかれないようにしがみつけるよう頑張りたいと思います。

で、今年初めてのエントリなんですが大好きなLinqのエントリ。

巷にはいろんなWeb Serviceがありますよね。AmazonしかりGoogleしかり。楽天やらもあるのかな?しかしながらLinqからWeb Serviceってちょっと触りにくいんですよね。当然結果自体はXMLに流し込むなりでIEnumerable<XElement>にしちまえばオッケーなんですが一回のリクエストなりの件数ってそう多くは取れないしそもそもあまりLinqしてる感が少ないのが不満でした。(だって呼び出しでほとんどの苦労が集約されてるし。。。)

じゃーやってみんべかとおもって調べてみました。

チュートリアル : IQueryable LINQ プロバイダーの作成

ん~なんか色々クラスが分かれててややこしい。。。が、くじけずそのまま書いてみる。Web Service部分はXDocument.Loadでいっか。

まずはIQueryable or IOrderedQueryable<TData>があってIQueryProviderとExpressionを内包してると。でもってIQueryProviderからTerraServerQueryContextのstaticなメソッド類に委譲している感じ。肝としてはこいつらの実装をやればよさげな感じ。元記事でヘルパとなってるExpressionTreeHelpers.csやらEvaluator.csやらTypeSystem.csやらInvalidQueryException.csはひとまずそのまま持ってくるとしてTerraServerQueryContextから呼び出されているInnermostWhereFinder.csがWhere句を拾ってくるところ見たい。まぁWhereした内容でURLたたきたいのでひとまずこのまんま。実際にExpressionTreeから色々拾ってくるであろうLocationFinderは後で類似のものをこさえましょう。

次はExpressionTreeModifier.csですがガッツリと型定義が入ってますがやる内容は汎用的っぽいのでジェネリックで型を追い出しときました。

こんな感じで。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public static class ExpressionTreeModifier
    {
        public static ExpressionTreeModifier<T> Create<T>(IQueryable<T> query)
        {
            return new ExpressionTreeModifier<T>(query);
        }
    }
    public class ExpressionTreeModifier<T> : ExpressionVisitor
    {
        private IQueryable<T> queryable;

        internal ExpressionTreeModifier(IQueryable<T> query)
        {
            this.queryable = query;
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            // Replace the constant Query<T> arg with the queryable Place collection.
            if (c.Type == typeof(Query<T>))
                return Expression.Constant(this.queryable);
            else
                return c;
        }
    }
}

名前空間にあるように今回はHotpepperのWeb Serviceに対して呼び出しをかけて見ようと思います。

でまずはIQueryableを実装してるQueryableTerraServerData<TData>ってのをクラス名を変えて実装。これ自体も基本的には汎用的なのでIQueryProviderを受け取るコンストラクタを追加。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Linq.Expressions;

namespace LinqToHotpepper
{

    public class Query<TData> : IOrderedQueryable<TData>
    {
        #region Constructors
        /// <summary>
        /// This constructor is called by the client to create the data source.
        /// </summary>
        public Query(IQueryProvider provider)
        {
            Provider = provider;
            Expression = Expression.Constant(this);
        }

        /// <summary>
        /// This constructor is called by Provider.CreateQuery().
        /// </summary>
        /// <param name="expression"></param>
        public Query(IQueryProvider provider, Expression expression)
        {
            if (provider == null)
            {
                throw new ArgumentNullException("provider");
            }

            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }

            if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type))
            {
                throw new ArgumentOutOfRangeException("expression");
            }

            Provider = provider;
            Expression = expression;
        }
        #endregion

        #region Properties

        public IQueryProvider Provider { get; private set; }
        public Expression Expression { get; private set; }

        public Type ElementType
        {
            get { return typeof(TData); }
        }

        #endregion

        #region Enumerators
        public IEnumerator<TData> GetEnumerator()
        {
            return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (Provider.Execute<IEnumerable>(Expression)).GetEnumerator();
        }
        #endregion
    }
}

お次はIQueryProviderを実装しているところ。元の記事ではTerraServerQueryProviderにあたる部分で処理実態はTerraServerQueryContextに委譲してたやつです。さすがにサービスごとにこのペアを起こしてくのは面倒なので共通部分をベースクラスとしてくくりだします。共通っぽいところは念力で。。。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace LinqToHotpepper
{
    public abstract class QueryProvider<T> : IQueryProvider
    {
        public IQueryable CreateQuery(Expression expression)
        {
            Type elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (System.Reflection.TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        // Queryable's collection-returning standard query operators call this method.
        public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
        {
            return new Query<TResult>(this, expression);
        }

        public object Execute(Expression expression)
        {
            return this.Execute<object>(expression, false);
        }

        // Queryable's "single value" standard query operators call this method.
        // It is also called from QueryableGourmetData.GetEnumerator().
        public TResult Execute<TResult>(Expression expression)
        {
            bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");

            return this.Execute<TResult>(expression, IsEnumerable);
        }

        public TResult Execute<TResult>(Expression expression, bool IsEnumerable)
        {
            // The expression must represent a query over the data source.
            if (!IsQueryOverDataSource(expression))
                throw new InvalidProgramException("No query over the data source was specified.");

            // Find the call to Where() and get the lambda expression predicate.
            InnermostWhereFinder whereFinder = new InnermostWhereFinder();
            MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
            Expression body = null;
            if (whereExpression != null)
            {
                LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
                body = lambdaExpression.Body;
            }
            IQueryable<T> result = GetResult(body).AsQueryable<T>();

            // Copy the expression tree that was passed in, changing only the first
            // argument of the innermost MethodCallExpression.
            var treeCopier = ExpressionTreeModifier.Create(result);
            Expression newExpressionTree = treeCopier.Visit(expression);

            // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
            if (IsEnumerable)
                return (TResult)result.Provider.CreateQuery(newExpressionTree);
            else
                return (TResult)result.Provider.Execute(newExpressionTree);
        }
        private bool IsQueryOverDataSource(Expression expression)
        {
            // If expression represents an unqueried IQueryable data source instance,
            // expression is of type ConstantExpression, not MethodCallExpression.
            return (expression is MethodCallExpression);
        }
        protected abstract IEnumerable<T> GetResult(Expression expression);
    }
}

やってることはジェネリックで型をくくり出した上で結果取得部分を抽象メソッドにしてやる感じ。こいつを継承しておいてからIEnumerable<T> GetResult(Expression expression); の中でサービス呼び出して結果を組み立てればOKですね。元記事では結果を完全に配列に仕立ててから送り出ししてますが気休め程度にイテレータ経由にしときます。(XDocumentなのでDOM構築しちゃうので。たくさんの件数が一度に取れるようなサービスの場合は@neueさん所のXStreamingReaderとか使うといいと思うよ!!)

で、実際に呼び出しをするやつを継承で作ります。ひとまずパラメータを要求しない呼び出し用に汎用的なものを一つこさえましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;

namespace LinqToHotpepper
{
    public class MasterQueryProvider<T> : QueryProvider<T>
    {
        /// <summary>発行されたAPIキー</summary>
        private string key;
        /// <summary>リクエストURL</summary>
        private string baseUrl;
        /// <summary>抽出対象のXML要素名</summary>
        private string name;
        /// <summary>結果のファクトリ</summary>
        private Func<XElement, T> builder;

        /// <summary>
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="apiKey">発行されたAPIキー</param>
        /// <param name="baseUrl">リクエストURL</param>
        /// <param name="name">抽出対象のXML要素名</param>
        /// <param name="builder">結果のファクトリ</param>
        public MasterQueryProvider(string apiKey, string baseUrl, string name, Func<XElement, T> builder)
        {
            this.key = apiKey;
            this.baseUrl = baseUrl;
            this.name = name;
            this.builder = builder;
        }

        /// <summary>
        /// パラメータチェック
        /// </summary>
        /// <param name="finder"></param>
        /// <returns></returns>
        private bool IsValidParameter(EqualsFinder<ShopResult> finder)
        {
            return true;
        }

        protected override IEnumerable<T> GetResult(Expression expression)
        {
            var sb = new StringBuilder(baseUrl);
            sb.AppendFormat("?key={0}", key);

            var element = XDocument.Load(sb.ToString());

            //エラーチェック
            var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
            if (error != null) throw new InvalidQueryException(new Error(error).Message);

            foreach (var item in element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name))
            {
                yield return builder(item);
            }
        }
    }
}

ちょっとごちゃごちゃしてますがWeb Serviceを使うのに必要なAPIキーのみを渡している感じです。注意点としてレスポンスが名前空間付なのでDescendantsのところが("{http://webservice.recruit.co.jp/HotPepper/}" + nameのような名前空間付のXNameになっているとこですかね。

ここまでできればクエリ式経由で一応結果が取れます。(何も使ってないからLinqの意味はない)

var context = new Query<CodeName>(new MasterQueryProvider<CodeName>(
    <APIキーをここに書く>,
    "http://webservice.recruit.co.jp/hotpepper/budget/v1/",
    "budget",
    x => new CodeName(x)));
var query = from result in context
    select result;
foreach (var item in query)
{
    System.Diagnostics.Debug.WriteLine(item.Name);
}
public class CodeName
{
    public string Code { get; set; }
    public string Name { get; set; }

    public CodeName(XElement element)
    {
        this.Code = (string)element.Element(element.GetDefaultNamespace() + "code");
        this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
    }
}
結果の型の組み立てのとこもGetDefaultNamespaceが必要なので注意。
ひとまずここまで+αのソースを置いておきます。
つづく
posted @ 23:46 | Feedback (2)

2011年10月26日 #

どうにもわからないEqualsやらGetHashCodeやらその辺のお話。

R#がリファレンスだろjkとかIEquatable<T>は値型用だから参照型への実装は云々など枝葉の話はよく聞くんですがどうにも実装しててややこしいので整理の意味でエントリ。

特にポリモーフィズムな型の等値性判断のあたりを何とかしたいなと。。。

関係ありそうなところをまず張っときます。

まずは基本のobject.Equals(object obj)

public class Test
{
    public string Name { get; set; }
    public string Address { get; set; }
}

みたいな型があったとしたら実装は

public override bool Equals(System.Object obj)
{
    // If parameter is null return false.
    if (obj == null)
    {
        return false;
    }

    // If parameter cannot be cast to Point return false.
    Test p = obj as Test;
    if ((System.Object)p == null)
    {
        return false;
    }

    // Return true if the fields match:
    return (Name == p.Name) && (Address == p.Address);
}

忘れずにGetHashCodeも実装

public override int GetHashCode()
{
    var hash = Name.GetHashCode();
    hash += 1234 ^ Address.GetHashCode();
    return hash;
}

この辺はまぁ割と普通の実装なのでそうはずしてはないかと。

で、値型でのBoxing回避やらDictionaryとかの流れでIEquatable<T>、おまけで==と!=あたりを実装してくかと思います。
この辺で実装が色々バリエーションが出てくるんですがベーシックに参照比較を織り交ぜつつこんな感じで。

public bool Equals(Test other)
{
    if (Object.ReferenceEquals(other, null))
    {
        return false;
    }

    if (Object.ReferenceEquals(this, other))
    {
        return true;
    }

    if (this.GetType() != other.GetType())
        return false;

    return (Name == other.Name) && (Address == other.Address);
}

で、もともとのObjrct.Equalsは型安全なほうに委譲。

public override bool Equals(object obj)
{
    return this.Equals(obj as Test);
}

ここからが割と本題で派生クラスで実装する場合。まぁほとんど同じですがメンバの比較やGetHashCodeでbase.Equalsみたいな感じで追加したメンバだけを入れ込んでいくのがベーシックなやり方だと思います。

public class TestEx : Test, IEquatable<TestEx>
{
    public string PhoneNumber { get; set; }

    public override bool Equals(object obj)
    {
        return base.Equals((TestEx)obj);
    }

    public override int GetHashCode()
    {
        var hash = base.GetHashCode();
        hash += 1234 ^ PhoneNumber.GetHashCode();
        return hash;
    }

    public bool Equals(TestEx other)
    {
        return base.Equals(other) && PhoneNumber == other.PhoneNumber;
    }
}

この辺からちょっぴり自信がないですがまぁだいたいこんな感じかと。

で、こういったケースだとそれぞれの型の比較はいいんですが基底クラスで比較した場合(今回だとTest)にTestのメンバのみで比較されちゃうのでうまくいきません。ポリモーフィズムな作りなのでoverrideされたものがうまく使われてほしいところですが呼び出し時にObjectにするなりしないと結局うまくいかないです。こういうものだって言われればそうなんですが==とかオーバロードした時には派生型で見てほしいのが人情かと思います。
もちろん基底側でObject.Equalsを軸にすれば期待する結果になるんですがタイプセーフじゃなくなる(ってほどでもないかも)のはどうにも違う感じなので。

派生クラスでは基底クラスのIEquatable<T>を実装するのが実はルールだったりするのかしらん?だとしたらヘビーすぎるのでIEquatable<T>いらにゃい!と言いたいです。はい。

おそらく根本的な理解がないのでMSDNやらのバラバラな実装に振り回されてるんだとは思いますが(EqualsやIEquatableの)おおもとの設計意図や最適解を導ければなぁと思いエントリしました。みなさんこんなケースではどう実装してますか?

posted @ 0:20 | Feedback (3)