2009/05/22

Recent entries from same category

  1. 災害報告にtwitterを使うという提案
  2. ぼくが知ってる twitter じゃない
  3. サイトの新着はてなブックマークフィードのdelicious版みたいなのを作った。
  4. microformatsでWebページにメタデータを埋め込む
  5. 今さら聞くのは恥ずかしい「microformatsとは何か?」

YQLを使うと色んなネットワークリソースをさもAPIを扱うかの様に操作でき、幾らでも新しい可能性が生まれて来ます。YQLには初期の状態でYahoo!で扱える色んなテーブル(flickrやdelicious等)が用意されており show tables
と入力することでそのテーブル一覧が表示されます。
yql-community-tables1
また右側のサイドバーにあるテーブル一覧で「Show Comminity Tables」をクリックするとユーザコミュニティが作成した便利なテーブルも扱う事が出来ます。
yql-community-tables2
これらのComminity Tablesはgithubで開発されており、日々新しいデータテーブルが作成されています。
実はこのユーザテーブルは、ネットワーク上にXMLを配置する事が出来る人ならば誰でも作れます。
今日はこのユーザテーブルを自作する手順をご紹介します。

ユーザテーブルはユーザテーブル群を纏めるenvファイルと、実際のクエリを記述するファイルとで構成され、YQLからenvパラメータを使って参照する事が出来ます。例えばユーザコミュニティのテーブルであればenvファイルのURLを指定して以下の様にアクセスされます。
http://developer.yahoo.com/yql/console/?env=http://datatables.org/alltables.env
このenvパラメータで指定されたURLにアクセスしてみて頂けると分かると思いますが形式は以下の様なuse文の羅列になっています。
use 'http://www.datatables.org/amazon/amazon.ecs.xml' as amazon.ecs;
use 'http://www.datatables.org/auth/auth.basic.xml' as auth.basic;
use 'http://www.datatables.org/bitly/bit.ly.shorten.xml' as bit.ly.shorten;
use 'http://www.datatables.org/data/data.html.cssselect.xml' as data.html.cssselect;
このenvファイルと、実際のXMLファイルを用意すれば自分専用のAPIを作る事が出来ます。
今日はサンプルとして、はてなのユーザプロフィールをスクレイピングしてXML(もしくはJSON)を返すAPIを作ってみたいと思います。
まず配置場所としては、Google Page Creatorを使用しました。Google Page CreatorならばGoogleのアカウントさえ作れば誰でも無料でXMLを配置する事が出来ます。
はてなプロフィールを扱うAPIなのでファイル名はhatena.profile.xmlとし、alltables.envファイル use 'http://mattn.jp.googlepages.com/hatena.profile.xml' as hatena.profile;
としました。
次に実際のAPI部ですが、形式は以下の様になります。
<?xml version="1.0" encoding="UTF-8"?>
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
  <meta>
    <author>mattn</author>
  </meta>
  <bindings>
    <select produces="XML" itemPath="">
      <urls>
        <url>http://www.hatena.ne.jp/{id}/profile</url>
      </urls>
      <inputs>
        <key id="id" type="xs:string" paramType="variable" required="true"/>
      </inputs>
      <execute/>
    </select>
  </bindings>
</table>
YQLではXMLの中に書かれたexecuteノードにjavascriptを記述する事ができ、response.objectというオブジェクト変数に値を格納する事で独自のフォーマットを作り出す事が出来ます。
またそこで使うことが出来るjavascriptにはE4X(ECMAScript for XML)が採用されており、XMLを扱いやすくなっています。
今回のはてなプロフィールAPIではhttp://www.hatena.ne.jp/mattn/で表示されるページを組み込み関数y.queryを使ってスクレイピングします。
以下javascript部のコード
var contents = y.query("select * from html where url=@url and xpath=@xpath", {
  url: "http://www.hatena.ne.jp/" + id + "/",
  xpath: "'//dl[@class=\"profile\"]/dd'"
});
var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
var profile =
<profile>
    <user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
    <nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
    <shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
    <longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
    <medals/>
    <addresses/>
    <services/>
</profile>;
for each(var n in contents.results..div.(@['class']=="medals").img) {
    profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
}
for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
    profile.addresses.address +=
    <address>
        <name>{n.th.*.text().toString()}</name>
        <value>{n.td.*.text().toString()}</value>
    </address>;
}
for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
    profile.services.service +=
    <service>
        <name>{n.img.@['title'].toString()}</name>
        <url>{n.@['href'].toString()}</url>
    </service>
}
response.object = profile;
y.queryにはYQLから使うことが出来るselect文を使用する事ができ、xpathを使って絞り込む事も出来ます。ただ今回は一つのAPIで
  • アカウントID
  • ニックネーム
  • 短い紹介
  • 自己紹介
  • アドレス情報
  • 使っているサービス一覧
  • 市民メダル情報
を一度に扱うので、個別にxpathでクエリを投げるのでは無く、一部を除いたページ全体を取得しておいて、E4XのDOM操作でxpathライクな処理を書いています。
例えばE4Xでは、DOMオブジェクトの階層スキップや条件指定検索を contents.results..div.(@['id']=="hatena-body")..dd[2];
等といった書き方をする事が出来ます。つまり無理にxpathで絞り込む必要は無いという事です。
おそらくサーバ側ではrhinoあたりを使っているのかもしれませんね。

XML全体のコードは以下の様になりました。
<?xml version="1.0" encoding="UTF-8"?>
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
  <meta>
    <author>mattn</author>
  </meta>
  <bindings>
    <select produces="XML" itemPath="">
      <urls>
        <url>http://www.hatena.ne.jp/{id}/profile</url>
      </urls>
      <inputs>
        <key id="id" type="xs:string" paramType="variable" required="true"/>
      </inputs>
      <execute><![CDATA[
      var contents = y.query("select * from html where url=@url and xpath=@xpath", {
        url: "http://www.hatena.ne.jp/" + id + "/",
        xpath: "'//dl[@class=\"profile\"]/dd'"
      });
      var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
      var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
      var profile =
      <profile>
          <user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
          <nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
          <shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
          <longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
          <medals/>
          <addresses/>
          <services/>
      </profile>;
      for each(var n in contents.results..div.(@['class']=="medals").img) {
          profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
      }
      for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
          profile.addresses.address +=
          <address>
              <name>{n.th.*.text().toString()}</name>
              <value>{n.td.*.text().toString()}</value>
          </address>;
      }
      for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
          profile.services.service +=
          <service>
              <name>{n.img.@['title'].toString()}</name>
              <url>{n.@['href'].toString()}</url>
          </service>
      }
      response.object = profile;
      ]]></execute>
    </select>
  </bindings>
</table>
あとはこれをYQLから指定してやれば良い事になります。
実際にはここで試す事が出来ます。

さてこれでAPIが出来上がったので、APIを使ったサンプルを書いてみましょう。
$(function() {
    $('#hatena-profile').submit(function() {
        $('#hatena-profile-ajaxloader').attr('src', 'http://mattn.kaoriya.net/images/ajax-loader.gif').show();
        var query = "select * from hatena.profile where id = '" + $('#hatena-id').val() + "'";
        var envurl = "http://mattn.jp.googlepages.com/alltables.env";
        $.getJSON('http://query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent(query) + '&format=json&env=' + encodeURIComponent(envurl) + '&callback=?', function(data) {
            $('#hatena-profile-ajaxloader').hide();
            $('#hatena-profile-user').text(data.query.results.profile.user);
            $('#hatena-profile-nickname').text(data.query.results.profile.nickname);
            $('#hatena-profile-medals').html(data.query.results.profile.medals.medal.join('<br />'));
            $.each(data.query.results.profile.addresses.address, function() {
                var html = $('#hatena-profile-addresses').html();
                $('#hatena-profile-addresses').html(html + this.name + ':' + this.value + '<br />');
            });
            $.each(data.query.results.profile.services.service, function() {
                var html = $('#hatena-profile-services').html();
                $('#hatena-profile-services').html(html + this.name + ':' + this.url + '<br />');
            });
            $('#hatena-profile-info').show('slow');
        });
        return false;
    });
});
簡単にプロフィール情報を表示するだけの物です。
以下実行例です。


APIがあればアプリケーション開発が楽しくなりますね。

皆さんも色んなAPI、作ってみて下さい。
Posted at by