2009/05/22


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;
    });
});
簡単にプロフィール情報を表示するだけの物です。
以下実行例です。

続きを読む...

Posted at by



2009/04/16


はてなブックマークをdeliciousに同期する場合、どうやってますか?
  • 同時ポストツール?
  • Plagger?
  • まさか、手作業?
同時ポストツールの場合、確かにその場で同時にポスト出来て便利ですね。でも携帯ではてなブックマークから登録した場合、同期されませんよね。 Plaggerだと、cronで動き続けるPCが要りますよね。家に24時間稼動可能なPC無いよ!なんて人いるかもしれません。
手作業?問題外!

先日、Google App Engineにcronが導入されました。
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™

Cron support: schedule tasks like report generation or DB clean-up at an interval of your choosing.

http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
つまり...
  1. cronでイベント発生
  2. はてなブックマークからRSS取得
  3. 内部のデータベースから既にポスト済みでないか確認
  4. deliciousにポストする
  5. モテモテ
って事で作ってみました。
mattn's hatenabookmark-meets-delicious at master - GitHub

post hatenabookmark to delicious periodically using google app engine

http://github.com/mattn/hatenabookmark-meets-delicious/tree/master
Google App Engineのアカウントを作成後アプリケーションを作成します。
app.yamlの application: your_app_name
version: 1
runtime: python
api_version: 1

handlers:
  - url: /tasks/sbm-sync
    script: sbm-sync.py

  - url: /
    script: sbm-sync.py

  - url: /favicon.ico
    static_files: static/images/favicon.ico
    upload: static/images/favicon.ico
    mime_type: image/x-icon

  - url: /static
    static_dir: static
your_app_nameの部分を作成したアプリケーションIDに書き換えて下さい。次にmy-config.yaml.exampleをmy-config.yamlにコピーし hatena_user : your_hatena_user
delicious_user : your_delicious_user
delicious_pass : your_delicious_password
timezone: JST
timeoffset: 9
あなたの、はてなブックマークIDとdeliciousユーザID/パスワードに書き換えて下さい。
あとはサーバにアップロードすれば10分毎に、はてなブックマークのRSSを取得してdeliciousに同期されます。
gae-sbm-sync

よろしければ使ってみて下さい。ソースはgithubにあるのでpatch welcomeです。
Posted at by




SQLite便利!
SQLite3におけるREGEXP演算子 - anon_193の日記

SQLite では、load_extension 関数を用いて、外部の拡張モジュールをロードすることが出来る。拡張モジュールは、いわばユーザ関数ライブラリで、SQLite3 ODBC Driver には標準で BLOB二次元マッピング拡張(sqlite3_mod_blobtoxy.dll)、外部データ取込・出力拡張(sqlite3_mod_impexp.dll)、全文検索拡張(sqlite3_mod_fts3.dll) が付属している。これらと同様にして、正規表現マッチングを行う regexp ユーザ関数を持つ拡張モジュールを制作し、ロードすれば、お目当ての REGEXP 演算子が使えるわけだ。

http://d.hatena.ne.jp/anon_193/20090114/1231935112
sqlite3_mod_regexp.cxx
#include <boost/regex.hpp>
#include <sqlite3ext.h>
extern "C" {
    SQLITE_EXTENSION_INIT1
    static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
        if (argc >= 2) {
            const char *target  = (const char *)sqlite3_value_text(argv[1]);
            const char *pattern = (const char *)sqlite3_value_text(argv[0]);
            try {
                boost::regex ereg(pattern, boost::regex_constants::perl);
                sqlite3_result_int(context, boost::regex_search(target, ereg));
            } catch (boost::regex_error &e) {
                sqlite3_result_error(context, e.what(), 0);
            }
        }
    }
    __declspec(dllexport) int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
        SQLITE_EXTENSION_INIT2(api);
        return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, (void*)db, regexp_func, NULL, NULL);
    }
}
VCでコンパイルしました。
# cl /EHsc -Isrc -I. -I "c:\boost_1_35_0" sqlite3_mod_regexp.cxx /LD "C:\boost_1_35_0\libs\regex\build\vc80\libboost_regex-vc80-mt-s-1_35.lib"
サンプルデータ
foo.sql
CREATE TABLE foo(id integer primary key, value text);
INSERT INTO "foo" VALUES(1,'abc');
INSERT INTO "foo" VALUES(2,'def');
INSERT INTO "foo" VALUES(3,'あいうえお');
INSERT INTO "foo" VALUES(4,'かきくけこ');
INSERT INTO "foo" VALUES(5,'さしすせそ');
utf-8で保存して下さい
# cat foo.sql | sqlite3 foo.db
そしてPerlのコード
use strict;
use warnings;
use utf8;
use YAML;
use DBIx::Simple;

my $db = DBIx::Simple->connect("dbi:SQLite:dbname=c:/foo.db", "", "")
    or die DBIx::Simple->error;

$db->func(1, "enable_load_extension");

my $result = $db->query("select load_extension('/sqlite3_mod_regexp.dll')")
    or die DBIx::Simple->error;
warn Dump $db->query("select * from foo where value regexp '^[あか]'")->hashes;
dbのパスとdllのパスは指定して下さい
実行すると...
---
id: 3
value: あいうえお
---
id: 4
value: かきくけこ
スゲーーーー便利!
Posted at by