JAMADAM.COM

RSS Subscribe to my RSS feed

Stripe Calendar

Sep, 2010
Aug 1516171819202122232425262728293031 Sep 123456789

Entry: ついったらータグs API(仮)

ついったらータグs API(仮)

Initial post: 2009.11.03 | Last modified: 2009.11.05

「ついったらータグs API(仮)」は、ついったらータグs(仮)のデータベースへの問い合わせをアプリケーションから利用するためのインターフェースを提供します。ついったらータグs(仮)のデータベースにはTwitterの各ユーザーページに対する、はてなブックマークタグの情報を格納しています。本APIを利用することで、任意のユーザーに対するタグ付けの情報はもちろんのこと、任意のタグがどのユーザーに適用されているかという情報を取得することができます。また、対象となるデータベースとして、独自のルールでタグ文字列の正規化を行ったテーブルを対象にすることもできます(任意)。

 全てのAPIは下記のURLで提供されます。

http://jamadam.com/th/

 現在サポートされている操作は、タグ-ユーザー検索(パラメータt=api/tag.json)とユーザー-タグ検索(パラメータt=api/user.json)です。いずれもレスポンスとしてJSONを返します。また、クエリーとしてcallbackを渡すことでJSONPを受け取ることもできます。

 

タグ-ユーザー検索

任意のタグ文字列を指定することで、このタグがつけられたユーザーのスクリーンネームのリストを得ます。

パラメータ

  • t (必須)
    "api/tag.json"を指定します。
  • str (必須)
    任意のタグを指定します。
  • nn (オプション)
    正規化されたテーブルを使用しないためのパラメータです。1を指定すると、はてなブックマークに登録された通りのタグ文字列が対象となります。デフォルトは0です。

 リクエスト例

/th/?t=api/tag.json&str=it

レスポンス例

{"result":[
{"user":"takapon_jp","occurance":"2"},
{"user":"m_kumagai","occurance":"1"},
{"user":"huehara88","occurance":"1"}
]}

 ※実際はレスポンスに改行は含まれません。

 

ユーザー-タグ検索

任意のスクリーンネームを指定することで、このユーザーに付与されたタグの一覧を得ます。

パラメータ

  • t (必須)
    "api/user.json"を指定します。
  • str (必須)
    任意のスクリーンネームを指定します。
  • nn (オプション)
    正規化されたテーブルを使用しないためのパラメータです。1を指定すると、はてなブックマークに登録された通りのタグ文字列が対象となります。デフォルトは0です。

リクエスト例

/th/?t=api/user.json&str=jamadam

レスポンス例

{"result":[
{"tag":"twitter","occurance":"38"},
{"tag":"音楽","occurance":"9"},
{"tag":"有名人","occurance":"4"},
{"tag":"webサービス","occurance":"4"}
]}

※実際はレスポンスに改行は含まれません。

 

実装例

ユーザー-タグ検索を利用してタグ一覧を取得するPerlコードは以下のようになります。

use LWP::UserAgent;
use JSON::XS;

my $tags = _twitterer_tags('jamadam');
foreach my $entry (@$tags) {
    print($entry->{tag});
    print(":");
    print($entry->{occurance}."回");
    print("¥n");
}

sub _twitterer_tags {
    my $user = shift;
    my $ua = LWP::UserAgent->new;
    my $res = $ua->get('http://jamadam.com/th/?t=api/user.json&str='. $user);
    my $obj = decode_json $res->content();
    return $obj->{result};
}

 

Entry: GreaseMonkey用pbtweetでIDNを正常動作させる

GreaseMonkey用pbtweetでIDNを正常動作させる

Initial post: 2009.10.12 | Last modified: 2009.10.12

縮.jpがFirefoxのGreaseMonkyスクリプト「pbtweet」のv1.4.10 GreaseMonkey 005810で動作しなかったので、pbtweet側を直してみた。

pbtweet_revamped.user.js

[2009.10.12追記] 本家に取り込んで頂いたのでこちらから最新版をDLするといいと思います。ちなみに、同様の現象はTwitter本家の検索結果でも起きていて、pbtweetの最新版を使うとこれを回避できます。

231-236行に下記を追加。

//jamadam added below
var links = 
    entry[i]
        .getElementsByClassName('entry-content')[0]
        .getElementsByTagName('a');
for (var cnt = 0; cnt < links.length; cnt++) {
    links[cnt].href=decodeURI(links[cnt].href);
}
// jamadam added above

 

うちだけの問題かもしれないけど、Mac版Firefox3.5.3のJavascriptでinnerHTMLを取得した場合、子要素のaタグのhref属性が、パーセントエンコードされてしまっている。上記ではこの現象をキャンセルしている。

HTML

<body id="main">
<a title="縮" href="http://縮.jp/">http://縮.jp/</a>
</body>

Javascript

var hoge1 = document.getElementById('main').innerHTML;
var hoge2 = document.getElementById('main').getElementByTagName('a')[0].href;
alert(hoge1);
alert(hoge2);

Firefoxの場合

<a title="縮" href="http://%E7%B8%AE.jp/">http://縮.jp/</a>
http://縮.jp/

Safariの場合

<a title="縮" href="http://縮.jp/">http://縮.jp/</a> 
http://縮.jp/

 

 

Entry: 開発小ネタ集その1 - 欠番取得SQL

開発小ネタ集その1 - 欠番取得SQL

Initial post: 2009.10.10 | Last modified: 2009.10.10

開発小ネタ集その1。Postgresqlで、基本連番だけどたまに欠番がある場合に、欠番を再利用するSQL。

SELECT
    COALESCE(MIN(TBL.num), 0) + 1 as nextval
FROM
    data AS TBL
    LEFT JOIN data AS TMP
    ON TBL.num + 1 = TMP.num
WHERE
    TMP.num IS NULL;

PRIMARY KEY numが1,2,4,5という並びであれば3を返す。COALESCEでテーブルが空の場合にも対応。

 

Entry: 縮 REST API

縮 REST API

Initial post: 2009.07.25 | Last modified: 2009.10.10

「縮 REST API」は、URL短縮/転送サービス「縮.jp」の機能を、アプリケーションから利用するためのインターフェースを提供します。

全てのAPIは下記のURL以下で提供されます。

http://api.xn--jj0a.jp/

現在サポートされている操作は、短縮URLの生成(generate.jsonへのPOST)と短縮URLの展開(lookup.jsonへのGET)です。いずれもレスポンスとしてJSONを返します。また、クエリーとしてcallbackを渡すことでJSONPを受け取ることもできるかもしれません。

 

GENERATE 

任意のURLを渡すことで、新規に短縮URLを生成します。

パラメータ

  • fUrl(必須)
    短縮対象のURL。http://かhttps://で始まる必要があります。
  • customId(オプション)
    任意の漢字の組み合わせで短縮URLの識別子を指定します。utf8でパーセントエンコードしてください。
  • offset(オプション)
    fUrlで指定したURLに対する既設の短縮URLがひとつ以上存在する場合、このパラメータに0以上の数値を指定することで任意の候補を選択することができます。既設URLは文字数の短い順に並んでいます。offsetが既設URLの数より大きいとき、新しいURLが生成されます。このパラメータは特殊な目的がない場合は指定しないでください。

 

リクエスト例

POST /generate.json

fUrl=http://example.com&id=%E4%B8%83

レスポンス例

{"result":{"fUrl":"http://example.com/","generated":"http://縮.jp/七"}}

 

LOOKUP

既存の短縮URLの識別子を受け取り、元のURLを返します。識別子はUTF-8でパーセントエンコードしてください。

パラメータ

  • id(必須)
    URLの識別子を指定します。
  • hash
    後方互換性のための、idの別名です。(廃止の予定)

リクエスト

GET /lookup.json?id=%E4%B8%83

レスポンス

{"result":{"fUrl":"http://example.com"}}

 

共通

HTTPレスポンスコード

正常/エラーに関わらず「200 OK」を返します。

エラー時のレスポンスボディ

下記の書式でJSONデータを返します。

{"error":"[エラーコード] [説明]"}

 

 エラーコード一覧

  • 400: generateの際、fUrlの書式が正しくない、customIdに不正な文字が含まれる、など。
  • 409: generateの際、customIdがすでに使用されている、など。
  • 404: lookupの際、指定のidが存在しない、など。

 

実装例

generate.jsonを利用して短縮URLを取得するPerlコードは以下のようになります。

use LWP::UserAgent;
use JSON::XS;

my $generated = _shuku('http://example.com');

sub _shuku {
    my $ua = LWP::UserAgent->new;
    my $res = $ua->post('http://api.xn--jj0a.jp/generate.json', {'fUrl' => $_[0]});
    my $obj = decode_json $res->content();
    my $decoded = $obj->{result}->{generated};
    utf8::decode($decoded);
    
    return $decoded;
}

 

Entry: Perlで差分バックアップスクリプト

Perlで差分バックアップスクリプト

Initial post: 2009.06.24 | Last modified: 2009.08.15

[2009-08-15] バキューム機能つけた

2006年にpdumpfsっぽいPerlスクリプトを書いた。glastreeの存在は知っていたけど、敢えて自分で作ってみた。それ以来、仕事でもプライベートでも重宝していたんだけど、最近問題発生。職場のファイルサーバーには百万越えのファイル数があって、毎日数万の差分が発生し、バックアップに5時間かかっていた。こいつはまずいということで手直しした。結果、取りあえず、自宅では以前の10倍高速になった。

ちなみに、今回はglastreeのソースを追って参考にしてみたらほぼ同じ内容になってしまったんだけど、glasstreeとの違いは下記のとおり。

  • バックアップの管理が日付に依存しないので任意の頻度(例えば2時間おきとか)で実行できる
  • コピーされたログを残す
  • コアモジュール以外に依存しない
  • 差分がない場合はバックアップを自動削除

ログが残るってのはなかなか便利です。差分リスト=作業履歴なので。

使い方

perl ddump.pl [バックアップ対象] [保存場所]

バックアップポイントの数の上限を指定。あぶれた分は古い方から削除される。

perl ddump.pl --vacuum=400 [バックアップ対象] [保存場所]

ソース

use strict;
use warnings;
use utf8;
use English;
use Getopt::Long;
use File::stat;
use File::Copy;
use File::Path;

binmode(STDIN,  ":utf8");
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
use open IO  => ":utf8";

use vars qw(%QUERY %stat_idx); # 設定
use vars qw($log $copy_count $file_count $symlink_count); # ログ関係変数
use vars qw($tgt_path $newdir $olddir); # ディレクトリ

main();

sub main {
    %stat_idx = ('mode' => 2, 'size' => 7, 'utime' => 9);

    %QUERY = ('mode' => 1, 'size' => 1, 'utime' => 1);
    GetOptions(\%QUERY, 'mode=s', 'size=s', 'utime=s', 'vacuum=i');

    ### 引数の整備と検証
    $ARGV[0] = &fixPath($ARGV[0]);
    $ARGV[1] = &fixPath($ARGV[1]);
    -d $ARGV[0] or die "$ARGV[0] not found";
    -d $ARGV[1] or die "$ARGV[1] not found";
    ### 引数を分離
    ($tgt_path, my $tgt_dir) = ($ARGV[0] =~ m!(.*)?/([^/]+)$!);

    if (opendir(DIR, "$ARGV[1]")) {
        my @dirs = sort grep{ -d "$ARGV[1]/$_" && /^\d+$/} readdir( DIR );
        close(DIR);
        ### 古いディレクトリを削除
        if ($QUERY{vacuum}) {
            for (my $i = 0; $i < $#dirs - $QUERY{vacuum}; $i++) {
                #print $dirs[$i] . " ";
                rmtree("$ARGV[1]/$dirs[$i]");
            }
        }

        ### 前回バックアップの時刻ディレクトリ名を取得
        $olddir = "$ARGV[1]/". (pop(@dirs) or '');
    }

    ### 時刻ディレクトリの作成
    my ($sec, $min, $hour, $mday, $mon, $year) = localtime($BASETIME);
    my $new_time =
        sprintf('%04d%02d%02d%02d%02d%02d', $year+1900, $mon+1, $mday, $hour, $min, $sec);
    $newdir = "$ARGV[1]/$new_time~";
    umask 0;
    mkdir($newdir, 0755) or die "cannnot make $newdir";

    ### ログファイル生成
    open($log, ">>$newdir/ddumplog.txt") or die "cannnot make log file";
    print $log <<EOF;
-----------------------------------------------------------------
-- ddump -- $new_time
-----------------------------------------------------------------
EOF

    ### ログ記録用変数
    $copy_count = 0;
    $file_count = 0;
    $symlink_count = 0;

    ### バックアップ
    &backup($tgt_dir);

    ### ログファイルに結果出力
    print $log "$copy_count/$symlink_count/$file_count (Copyed/symlink/Total)\n";
    print $log (time() - $BASETIME). "sec passed\n";
    close($log);

    if (! $copy_count) {
        ### コピーなしならディレクトリ削除
        rmtree($newdir);
    } else {
        ### ファイル名確定
        rename($newdir, substr($newdir, 0 ,length($newdir) - 1));
    }
}

### --------------------------------------------
### 再帰的ディレクトリバックアップ
### --------------------------------------------
sub backup {

    my $cwd = shift;
    my $tgt_cwd = "$tgt_path/$cwd";
    my $old_cwd = "$olddir/$cwd";
    my $new_cwd = "$newdir/$cwd";

    ### ディレクトリの作成
    &copydirstat($tgt_cwd, $new_cwd);

    ### カレントディレクトリ内の全ファイルを配列に格納
    opendir(DIR, $tgt_cwd) or return 0;
    my @files_and_dirs = readdir(DIR);
    close(DIR);

    my @files = grep { -f "$tgt_cwd/$_" or -l "$tgt_cwd/$_" } @files_and_dirs;

    ### @files_and_dirsのうち、ファイルを処理
    foreach my $a_file (@files) {
        my $old_file = "$old_cwd/$a_file";
        my $new_file = "$new_cwd/$a_file";
        my $tgt_file = "$tgt_cwd/$a_file";
        my $command = 1; # 0: nothing, 1: hard link 2: copy

        if (-l $tgt_file) {
            symlink(readlink($tgt_file), $new_file);
            $symlink_count++;
            $command = 0;
        }

        elsif (-f $old_file) {
            my $old_stat    = stat($old_file);
            my $crnt_stat   = stat($tgt_file);

            ### 更新ありならコピー
            foreach my $key (keys %stat_idx) {
                if ($QUERY{$key} and
                    $old_stat->[$stat_idx{$key}] ne $crnt_stat->[$stat_idx{$key}]) {

                    $command = 2;
                    last;
                }
            }
        } else { ### ファイルがなければコピー
            $command = 2;
        }

        ### コピー
        if ($command == 2) {
            copystat($tgt_file, $new_file);

            $copy_count++;

            ### ログ出力
            utf8::decode($new_file);
            print $log "$new_file\n";
        }

        ### ハードリンク
        elsif ($command == 1) {
            link($old_file, $new_file);
        }

        $file_count++;
    }

    ### @files_and_dirsのうち、ディレクトリを再帰処理
    my @dirs = grep { -d "$tgt_cwd/$_"
                    and not -l "$tgt_cwd/$_"
                    and $_ ne '.'
                    and $_ ne '..' } @files_and_dirs;

    foreach my $a_dir (@dirs) {
        &backup("$cwd/$a_dir") or print "Error at $cwd/$a_dir\n";
    }

    return 1;
}

### --------------------------------------------
### 属性丸ごとファイルコピー
### --------------------------------------------
sub copystat {
    my ($from, $to) = @_;
    my $stat = stat $from;

    copy($from, $to) or die "cannot copy $from to $to";
    chown($stat->uid, $stat->gid, $to) if $EUID == 0;
    chmod($stat->mode, $to);
    utime($stat->mtime, $stat->mtime, $to);
}

### --------------------------------------------
### 属性真似てディレクトリ作成
### --------------------------------------------
sub copydirstat {

    my ($from, $to) = @_;
    my $stat = stat $from;

    mkdir $to, 0555 or die "cannot mkdir $to";
    chown ($stat->uid, $stat->gid, $to) if $EUID == 0;
    chmod ($stat->mode, $to);
    utime ($stat->mtime, $stat->mtime, $to);
}

### --------------------------------------------
### ファイルパスの整備
### --------------------------------------------
sub fixPath {
    my $in = $_[0];
    $in =~ s!^!\./! unless ($in =~ m!^\.*/!);
    $in =~ s!/$!!;
    return $in;
}

 

Entry: はてブ窓にコメント表示した

はてブ窓にコメント表示した

Initial post: 2009.07.29 | Last modified: 2009.07.29

連日、日付越えの激務のため趣味の開発などには全く身が入らない、なんていいつつ、ささやかな成果物。はてブ窓にコメントを表示するようにした。これのためにどうでもいいコメントもつけた。

関係ないけど、はてブatomfeedのofパラメータってなんであんな回りくどい仕様なんだろう。このブログでは必要なデータ取得するのに下記みたいなことをやってみたけど、使い方間違ってる?

var hatebu_epp = 20; // API固定の取得件数
var disp_epp = 5; // 例えば5件表示
var page = 5; // 例えば5ページ目を取得
var ppr = hatebu_epp / disp_epp; // 1リクエストで4ページ分
var request_offset = Math.floor((page - 1) / ppr) * hatebu_epp; // ofパラメータは20
var local_offset = Math.floor((page - 1) % ppr) * disp_epp; // 取得したデータ中のオフセットは0

 

Entry: ブログにはてなブックマーク窓をつけた

ブログにはてなブックマーク窓をつけた

Initial post: 2009.07.14 | Last modified: 2009.07.14

つけた。自分のはてブをブログ上で公開しても意味ないんだけどね。露出狂なの。

ついったー窓のスクリプトを流用すべく、はてブのatomfeedで取得したRSSをjsonに変換する必要があったのでGoogleのfeed APIに丸投げ。最初、全部自前でやろうと思って、クロスドメイン対策用のproxyも作ってみたけど、今回は使用を見送った。別の機会に役に立ちそう。

Entry: AJAXデータの取得先URLをどう管理するかという話

AJAXデータの取得先URLをどう管理するかという話

Initial post: 2009.06.01 | Last modified: 2009.06.01

AJAX満載のサイトでアンカーに紐づいたAJAXデータの取得先URLをどう管理するかという話。with jQuery1.3.2。

HTML

<a href="./path/to/parmalink.html">通常リンク</a>
<a href="./path/to/snippet.html" class="widget-opener">動的コンテンツ</a>

Javascript

$('a.widget-opener').live('click', function () {
    $.get($(this).attr('href'), function(html){
        //appendなりprependなり
    });
    
    return false;
});

上記の場合、javascriptオフの人が「動的コンテンツ」を叩くとHTMLスニペットやjsonテキストが画面いっぱいに広がるページに遷移してしまったりする。 また、検索クローラーはリンク先のページを収集してしまうかもしれない。

かといって、href="#"とでもして、すべてのアンカーにidをふって、script内でURLを関連づけていくのも煩雑だ。

HTML

<a id='id1' href="#" class="widget-opener">記事1</a>
<a id='id2' href="#" class="widget-opener">記事2</a>
<a id='id3' href="#" class="widget-opener">記事3</a>

Javascript

url.id1 = './path/to/snippet1.html';
url.id2 = './path/to/snippet2.html';
url.id3 = './path/to/snippet3.html';

そこで、アンカータグのonclickとjQueryの$.data()メソッドでAJAX URLをインラインで管理するようにしてみた。 $.data()はDOM要素に紐づいたデータを管理することが目的という、本件におあつらえ向きなメソッド。

HTML

<a href="#" class="widget-opener" onclick="$.data(this, 'ajaxURL', './ajax1.html')">記事1</a>
<a href="#" class="widget-opener" onclick="$.data(this, 'ajaxURL', './ajax3.html')">記事2</a>
<a href="#" class="widget-opener" onclick="$.data(this, 'ajaxURL', './ajax3.html')">記事3</a>

Javascript

$('a.widget-opener').live('click', function () {
    $.get($.data(this, 'ajaxURL'), function(html){
        //appendなりprependなり
    });
    
    return false;
});

onclickとliveでバインドしたイベントとの実行順が気になったけど、意図した順番に実行されている。ここで念のためonmousedownとかに逃げてしまうと、 今度は.trigger()が使えなくなってしまうのでonclickしかない。実行順が覆るケースがあるようなら、click.not_inlineというようにnamespacedイベントを利用して onclickイベントとの実行順を制御することもできる。

また、パーマリンクが存在するアンカーであれば、当然hrefにそれをかける。

<a href='./parmalink1.html' class="widget-opener" onclick="$.data(this, 'ajaxURL', './ajax1.html')">記事</a>

こうすることで、新規タブでリンクを開く可能性に配慮した仕組みも実現できる。実際、このブログの「最近の記事」のボタンは左クリックで新規ウィジェットを起動するけど、コンテキストメニューから新規タブでパーマリンクを開くこともできる。そして、この2つのURLをひとつのアンカータグ内で管理している。

まあ、HTML内に書くのには$.data()はちょっとだけ汚らしいというのは認める。

Entry: Photoshopでsocket通信

Photoshopでsocket通信

Initial post: 2009.04.04 | Last modified: 2009.04.04

Adobe ExtendScriptでsocket通信にトライ。結構苦労してサーバーから日本語を含むJSONデータを正常に取得することができた。

国内の解説サイトでsocket通信に言及しているのは2件。うち一件はメールの送信について解説していて、もう一件は日本語サイトの取得に失敗したとのことで、他には見つからない。

成功するポイントは、openメソッドの第二引数は必ずbinaryにすることです。UTF8などを指定すると長いコンテンツが途中で切れてしまう。どうやらマルチバイトのデータサイズをreadメソッドがきちんと処理してくれてないことが原因。また、openメソッドをテキストモードにすると改行コードも変更されてしまってとても混乱。

それから、エンティティボディの処理はきちんとチャンクサイズに基づいて行うこと。マルチバイトのあるなしに関わらず、ある程度以上のサイズを一気にreadまたはreadlnしようとすると途切れる。なので、サンプルスクリプトやAdobeのドキュメントに書いてあるread(999999)みたいなのは実用的には全く使えない。

結局、マルチバイトのエンコードもchunkedなコンテンツのデコードも全て自前の処理になるので、普通に面倒だけど寧ろそれが普通か。なんか以前にもPerlで同じコードを書いた気がする。

Entry: ブックマーク