2009/06/22


IRCのWebChatと言えば、Mibbit.comが有名ですが、Freenode自身がqwebircという、TwistedMootoolsを使ったIRCチャットサーバアプリを持っている事に気づきました。
freenode Web IRC (qwebirc)

Connect to freenode IRC

http://webchat.freenode.net/
Webページへの埋め込みもウィザード付きで簡単です。チャネル#Vim-users.jpであればこんな感じの埋め込みでいけます。

続きを読む...

Posted at by



2009/06/03


どっちかって言うと前向きな発言が好きかな。私は

Web進化論も読んでないし、私本人Web屋でもない。ただ後ろ向きな話が多くて滅入る。
実験的なサービスを出したり、模索したりするのは良い事。ただ新しくサービスを作ると既存ユーザから「あーだこーだ」言われる事もあるだろう。
そんな中最近感じているのは、はてなは今後既存ユーザを無視したサービスを作って行くべきなんじゃないかと。

物理的にも切り離して良いと思う。新しくサービス作って、はてなのアカウントは引き継がず。さらに言うなら、既存ユーザに告知もせず後から「へぇ、これはてなが作ってたんだ」なんてのも良いと思う。
ラボなんかで出てくるサービス見てても既存ユーザありきで盛り上がってるだけだし、おおよそ検討が付いてしまうサービスは数日で飽きてしまい、いつもの辛口ユーザに叩かれてブクマ炎上し、まだ使ってもなかったユーザに悪い印象垂れ流し状態になるまで半月も掛からないだろう。

じゃぁ何を作ればいいか。とても漠然として難しいお題だろうけど、コンテンツはサービスが作るんじゃなく、ユーザが作る物。それをどう引き出すかでサービスの良し悪しが決まるはず。
これはWebが進化したとしても変わらないよね?
例えば、馴れ合った既存ユーザを新しいサービスに持ち込んだとしても、新規サービス開始時に見られる「誰だこれ」「面白いじゃんこの人」とか全く無いだろうし、新規サービスにありがちな「サービス規約を無視したユーザと、自警団」みたいな物は既存ユーザは既にやってしまっていて、ある意味面白みがない。既存ユーザは新しいサービスが出たとしても、その中で一定の認知度を獲得してしまうと納得してしまって使わなくなってしまうんだよね。アカウントの早取り合戦や新規ユーザ同士でルール作って行ったりしてサービスの色が付いて行くんじゃないかな。新規のサービスで盛り上がるまでを体験した事のある人なら分かるよね?

別に自分の所で全部作らなくてもいいと思う。Googleだって色んなサービスは別の会社が作ってた物を買い取ってGoogleブランドにしてたりするし、「はてなだってやっちゃえ!」って思うな。なんていうかびっくり出来ないんだよな。
言ってしまうと既存ユーザに見きられる程度の行動範囲しか取れてないんじゃないかと。

Web屋でも無い私に言われる筋合いもないだろうし、内情知らない部外者が何を言ってるんだと言われるかもしれない。

だとしてもやっぱり思うのは、既存ユーザに見られながら作られる新規サービスには高揚感が無い。


んー。オチも結論も無い話になるけど、言って置きたいのは、はてないっそはてなアカウントを切り離したサービスを作ってみるのが面白いんじゃないかなと思うなー。
Posted at by



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