だるろぐ

明日できることは、今日しない。

あちこちスクレイピングして、松山の鉄道・船・飛行機の運行状況をまとめるサイトを作ってみた

f:id:daruyanagi:20150120025457p:plain

WebMatrix + Azure Web サイト(マイクロソフトのクラウド環境や開発ツールを無償提供 | Microsoft BizSpark、ありがとう!)で愛媛・松山の鉄道・船・飛行機の運行状況を集約したサイトを作ってみました(飛行機の英語の綴りミスなんかがボロボロ見つかる程度の成熟度です)。誰の役に立つというものでもないですが、自分的には満足です。そのうちわざわざサイトに行くのも面倒になると思うので、ゆくゆくは Twitter の BOT か何か作って、情報をプッシュできるようにしたいですね。

このサイトの情報は、大きく分けて二つの方法で取得しています。

  • サイトをスクレイピングして情報取得(鉄道、船、JAL)
  • (非公開)API を叩いて JSON から情報取得(ANA、ピーチ)
  • (ジェットスターだけはうまくいかなかった! 今度誰か教えて!*1

スクレイピングで面倒だったのは、情報の形式が一定していないこと、文字コードがバラバラなこと(ローカルでテストしているときは文字化けしないのに、クラウドで動かすと文字化けすることもあった)でしょうか。とくに情報の形式が一定していないのはしんどいですね。イレギュラーなケースを見つけるたびにデータ標準化のための処理が膨れ上がっていくので、最後は妥協、妥協、妥協。ペライチのページにコードをべた書きして、それをまとめていくという開発手法は WebMatrix が得意とする分野だと思うけれど、最後のほうはちょっと力不足かなって感じもしました。まぁ、そんなときはサクッと Visual Studio に切り替えちゃうんですけど。

開発の流れ

f:id:daruyanagi:20150120031218p:plain

運行情報のページをブラウザーでみる。ブラウザーの開発者ツールで DOM をみたり、リクエスト・レスポンスをみたりしながら、静的ページであればスクレイピング、ページを動的に組み立てているなら API を探してそれを叩いてみるという感じ。

スクレイピングであれば、NuGet Gallery | HtmlAgilityPack 1.4.9 が超簡単。XPath でノードを指定してサクッと中身を取ってこれる。

using (var client = new WebClient())
{
    var response = client.DownloadString(url);
    var json = Json.Decode(response);

    var doc = new HtmlAgilityPack.HtmlDocument();
    doc.LoadHtml(doc)

    // <div class="ss_comment">ここがほしいやで</div>
    var message = doc.DocumentNode
        .SelectSingleNode(@"//div[@class=""ss_coment""]")
        .InnerText;

    @ObjectInfo.Print(json)
}

これが使えないならば、正規表現で頑張ることになるのかな。今回はそこまでしなければならないケースはなかった。

非公開 API があるのならば、JSON Helper が便利。Chrome の場合、開発者ツールの[Network]タブでリクエスト・レスポンスをみると、内部 API らしきものが見るかる。今回見つけたなかでは、ANA が出発・到着空港の天気までとれる重量級。一方、ピーチは割と扱いやすい感じ(ASP.NET っぽかった)。

using (var client = new WebClient())
{
    var response = client.UploadString(url, postData);
    var json = Json.Decode(response);

    @ObjectInfo.Print(json)
}

ObjectInfo.Print で Json オプジェクトの中身をみながら、自分が使いやすいようにデータを取捨・加工する。

コーディング自体はとても簡単で、開発時間のほとんどは API のパラメーター解析に費やされた。

これから

閲覧時に情報を取りに行く設計になっているので、キャッシュが効いていない場合にロードがとても遅い。相手にも負荷がかかって迷惑*2なので、バックグラウンドで情報を取得するようにしたい。Twitter BOT にするにも必要な処理だし。

*1:幸い JAL とのコードシェアなので、そっちから情報を取得

*2:本当に迷惑になりそうな、体力がなさげなサイトにはすでに対策を入れてある

WebMatrix:愛媛のニュースだけ読みたいので、Google ニュースから引っ張ってくる

f:id:daruyanagi:20150118081201p:plain

地元のニュースだけ読みたいので、それを Google ニュース引っ張ってくる BOT でも作ろうかと思って少し調べてみた。

# ~/App_Code/GoogleNews.cshtml

@using System.ServiceModel.Syndication
@using System.Xml

@helper Get(string query)
{
    var url = string.Format(
        "http://news.google.com/news?q={0}&output=rss",
        query
    );

    using (var reader = XmlReader.Create(url))
    {
        var feed = SyndicationFeed.Load(reader);

        // @ObjectInfo.Print(feed);

        var data = feed.Items.Select(_ => new {
            Title = _.Title.Text,
            Summary = _.Summary.Text,
            PublishDate = _.PublishDate,
            Url = _.Links.First().Uri.ToString().Split('=').Last(),
        });

        // @ObjectInfo.Print(data);

        foreach (var d in data)
        {
            // <h2><a href="@d.Url">@d.Title</a></h2>
            <p>@Html.Raw(d.Summary)</p>
            // <p>@d.PublishDate</p>
        }
    }
}

パラメーターに output=rss をくっつけると RSS 形式でデータを取得できるので XmlReader と SyndicationFeed で読み込み・整形してやるとよさげ。

残念なのでは地域ごとに絞れないこと*1。そのため「松山」で検索すると台湾の松山のニュースも引っかかるが……まぁ、これは仕方ないかな。JavaScript の API を利用すれば、ちゃんと制御できるようなので、JavaScript ができる人はそちらで書けばいいと思う。

結果

f:id:daruyanagi:20150118080353p:plain

# ~/Default.cshtml

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title></title>
        <style>
            * {
                font-family: Meiryo, sans-serif;
            }
        </style>
    </head>
    <body>
        @GoogleNews.Get("愛媛 OR 松山 -秀樹 -ゴルフ")
    </body>
</html>

天気予報機能も付けて(「☀時々☁、最高10度最低0度、西風後南西風海上後南西風稍強、波0.5米後1米」)、Twitter BOT にでもしようかな。

*1:本当はパラメーターに ned=jp を付けるとその国のニュースのみにデータを絞れるはずなのだけど、日本語版の Google ニュースでは利用できないようだ。

WebMatrix: @nakaji のコードをパクって「えひめFreeWi-Fi」スポットを Google Map へマッピング

f:id:daruyanagi:20140728155412j:plain

SGMLReaderで「えひめFreeWi-Fi」サービス提供箇所をスクレイピング - なか日記 を読んで存在を知った。

産学官で構成する愛媛県公衆無線LAN推進協議会では、外国人観光客や県内外の旅行者、地域住民等が無料で利用できるWi-Fiスポットの整備を民設民営で進めることにより、その利便性を確保し、愛媛県内の地域活性化を図る「えひめFreeWi-Fiプロジェクト」を推進しています。

愛媛県庁/えひめFree Wi-Fiサービス提供箇所等のお知らせ

“利便性を確保し”とか“地域活性化を図る”とかいってる割りには、クソ不便なテーブルデータしか用意してないのって、ほんとお役所だなーと思いますね。

というわけで、なかじが作ってくれたコードをまるパクリして、それを Google Map へマッピングしてみた。これで、ちょっとは利便性が向上するんじゃないだろうか。

マッピングのライブラリは、

をもらってきた。ちょっとコードが古いので、TypeScript で書き直したりしてみたい。

Web ページのコードはこんな感じ(spotList あたりのコードとか汚いけど)。WebMatrix(ASP.NET Web Pages)はこういう“ペライチ”のページを作るときに便利よね。

# ~/Default.cshtml

@using System.Xml.Linq
@using Sgml

@{
    var urlString = "http://www.pref.ehime.jp/h12600/wifi/osirase260822.html";

    XDocument xml;
    using (var sgml = new SgmlReader() { Href = urlString, IgnoreDtd = true })
    {
        xml = XDocument.Load(sgml);
    }

    var ns = xml.Root.Name.Namespace;
    var spots = xml.Descendants(ns + "table")
        .Last()
        .Descendants(ns + "tr")
        .Skip(1) // タイトルをスキップ
        .Select(e => e.Elements(ns + "td").ToList())
        .Select(x => new
        {
            Place = x[1].Value,
            Address = x[2].Value,
            ServiceProvider = x[3].Value
        });

    var spotList = string.Join("\r\n", spots.Select(_ => _.Address.Replace("&minus;", "-")).ToArray());
}

<!DOCTYPE html>

<html lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <script src="http://maps.google.com/maps/api/js?sensor=false"></script>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
        <script src="~/Scripts/sirusiizu.js">
        </script>
    </head>
    <body>

        <textarea id="addressList" style="width: 80%; height: 120px;">@spotList</textarea>

        <div id="map" style="width: 80%; height: 360px;">
        
        </div>

        <script>     
            sirusiizu.initialize("map");
            sirusiizu.marking(document.getElementById("addressList").value);
        </script>
    </body>
</html>

f:id:daruyanagi:20141216181643p:plain

割とたくさんあるんやなー。

まじ申し訳ないけど、割とやっつけ仕事なので、キレイに仕上げてくれていいのよ?

WebMatrix: Google Analytics API を使って前日の PV を取得するコードを C# で書いてみた

f:id:daruyanagi:20141202020215p:plain

Google Analytics API を使って前日の PV を取得するコードを C# で書いてみた - しばやん雑記 を WebMatrix でやってみた。とりあえず、前日の PV を表示するとことまで。

つまづいたところその一

f:id:daruyanagi:20141202020415p:plain

'/' アプリケーションでサーバー エラーが発生しました。

Error:"invalid_grant", Description:"", Uri:""

パラメーターが間違っていたりすると、認証エラーが出る。自分の場合は、PC の時刻が狂っていた。ちゃんと合わせておきましょう。

つまづいたところその二

f:id:daruyanagi:20141202020715p:plain

502 - Web server received an invalid response while acting as a gateway or proxy server.

There is a problem with the page you are looking for, and it cannot be displayed. When the Web server (while acting as a gateway or proxy) contacted the upstream content server, it received an invalid response from the content server.

ローカルではちゃんと動くのに、Windows Azure に置くと 502 エラーが出る。

var certificate = new X509Certificate2(
    HttpContext.Current.Server.MapPath(key), 
    "notasecret",
    X509KeyStorageFlags.Exportable
);

この部分を、以下のように修正。

var certificate = new X509Certificate2(
    HttpContext.Current.Server.MapPath(key), 
    "notasecret", 
    X509KeyStorageFlags.Exportable | 
    X509KeyStorageFlags.MachineKeySet
);

StackOverflow さまさまやでぇ。

つまづいたところその三

f:id:daruyanagi:20141202021217p:plain

そもそもレポジトリのソースを変えないと NuGet Gallery | Google.Apis.Analytics.v3 1.38.0.1306 が出てこない感じ。

で、やっとこさ検索しても、肝心の NuGet パッケージがインストールできねえ……NuGet Package Manager のバージョンが古いからみたいだけど、結局 Visual Studio にスイッチして NuGet をバージョンアップしたりごにょごにょして解決。

結局こんな感じになった。

# ~/_App_Start.cshtml

@using System.Security.Cryptography.X509Certificates
@using Google.Apis.Auth.OAuth2
@using Google.Apis.Analytics.v3
@using Google.Apis.Services

@{
    var key = @"~/API Project-***.p12";
    var mail = @"***@developer.gserviceaccount.com";
    var app_name = "sample app";
    var view_id = "***";

    var certificate = new X509Certificate2(
        HttpContext.Current.Server.MapPath(key), 
        "notasecret", 
        X509KeyStorageFlags.Exportable | 
        X509KeyStorageFlags.MachineKeySet
    );

    var credential = new ServiceAccountCredential(
        new ServiceAccountCredential.Initializer(mail)
        {
            Scopes = new[] {
                AnalyticsService.Scope.Analytics, 
                AnalyticsService.Scope.AnalyticsReadonly
            }
        }.FromCertificate(certificate)
    );

    var service = new AnalyticsService(new BaseClientService.Initializer
    {
        HttpClientInitializer = credential,
        ApplicationName = app_name,
    });

    // Azure は UTC なので +9 時間して -1 日
    var date = DateTime.UtcNow.AddHours(9).AddDays(-1).ToString("yyyy-MM-dd");

    App.PageView = service.Data.Ga
        .Get("ga:" + view_id, date, date, "ga:pageviews")
        .Execute()
        .Rows[0][0];
}

(別に ~/_AppStart.cshtml に書く必要はないけど、成り行きで何となくそうしてしまった)

# Default.cshtml

@{
    
}

<!DOCTYPE html>

<html lang="ja">
    <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    </head>
    <body>
        <p>昨日のだるろぐの PV は @App.PageView でした。 https://blog.daruyanagi.jp/</p>
    </body>
</html>

ほとんどしばやんのコードのまるパクリになったので、こんど万世のローストビーフでもおごってあげようと思った。

WebMatrix: 型または名前空間 'Linq' は名前空間 'System' に存在しません。アセンブリ参照が不足しています。

f:id:daruyanagi:20141114193956p:plain

問題

WebMatrix で作った ASP.NET Web Pages のプロジェクトを Visual Studio で開くと、

型または名前空間 'Linq' は名前空間 'System' に存在しません。アセンブリ参照が不足しています。

というエラーが出てコンパイルできない。

解決策

Web.config で明示的にターゲットフレームワークを指定する。WebMatrix はこれがなくても動くのだけど、Visual Studio の方はちゃんと書いておかないと動かない。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation targetFramework="4.0" debug="true"/>
  </system.web>
</configuration>

このエラーが出るたびに「あぁ、あれか」と思うのだけど、いつも具体的なコードが思い出せなくて、結局ググってたりする。

はてなブログが oEmbed に対応したらしいので WebMatrix で使ってみる

f:id:daruyanagi:20140908003018p:plain

せっかくなので、WebMatrix のヘルパーにしてトップページでも使ってみた。

# ~/App_Code/OEmbed.cshtml

@helper Hatena (string url) {
    try
    {
        using (var downloader = new WebClient())
        {
            var request = string.Format(
                "http://hatenablog.com/oembed?url={0}&format={1}",
                url, "json"
            );

            var oembed_data = downloader.DownloadString(request);
            var oembed_json = Json.Decode(oembed_data);

            @Html.Raw(oembed_json.html);

            // @ObjectInfo.Print(oembed_json)
        }
    }
    catch (Exception e)
    {
        <p class='error'>@url: @e.Message</p>
    }
}

使い方はこんな感じ。ちゃんと運用するなら WebCache とか使って少し高速化してみるのもいいかもしれない。

@OEmbed.Hatena("https://blog.daruyanagi.jp/entry/2014/07/03/035624")

f:id:daruyanagi:20140908003132p:plain

はてなブログのところだけデザインがカッコよくて違和感があるけれど、まぁ、とりあえずこれで。

f:id:daruyanagi:20140908003322p:plain

@ObjectInfo.Print() を有効化してみると JSON でもらえる値がわかるので、これを利用して周りにフィットするようにダサくカードをデザインし直してもいいな(何

WebMatrix 3:oEmbed ヘルパーを作ってみた(2)

WebMatrix 3:oEmbed ヘルパーを作ってみた - だるろぐ の続き。今回は Flickr の埋め込みをやってみようかと思う。

~/App_Code/OEmbed.cshtml

@helper Flickr(string url) {
    const string API_ENDPOINT = "http://www.flickr.com/services/oembed/";

    using (var downloader = new WebClient())
    {
        try
        {
            // URL を組み立てて JSON の oEmbed データを取得
            var request = string.Format("{0}?url={1}&format={2}", API_ENDPOINT, url, "json");
            var oembed_data = downloader.DownloadString(request);
            var oembed_json = Json.Decode(oembed_data);

            @ObjectInfo.Print(oembed_json) // デバッグのため

            var embed_type = oembed_json.type as string;

            switch (embed_type) // photo と video の二種類がある
            {
                case "photo":
<figure>
    <img src='@oembed_json.url' alt='@oembed_json.title'>
    <figcaption>
        <img src='http://favicon.qfor.info/f/@oembed_json.provider_url' />
        <a href='@oembed_json.web_page'>@oembed_json.title</a>
        by <a href='@oembed_json.author_url'>@oembed_json.author_name</a>
    </figcaption>
</figure>
                    break;
                case "video":
<figure>
    @Html.Raw(oembed_json.html)
</figure>
                    break;
                default:
                    break;

            }
        }
        catch (Exception exception)
        {
            <p class='error'>@url: @exception.Message</p>
        }
    }
}

~/Default.cshtml

<!DOCTYPE html>

<html lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    </head>
    <body>
        @OEmbed.Flickr("https://www.flickr.com/photos/daruyanagi/6219334657/")
        @OEmbed.Flickr("https://www.flickr.com/photos/dmartinie/5760711397/")
    </body>
</html>

結果

たとえば Photo の場合。Twitter のときみたいに html を返してくれないので、自分で組み立てる。

f:id:daruyanagi:20140812194342p:plain

<figure>
    <img src='https://farm7.staticflickr.com/6162/6219334657_ba91a4498d_z.jpg' alt='SMSはやく使えるようになりたい'>
    <figcaption>
        <img src='http://favicon.qfor.info/f/https://www.flickr.com/' />
        <a href='https://www.flickr.com/photos/daruyanagi/6219334657/'>SMSはやく使えるようになりたい</a>
        by <a href='https://www.flickr.com/photos/daruyanagi/'>daruyanagi</a>
    </figcaption>
</figure>

Video の場合。これは html があるのでそれを使う。

f:id:daruyanagi:20140812194506p:plain

<figure>
    <object type="application/x-shockwave-flash" width="500" height="281" data="https://www.flickr.com/apps/video/stewart.swf?v=145061" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="intl_lang=en-us&photo_secret=5f7c3bff83&photo_id=5760711397&flickr_show_info_box=true"></param> <param name="movie" value="https://www.flickr.com/apps/video/stewart.swf?v=145061"></param> <param name="bgcolor" value="#000000"></param> <param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="https://www.flickr.com/apps/video/stewart.swf?v=145061" bgcolor="#000000" allowfullscreen="true" flashvars="intl_lang=en-us&photo_secret=5f7c3bff83&photo_id=5760711397&flickr_show_info_box=true" height="281" width="500"></embed></object>
</figure>

f:id:daruyanagi:20140812194912p:plain

写真の方は CSS でいい感じにデコってみた。なかなかいいかも。

WebMatrix 3:oEmbed ヘルパーを作ってみた

oEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.

oEmbed

要はこういうのです。

# はてな記法の場合
https://twitter.com/daruyanagi/status/497645195769298944:embed

URL → 埋め込み HTML を得るための API って感じですかね。

oEmbed API の提供方法は二種類あります。

  • API Endpoint があらかじめ公開されており、それを叩く
  • link タグに埋め込み oEmbed のURLが埋め込まれいるので、それを叩く

両方とも提供してくれている場合もありますが(たとえば YouTube)、前者だけだったり(Flickr)、後者だけだったり(Twitter、一応公開されていますが一般的な API に比べてちょっとめんどいので link タグ探した方がいい)もします。

今回は後者だけ実装してみました。前者については、

で一度やったことがあります。URL パターンとエンドポイントのディクショナリでももっておいて、URL がパターンにマッチした時はエンドポイントを叩く……みたいに実装すれば汎用的になるかと。

準備

f:id:daruyanagi:20140808184043p:plain

HTML から

<link rel="alternate" type="application/json+oembed" href="***" title="***">

みたいなのを探してとってくる必要があるので、スクレイパー御用達の CodePlex Archive を NuGet で追加しておきます。

コード

んじゃ、ガリガリ書いていきます。

~/App_Code/OEmbed.cshtml

まずは、ヘルパー部分を書き書き。

@using HtmlAgilityPack

@helper GetHtml(string url){
    try
    {
        using (var downloader = new WebClient())
        {
            var html = downloader.DownloadString(url);

            var document = new HtmlDocument();
            document.LoadHtml(html);

            url = document.DocumentNode.Descendants("link")
                .Where(_ => _.GetAttributeValue("type", string.Empty) == "application/json+oembed")
                .Select(_ => _.GetAttributeValue("href", string.Empty))
                .First();

            var oembed_data = downloader.DownloadString(url);
            var oembed_json = Json.Decode(oembed_data);

            @Html.Raw(oembed_json.html)
        }
    }
    catch (Exception e)
    {
        <p class='error'>@url: @e.Message</p>
    }
}

oEmbed は XML 形式か JSON 形式でレスポンスを返すのですが、Twitter は JSON のみをサポートします。どのサービスも JSON には対応していることが多いので、JSON だけ処理すればたいていは十分です。

あと、oembed_json.html を直接使うのは怖い。サニタイズしておいた方がいいかも。ほんとはキャッシュの仕組みとかも入れておきたいですね。

~/Default.cshtml

次にテスト用のページを書きかき。

<!DOCTYPE html>

<html lang="ja">
    <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta charset="utf-8" />
        <title>マイ サイトのタイトル</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    </head>
    <body>
        @OEmbed.GetHtml("https://twitter.com/daruyanagi/status/497645195769298944")
        @OEmbed.GetHtml("https://twitter.com/daruyanagi/status/49764519576929894")
        @OEmbed.GetHtml("https://www.flickr.com/photos/daruyanagi/6219334657/")
    </body>
</html>

結果

f:id:daruyanagi:20140808184012p:plain

2番目は URL を間違ってみた。3番目は oEmbed 対応だけど link タグ形式ではないサービス(Flickr)で試してみた。Flickr については、API Endpoint を叩いてーという処理が必要になりますね。

WebMatrix 3:サイトをライブタイルに対応させてみた

f:id:daruyanagi:20140726031239p:plain

Windows 8.1 で(Windows 8 は切り捨てた)http://daruyanagi.net/ をスタート画面へピン留めすると、こんな感じになるハズ。前々からやってみたかったのだけど、なかなか腰が上がらなかった。

用意するもの

f:id:daruyanagi:20140726033408p:plain

  • browserconfig.xml
    • この名前でなくてもいいが、その場合はメタタグに名前を記述
  • 通知タイルを定義した XML ファイル
    • browserconfig.xml の notifications 属性に記述。必須ではないが、用意しておくとタイルがくるくる切り替わるようになる
    • 今回はデータベースをもとに Notifications.cshtml で動的に生成
    • 最大5つまで
  • <meta name="application-name" content="アプリ名"/>
    • レイアウトファイルに一行挿入
    • browserconfig.xml の内容をメタタグとして埋め込むスタイルもあるが、今回は分離しておく。はてなブログをタイルに対応させる場合は、ヘッダーへメタタグを記述する方式がよさそう
  • tiny、square、wide、large の各サイズ向けタイル画像
    • 今回は青色に塗りつぶしただけのシンプルなものを用意
    • タイルの背景に使う tile.jpg も準備しておいた(「画像は .JPG、.GIF、.PNG 形式のファイルであり、容量を 200 KB 未満、サイズを 1024 x 1024 ピクセル未満にする必要があります。」とのこと)
タイルのサイズ 標準のタイルのサイズ 最小画像サイズ 推奨される画像のサイズ
小サイズ 70 x 70 56 x 56 128 x 128
普通サイズ 150 x 150 120 x 120 270 x 270
ワイド サイズ 310 x 150 248 x 120 558 x 270
大サイズ 310 x 310 248 x 248 558 x 558

browserconfig.xml

こんな感じで記述

<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
  <msapplication>
    <tile>
      <square70x70logo src="tiny.png"/>
      <square150x150logo src="square.png"/>
      <wide310x150logo src="wide.png"/>
      <square310x310logo src="large.png"/>
      <TileColor>#007fff</TileColor> // “aliceblue”なんかは使えない鴨
    </tile>
    <notification> // 必須ではない。まずはこれなしで動くかテストすることをお勧め
      <polling-uri src="http://daruyanagi.net/Notifications/0"/>
      <polling-uri2 src="http://daruyanagi.net/Notifications/1"/>
      <polling-uri3 src="http://daruyanagi.net/Notifications/2"/>
      <polling-uri4 src="http://daruyanagi.net/Notifications/3"/>
      <polling-uri5 src="http://daruyanagi.net/Notifications/4"/>
      <frequency>30</frequency> // 30、60、360、720、1440 のいずれか
      <cycle>1</cycle> // 0 ~ 7 のいずれか
    </notification>
  </msapplication>
</browserconfig>

cycle 属性の値はこんな感じ。

  • 0: (通知が 1 つだけの場合の既定値) 循環しません。
  • 1: (通知が複数ある場合の既定値) すべてのタイルのサイズで通知を循環します。
  • 2: 普通サイズのタイルに対する通知のみを循環します。
  • 3: ワイド タイルに対する通知のみを循環します。
  • 4: 大きいタイルに対する通知のみを循環します。
  • 5: 普通サイズのタイルやワイド タイルに対する通知のみを循環します。
  • 6: 普通サイズのタイルや大きいタイルに対する通知のみを循環します。
  • 7: ワイド タイルや大きいタイルに対する通知のみを循環します。

ちゃんと規定値があるので、めんどくさいなら省いてもいいぐらい。

Notifications.cshtml

小・ワイド・大の3つに対応したタイルスキーマを返しておく。テンプレートについては

を参照のこと。

@{
    Response.ContentType = "application/xml";

    dynamic item;

    using (var db = Database.Open(“***”))
    {
        item = db.Query("SELECT * From ***")
            .ElementAt(UrlData[0].AsInt(0));
    }
}
<tile>
  <visual lang="en-US" version="2">  
    <binding template="TileSquare150x150PeekImageAndText04" branding="name">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/>
      <text id="1">@App.Title</text> 
      <text id="2">@item.Title</text> 
    </binding>

    <binding template="TileWide310x150SmallImageAndText04" branding="logo">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/> 
      <text id="1">@App.Title</text> 
      <text id="2">@item.Title</text> 
    </binding>
 
    <binding template="TileSquare310x310ImageAndTextOverlay02" branding="logo">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/>
      <text id="1">@App.Title</text> 
      <text id="2">@item.Title</text> 
    </binding>
  </visual>
</tile>

http://daruyanagi.net/Notifications/0 を叩くと

<tile>
  <visual lang="en-US" version="2">  
    <binding template="TileSquare150x150PeekImageAndText04" branding="name">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/>
      <text id="1">@daruyanagi</text> 
      <text id="2">Surface Pro 3:COM Surrogate や Peer Name Resolution Protcol が妙にリソースを食っている</text> 
    </binding>

    <binding template="TileWide310x150SmallImageAndText04" branding="logo">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/> 
      <text id="1">@daruyanagi</text> 
      <text id="2">Surface Pro 3:COM Surrogate や Peer Name Resolution Protcol が妙にリソースを食っている</text> 
    </binding>
 
    <binding template="TileSquare310x310ImageAndTextOverlay02" branding="logo">
      <image id="1" src="http://daruyanagi.net/Tile.jpg"/>
      <text id="1">@daruyanagi</text> 
      <text id="2">Surface Pro 3:COM Surrogate や Peer Name Resolution Protcol が妙にリソースを食っている</text> 
    </binding>
  </visual>
</tile>

が返る。branding 属性はタイル左下に表示される項目を指定するもので、規定は name のようだ。logo にしておくと favicon が表示される。

これで準備は完了のはず。

注意する点。

  • 画像へのパスが間違ってるとライブタイルは更新されない
  • パスの指定はドメインを含めたすべてを指定しなければならないようだ
  • タイルの version 1 は要らない
  • フォールバック(fallback)属性を書くと、ライブタイルは動作しない

とくに最後の項目は タイル テンプレート カタログ (Windows ランタイム アプリ) - Windows app development からコードをコピペした場合に注意。

<tile>
  <visual>
    <binding template="TileSquarePeekImageAndText02">
      <image id="1" src="image1" alt="alt text"/>
      <text id="1">Text Field 1 (larger text)</text>
      <text id="2">Text Field 2</text>
    </binding>  
  </visual>
</tile>

<tile>
  <visual version="2">
    <binding template="TileSquare150x150PeekImageAndText02" fallback="TileSquarePeekImageAndText02">
      <image id="1" src="image1" alt="alt text"/>
      <text id="1">Text Field 1 (larger text)</text>
      <text id="2">Text Field 2</text>
    </binding>  
  </visual>
</tile>

これをコピペしても動かない。正しくはこんな感じ。

<tile>
  <visual version="2">
    <binding template="TileSquare150x150PeekImageAndText02">
      <image id="1" src="image1" alt="alt text"/>
      <text id="1">Text Field 1 (larger text)</text>
      <text id="2">Text Field 2</text>
    </binding>  
  </visual>
</tile>

WebMatrix:IHttpModule で定期実行を実装する

WebMatrix 3: Twitter Bot (+リアルタイムログ表示付き)でも作ってみる。 - だるろぐ で System.Timers.Timer を使って ASP.NET の定期処理を実装したのだけど、のいえ先生の neue cc - ASP.NETでの定期的なモニタリング手法 でもう一度実装しなおしてみた。

f:id:daruyanagi:20140720222501p:plain

Web.config

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.webServer>
    <modules>
      <add name="SchedulerModule" type="SchedulerModule"/>
    </modules>
  </system.webServer>
</configuration>

Web.config でモジュールを登録する。

~/App_Code/ScheduleModule.cs

using System;
using System.Threading;
using System.Web;

public class SchedulerModule : IHttpModule
{
    static int initializedModuleCount = 0;
    static Timer timer;
 
    public void Init(HttpApplication context)
    {
        var count = Interlocked.Increment(ref initializedModuleCount);

        if (count != 1) return;
 
        timer = new Timer(_ =>
        {
            try
            {
                System.Diagnostics.Debug.WriteLine("だん!");
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine(e.Message);
            }
        }, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
    }
 
    public void Dispose()
    {
        var count = Interlocked.Decrement(ref initializedModuleCount);

        if (count != 0) return;

        var target = Interlocked.Exchange(ref timer, null);

        if (target != null) target.Dispose();
    }
}

SchedulerModule はこんな感じ。IHttpModule インターフェイス(Init、Dispose)を実装する。

f:id:daruyanagi:20140720222920p:plain

(ExpressWeb で動かして initializedModuleCount をインクリメント・デクリメントをメールで通知するようにしてみたらこんな感じ)

SchedulerModule はいくつも作成(&いくつも殺される)ので、ちゃんと数をカウントして、タイマーが大量生産されないように管理する必要があるそうな(← ASP.NET のライフサイクルをちゃんとわかってない。確かライフサイクルを書いたポスターがあったはずなので、今度確認しておこう)。