Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)

C#と親和性の高いデータ形式はXMLです。何と言ってもLinq to Xmlが強力です。また、SOAPも悪くない、というのもVisual Studioの自動生成が効くので何も考えずともホイホイ使えます。ではJSONは、というと、これは割と扱いづらいところがあるのが正直なところ。しかしWindows Phone 7においては、JSONを選択すべきでしょう。なにせ、モバイル機器。ネットワークがとても貧弱。データは小さいに越したことはない。XMLとJSONとでは、雲泥の差です。

WPFではJsonReaderWriterFactory(と、内部にそれを用いたDynamicJson)、SilverlightではSystem.Jsonなどが用意されていますが、WP7には一切ありません。じゃあどうするかといえば、シリアライザを使います。WP7ではDataContractJsonSerializerが標準で用意されている(WPF, SLにもあります)ので、それを使ってデシリアライズしてJSONをオブジェクトに変換するのが基本戦略となります。

外部ライブラリ、Json.NETを使うという手も勿論ありますが。

BingからのJSONの取得

何はともあれ、サンプル題材のJSONを拾ってきましょう。Webからの取得というと、最近はいつもTwitterのPublic Timelineでマンネリ飽き飽きなので、別のものを。WP7なので、Bing APIを使いましょう!Bing APIはIDを取得しないと使えないのでサンプル的にどうよ、というところもありますが、IDの取得は簡単(ほんとワンクリックです)だしWP7と親和性の高いAPIでもあるので、これを機に、試しに取ってみるのも良いのではと思います。画像検索、翻訳など色々種類があるのですが、今回はWeb検索(sources=web)にします。

// 標準WP7テンプレのMainPage.xaml.csにベタ書き

const string AppId = ""; // AppIdは登録してください

Uri CreateQuery(params string[] words)
{
    // countなどは変数で置き換えれるようにするといいのではと思います、ここでは固定決め打ちですが
    var query =
          "?Appid=" + AppId
        + "&query=" + Uri.EscapeUriString(string.Join(" ", words))
        + "&sources=web"
        + "&version=2.0"
        + "&Market=ja-jp"
        + "&web.count=20"
        + "&web.offset=0";

    return new Uri("http://api.search.live.net/json.aspx" + query);
}

public MainPage()
{
    InitializeComponent();

    var wc = new WebClient();

    Observable.FromEvent<DownloadStringCompletedEventHandler, DownloadStringCompletedEventArgs>(
            h => h.Invoke, h => wc.DownloadStringCompleted += h, h => wc.DownloadStringCompleted -= h)
        .ObserveOnDispatcher()
        .Subscribe(e =>
        {
            var json = e.EventArgs.Result; // ダウンロード結果(json文字列)
            MessageBox.Show(json);
        });

    wc.DownloadStringAsync(CreateQuery("地震"));
}

json.aspxにクエリ文字列をつけてGETするだけなので割とお手軽。クエリ文字列がゴチャゴチャして分かりづらいのですが、基本的に弄るのはqueryとweb.countぐらいかな、と思います。BingのReferenceは、生成元のクラス構造がまんま掲示されているだけで、恐ろしく分かりづらいので、適当にサンプルから当たりをつける感じで。

非同期通信の実行はReactive Extensions(Rx)で行います。Windows Phone 7では標準で入っているのでSystem.ObservableとMicrosoft.Phone.Reactiveを参照に加えてください。非同期通信を生でやるなんてありえませんから!Rx利用を推奨します。

得られるJSONは下記のものです。

{
    "SearchResponse": {
        "Version": "2.0",
        "Query": {
            "SearchTerms": "地震"
        },
        "Web": {
            "Total": 88,
            "Offset": 0,
            "Results": [
                {
                    "Title": "地震情報 - Yahoo!天気情報",
                    "Description": "Yahoo!天気情報は、市区町村の天気予報、世界の天気...",
                    "Url": "http://typhoon.yahoo.co.jp/weather/jp/earthquake/",
                    "DisplayUrl": "typhoon.yahoo.co.jp/weather/jp/earthquake",
                    "DateTime": "2011-03-29T19:11:00Z"
                },
                {
                    "Title": "地震情報 :: ウェザーニュース",
                    "Description": "最新の地震の震度、震源地、震度分布を速報で届けます...",
                    "Url": "http://weathernews.jp/quake/",
                    "DisplayUrl": "weathernews.jp/quake",
                    "DateTime": "2011-03-28T09:10:00Z"
                },
                // 配列上なので幾つも...
            ]
        }
    }
}        

JSONに関してはJSON ViewerをVisual StudioのVisualizerに組み込むとかなり快適にプレビュー出来るようになります。が、カスタムVisualizerはWP7では実行出来ないのでテキストで見て、スタンドアロンのものにコピペってのを実行ですね、しょんぼり。

DataContractJsonSerializer

では、JSONをオブジェクトに変換しましょう。基本的には、1:1に対応するクラスを作るだけ。必要に応じて System.Runtime.Serializationの参照を加えDataContract, DataMember属性なども加えればよし。

public class BingWebRoot
{
    public SearchResponse SearchResponse { get; set; }
}

public class SearchResponse
{
    public string Version { get; set; }
    public Query Query { get; set; }
    public Web Web { get; set; }
}

public class Query
{
    public string SearchTerms { get; set; }
}

public class Web
{
    public int Total { get; set; }
    public int Offset { get; set; }
    public Results[] Results { get; set; }
}

public class Results
{
    public string Title { get; set; }
    public string Description { get; set; }
    public string Url { get; set; }
    public string DisplayUrl { get; set; }
    public string DateTime { get; set; }
}

JSONは、JavaScriptのオブジェクトとほぼ同一の記述ですが、ようするに{}になっている部分はクラスで、[]になっている部分は配列で、置き換えていけばいい、ということで。難しくはないのですが、面倒くさいには大変面倒くさい。なお、JSONの構造を全部記述する必要はなく、必要なものだけでも構いません、例えばVersionやQueryはいらないから省くとか、全然アリです。

そして、 System.ServiceModel.Web を参照設定に加え、DataContractJsonSerializerを使います。

var wc = new WebClient();

Observable.FromEvent<OpenReadCompletedEventHandler, OpenReadCompletedEventArgs>(
        h => h.Invoke, h => wc.OpenReadCompleted += h, h => wc.OpenReadCompleted -= h)
    .ObserveOnDispatcher()
    .Subscribe(e =>
    {
        using (var stream = e.EventArgs.Result)
        {
            var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
            var result = (BingWebRoot)serializer.ReadObject(stream);

            MessageBox.Show(result.SearchResponse.Web.Results[0].Title);
        }
    });

wc.OpenReadAsync(CreateQuery("地震"));

デシリアライズはReadObject、シリアライズはWriteObjectで行います。基本はstreamを渡すだけでオブジェクトの出来上がり。

with Reactive Extensions

ですが、まあ、Resultsが欲しいだけなのにSearchResponse.Web.Resultsは長げーよ、とか、DateTimeがstringでイヤだー、とか色々あります。そういう場合はJSONとのマッピング用のクラスとは別に、アプリケーション側で使うクラスを別に立ててやればいいんぢゃないかしら。

public class SearchResults
{
    public string Title { get; set; }
    public string Url { get; set; }
    public DateTime DateTime { get; set; }

    public override string ToString()
    {
        return DateTime + " : " + Title + " : " + Url;
    }
}

TitleとUrlとDateTimeしかいらない!という具合で。これを、今度はWebRequestを使って書くと

var req = WebRequest.Create(CreateQuery("ほむほむ"));

Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
    .Select(r =>
    {
        using (var stream = r.GetResponseStream())
        {
            var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
            return (BingWebRoot)serializer.ReadObject(stream);
        }
    })
    .SelectMany(x => x.SearchResponse.Web.Results)
    .Select(x => new SearchResults { DateTime = DateTime.Parse(x.DateTime), Title = x.Title, Url = x.Url })
    .ObserveOnDispatcher()
    .Subscribe(x => 
    {
        // 加工は全部終わってるのでここで色々自由に処理
        Debug.WriteLine(x);
    });

となります。最初のSelectは非同期の結果、次のSelectManyではResults[]、つまり普通の配列を平坦化して、以降は普通のLinqのようなコレクション処理をしています。

非同期リクエストとオブジェクトのコレクション処理が、完全にシームレスに溶け込んでいます。これが、RxがLinqとして存ることの真価の一つです。記述が統一され、かつ限りなくシンプルになる。Rxは非同期が、イベントが、時間が、簡単に扱えます。でも、本当の真価は単独で使うというだけでなく、それらが全てPush型シーケンスに乗っていることで、統合することが可能だというところにあります。

でも、むしろ分かりにくい?ふむむ……。慣れの問題、などというと全く説得力がなくてアレですが、しかし、慣れです。記述がシンプルになり、柔軟性と再利用性が増していることには間違いないわけで、後は一度全て忘れてLINQの世界に飛び込んでしまえばいいと思うんだ。

Linqは各処理の単位が細分化されている(Selectは射影、Whereはフィルタ)ことも特徴ですが、これは思考の再利用可能性を促します。非同期->オブジェクト配列=SelectManyなど、単純な定型パターンに落とし込めます。C#はもとより強力なIntelliSenseにより、ブロックを組み立てるかの如きなプログラミングを可能にしていますが、Linqでは、それが更に先鋭化されていると見れます。

まとめ

これも現在製作中のWP7アプリからの一部です。最近Bing API利用に切り替えたので。無駄に汎用化して作りこみつつきりがないので適度なところできりあげつつ。ユニットテスト作ってあったので移行自体は幸いすんなりいった。良かった良かった。テスト大事。

Bing APIの前は諸事情あってGoogleからのスクレイピングでした。スクレイピングはグレーだろうということで代替案をずっと探していて、何とかBingに落ち着きました。最初はどうにも使い物にならない、と思ったのですが、検索パラメータを色々変えて、ある程度望む結果が出るようにはなったかな、と。Bingは結構癖があって、調整大変ですね。その話は後日、WP7アプリが完成したときにでも……。

コード的にはスクレイピングのほうも割と凝ってたんですけどねー、バッサリとゴミ箱行き。復活することは、ないかな。もったいないけどしょうがない。いつかそのうち紹介する日は、来るかも来ないかも。

そんなわけで延々と足踏みしていて実装は相変わらず一歩も進んでませんが(!) 順調に制作は進行中なので乞うご期待。いやほんと。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive