Published on Blog Developer's Cookbook.
Printer friendly version of http://blog.bulknews.net/cookbook/blosxom/trackback/tb_discovery.html.


Trackback Auto Discovery

by miyagawa at Sun, 07 Dec 2003 22:40

Trackback をサポートしているBlog エントリには、"このエントリの Trackback URL" という表記がしてあります。これをコピーすれば Trackback Ping を打つ URL がわかるというわけですが、はっきりいってメンドウです。こういうものは人手でやるのではなくて、プログラムから自動でわかるようにした方が便利です。というわけで、今回は Trackback Ping URL を自動検出する方法について解説します。


ある Blog に対する Trackback Ping URL は、Trackback 仕様書 に Trackback Auto-Discovery として記述されています。

Trackback Auto-Discovery は、RDF (Resource Description Format) と呼ばれるフレームワークを利用して実装されています。RDF は、Web サイトのメタデータを Machine Readable にするという"セマンティックWeb" のベースとなる、メタデータ記述フォーマットです。

各 Blog に関連する Trackback Ping URL を、RDF データ内に記述し、 HTML 内に埋めこみます。サンプルを見た方が理解が早いでしょう。このエントリの RDF データは以下のようになります。

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:dc="http://purl.org/dc/elements/1.1/"
         xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
<rdf:Description
  rdf:about="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_discovery.html"
  trackback:ping="http://blog.bulknews.net/cookbook/trackback/trackback_tb_discovery"
  dc:title="Trackback Auto Discovery"
  dc:identifier="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_discovery.html" />
</rdf:RDF>

このように rdf タグ内に Description という単一要素タグで、Blog エントリのメタデータについて記述しています。ここでは先頭の xmlns 宣言で、RDF 内に 3 つの名前空間を定義しています。
名前空間URI概要
rdfhttp://www.w3.org/1999/02/22-rdf-syntax-ns#Resource Description Format
dchttp://purl.org/dc/elements/1.1/Dublin Core 1.1
trackbackhttp://madskills.com/public/xml/rss/module/trackback/Trackback Ping
RDF に trackback モジュールをインポートし、

  trackback:ping="http://blog.bulknews.net/cookbook/trackback/trackback_tb_discovery"

の部分で、Ping URL を指定しています。ちなみに dc:title にはエントリのタイトル、dc:identifier にはエントリの Permalink を指定しています。rdf:about については、意味的にいろいろな指定方法が考えられますが、ここでは "Trackback Ping URL についてのメタデータ" であると考え、Trackback Ping URL を指定しています。

このような RDF タグを、Blog ツールのテンプレートなどを編集して HTML 内に埋めこみます(*1)。ただ、XHTML 内にこうした RDF タグを埋めこんだ場合、User-Agent によっては表示に不具合を起こす場合があるので、この RDF を HTML エスケープするのが一般的なようです(*2)

RSS Auto Discovery では HTML の link タグに URL を指定していたことと比べてみると面白いですね。

サンプルコード

こうして埋めこまれた Trackback Ping URL を自動的に検出するスクリプトを書くと、List 1 のようになります。このスクリプトは Trackback 仕様書 に記述されていたサンプルコードを基に、MovableType の Bookmarklet 部で使用されているものを参考にして記述しています(*3)

my $url      = shift;
my $find_all = shift;
このスクリプトは、Blog エントリ(あるいはインデクスページ) の URL を渡すと、関連した Trackback Ping URL を表示するスクリプトになっています。引数は Blog エントリの URL と、複数の Ping URL があった場合、それをすべて表示するかどうかのフラグになります。

my @pings = discover_tb($url, $find_all);
for my $ping (@pings) {
    print "$ping->{url}\n($ping->{title})\n";
}
discover_tb で Ping URL を検出し、見つかれば Ping URL をタイトルとともに表示します。

sub discover_tb {
    my($url, $find_all) = @_;
    my $html = get($url) or die "Can't GET $url\n";
URL を LWP::Simple を使用して HTTP GET します。取得できなければエラー終了です。

    # no-anchor version for matching
    (my $url_no_anchor = $url) =~ s/#.*$//;
$url のアンカー部分を削除した $url_no_anchor を用意します。例えばエントリの URL が http://example.com/1234.html#more 等だった場合に、このアンカー部がない URL でマッチした場合にも、検出するためです。

    while ($html =~ m!(<rdf:RDF.*?</rdf:RDF>)!sg) {
        my $rdf = $1;
        my $ping_url;
        my($permalink) = $rdf =~ m!dc:identifier="(.+?)"!;
HTML から RDF 部分をマッチさせ、さらにそこから dc:identifier を拾って Permalink となる URL を抜き出します。

        # Check if $pemalink matches to my $url
        next unless $find_all || $permalink eq $url
            || $permalink eq $url_no_anchor;
Permalink が、引数となる URL と同じかどうかを比較します。これがマッチしない場合には、対象でないので次のマッチを処理します。$find_all (スクリプトの第2引数) が指定されているときは、Permalink にマッチするかどうかに関わらずすべての Trackback Ping URL を抜き出します。

        # find Trackback URLs, fallback to rdf:about
        if ($rdf =~ m!trackback:ping="(.+?)"!) {
            $ping_url = $1;
        } elsif ($rdf =~ m!about="(.+?)"!) {
            $ping_url = $1;
        }
trackback:ping 属性を抜き出して Ping URL を取得します。RDF内に trackback モジュールを宣言していない場合には、rdf:about で代用します。

        my($title) = $rdf =~ m!dc:title="(.+?)"!;

        push @items, { title => $title, url => $ping_url };
        last unless $find_all;
dc:title でタイトル文字列を取得します。タイトルと Ping URL をハッシュリファレンスにして @items に格納します。$find_all がある場合は、次の RDF も処理しますが、そうでない場合は1個見つかれば OK なので、ループを抜けます。

実行例

この Blog の最初のエントリで実行してみます。

% ./tb_discovery.pl http://blog.bulknews.net/cookbook/blosxom/common/readme_first.html
http://blog.bulknews.net/cookbook/trackback/common_readme_first
 (About This Blog (Blog Developer's Cookbook))

のように、エントリのタイトルと Ping URL が出力されます。インデクスページに対して実行すると、

% ./tb_discovery.pl http://blog.bulknews.net/cookbook/

のように何もマッチしませんが、2番目の引数に 1 を渡せば、

% ./tb_discovery.pl http://blog.bulknews.net/cookbook/ 1 
http://blog.bulknews.net/cookbook/trackback/trackback_tb_rss
(Trackback Ping 一覧を RSS 出力する)
http://blog.bulknews.net/cookbook/trackback/trackback_tb_receive
(Trackback Ping を受信する)
http://blog.bulknews.net/cookbook/trackback/trackback_tb_send
(Trackback Ping を送信する)
http://blog.bulknews.net/cookbook/trackback/common_wiki_created
(WikiWikiWeb Created)
http://blog.bulknews.net/cookbook/trackback/rss_rss2js
(RSS feed を JavaScript で HTML に埋め込む)
http://blog.bulknews.net/cookbook/trackback/rss_rss2htmlmail
(RSS をメールで送信する (HTML メール編))
http://blog.bulknews.net/cookbook/trackback/rss_genfeed
(genfeed - 汎用 RSS ジェネレータ)
http://blog.bulknews.net/cookbook/trackback/opml_blogrolling2opml
(BlogRolling を利用して RSS 巡回先を管理 (OPML編))
http://blog.bulknews.net/cookbook/trackback/rss_blogrolling2rss
(BlogRolling を利用して RSS 巡回先を管理)
http://blog.bulknews.net/cookbook/trackback/rss_rss2email
(RSS をメールで送信する)

のようにそのページ内に含まれるすべての Ping URL が取得できます(*4)

Hack the Hacks

Net::TrackBack では、discover メソッドで Trackback Ping URL の検出を行うことができます。ただし、このメソッドでは URL の抽出のみしか行うことはできず、この例のように dc:title も一緒に取得することはできません。

Listings

List 1: tb_discovery.pl
#!/usr/local/bin/perl -w
# tb_discovery - Trackback Auto Discovery

use strict;
use LWP::Simple;

my $url      = shift;
my $find_all = shift;

my @pings = discover_tb($url, $find_all);
for my $ping (@pings) {
    print "$ping->{url}\n($ping->{title})\n";
}

sub discover_tb {
    my($url, $find_all) = @_;
    my $html = get($url) or die "Can't GET $url\n";

    # no-anchor version for matching
    (my $url_no_anchor = $url) =~ s/#.*$//;
    my @items;
    while ($html =~ m!(<rdf:RDF.*?</rdf:RDF>)!sg) {
        my $rdf = $1;
        my $ping_url;
        my($permalink) = $rdf =~ m!dc:identifier="(.+?)"!;

        # Check if $pemalink matches to my $url
        next unless $find_all || $permalink eq $url
            || $permalink eq $url_no_anchor;

        # find Trackback URLs, fallback to rdf:about
        if ($rdf =~ m!trackback:ping="(.+?)"!) {
            $ping_url = $1;
        } elsif ($rdf =~ m!about="(.+?)"!) {
            $ping_url = $1;
        }

        my($title) = $rdf =~ m!dc:title="(.+?)"!;

        push @items, { title => $title, url => $ping_url };
        last unless $find_all;
    }

    return @items;
}
*1) MovableType ではデフォルトでこの記述がテンプレートに埋めこまれます。
*2) せっかく RDF に準拠しているのに、こうやって ad-hoc に回避するのは、あまり美しくないと反発する人もいるようです。
*3) MovableType のコードは GPL 等ではないライセンスを採用しているため、そのまま引用という形にはしていません。
*4) UTF-8 で出力されるので、端末で表示できないかもしれません。その場合は iconv -f utf-8 -t euc-jp 等でフィルタするとよいでしょう。

Copyright©2002-2003 Tatsuhiko Miyagawa