はてなキーワードよろしく記事文中のタグを自動リンクする

Posted by
ぴろり
Posted at
2005/11/05 19:22
Trackbacks
関連記事 (1)
Comments
コメント (16)
Post Comment
コメントできます
Category
プラグイン カテゴリ

 MovableType から、はてなダイアリーキーワードへリンクを貼るプラグインは幾つか存在するようですが、折角に訪問頂いたビジターを外部コンテンツに安易に誘導することは少なからず勿体無い気がします。当サイトでは Tagwire plugin を使って独自にタグデータベースを蓄積していますので、これを上手に使って似たようなことができないでしょうか?
 そこで、ビジターに一つでも多くの記事を参照してもらう機会を増やせるよう、はてなのキーワードリンクよろしく記事文中に現れた文字列に自動的にタグリンクを張れるようにしてみました。

このエントリーをはてなブックマークに追加  

Regexp::TrieEUC の導入

 Regexp::TrieEUC は文字列のリストから TRIE の生成を行い、 それを Perl の正規表現式として変換するためのモジュールです。オリジナルは、 404 Blog Not FoundDan Kogaiさんが、 TRIE-Optimized Regexp で公開されている mk_trie_regexp.pl です。 これを使用することで、大量のキーワード文字列を効率的にパターンマッチ可能な正規表現式を得ることができます。
 今回は、これを日本語(euc-jp)で動作するように少し手を入れています。 以下のコードを(MovableType のインストールパス)/extlib/Regexp/TrieEUC.pmとして保存します (アーカイブがダウンロードできます)
package Regexp::TrieEUC;
;#	original code from
;#		mk_trie_regexp.pl,v 0.1 2005/09/10 20:19:44 dankogai
;#		@see http://blog.livedoor.jp/dankogai/archives/50074802.html
;#	copyright (c) 2005 Piroli YUKARINOMIYA. Some rights reserved.
;#		@see http://www.magicvox.net/archive/2005/11051922.php

use strict;
use warnings;
use Jcode;

sub new{ bless {} => shift }
sub add{
    my $self = shift;
    my $str  = shift;
    my $ref  = $self;
    for my $char (Jcode->new($str)->euc
			=~ /[x00-x7F]|[x8ExA1-xFE][xA1-xFE]|x8F[xA1-xFE][xA1-xFE]/og){
			;# @see http://www.din.or.jp/~ohzaki/perl.htm#JP_Split
		$ref->{$char} ||= {};
		$ref = $ref->{$char};
	}
	$ref->{''} = 1;# as terminator
	$self;
}
sub _regexp{
    my $self = shift;
    return if $self->{''} and scalar keys %$self == 1; # terminator
    my (@alt, @cc);
    my $q = 0;
    for my $char (sort keys %$self){
        my $qchar = quotemeta $char;
        if (ref $self->{$char}){
            if (defined (my $recurse = _regexp($self->{$char}))){
                push @alt, $qchar . $recurse;
            }else{
                push @cc, $qchar;
            }
        }else{
            $q = 1;
        }
    }
    my $cconly = !@alt;
    @cc and push @alt, @cc == 1 ? $cc[0] : '(?:'. join('|', @cc). ')';
    my $result = @alt == 1 ? $alt[0] : '(?:' . join('|', @alt) . ')';
    $q and $result = $cconly ? "$result?" : "(?:$result)?";
    $result;
}
sub as_regexp{ my $str = shift->_regexp; qr/$str/ }

1;

Tagwire plugin の改造

 少し長いですがガンバってください(?)  Ogawa::Memorandaで公開されている 小川宏高さん作の Tagwire pluginに以下のコードを追加します (修正済みアーカイブがダウンロードできます)

 MTTagsAsKeyword の処理については、再びDan Kogaiさん公開の Click'n Hatenizeを 大変参考にさせて頂きましたm(_ _)m ありがとうございます。

;# piroli++, '05/11/04
MT::Template::Context->add_tag('TagsAsRegexp' => &tags_as_regexp);
sub tags_as_regexp {
    my ($ctx, $args) = @_;

	;# retrieving parameters and set default values
    # cutoff_score (0..100, default = 0)
    my $tag_score = $args->{cutoff_score} || 0;
    # cutoff_length (default = 0)
    my $tag_length = $args->{cutoff_length} || 0;
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ? $args->{case_sensitive} : 1;

	;# add up tags
    my $blog_id = $ctx->stash('blog_id');
    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
			or return '';
    my %tindex = %{$data->{tindex}};

    my %tags = ();
    my $most_tags_count = 1;
	foreach (keys %tindex) {
	    my $t = $case_sensitive ? $_ : lc $_;
	    $tags{$t} += scalar @{$tindex{$_}->{eids}};
	    $most_tags_count = $tags{$t} if ($most_tags_count < $tags{$t});
	}

	;# create TRIE
	use Regexp::TrieEUC;
	my $trie = Regexp::TrieEUC->new;
	foreach (keys %tags) {
		$trie->add($_)
				if $tag_length <= length
						and $tag_score <= int (0.5 + $tags{$_} * 100 / $most_tags_count);
	}

	;# return as regular expression
	'qr{'. $trie->as_regexp. '}';
}

MT::Template::Context->add_container_tag('TagsAsKeyword' => &tags_as_keyword);
sub tags_as_keyword {
    my ($ctx, $args, $cond) = @_;

	;# retrieving parameters and set default values
    # repeat_count
    my $kw_repeat = $args->{repeat_count} || 0;
	# pattern
	my $kw_pattern = $args->{keyword_pattern}
			or $ctx->error('no keyword pattern is specified.');

	;# retrieving the regexp pattern of keywords
	my $re;
	if (defined (my $regexp_file = $args->{regexp_file})) {
		;# @see <$MTInclude file="..."$>
		$re = MT::Template::Context::_hdlr_include (
				$ctx, {'file' => $args->{regexp_file}}, $cond);
	} elsif (defined (my $regexp_template = $args->{regexp_template})) {
		;# you can use the file which is built with Index Template
		;# @see <$MTLink template="..."$>
		my $file_path = MT::Template::Context::_hdlr_link (
				$ctx, {'template' => $args->{regexp_template}}, $cond);
		my $site_url = $ctx->stash('blog')->site_url;
		my $site_path = $ctx->stash('blog')->site_path;
		$file_path =~ s/Q$site_urlE/$site_path//;
		$file_path =~ s//+///g;
		$re = do $file_path;
	} elsif (defined (my $regexp_module = $args->{regexp_module})) {
		;# it may be the performance issue when too many tags
		;# but you can control this behaviour by parameters :)
		;# @see <$MTInclude module="..."$>
		$re = MT::Template::Context::_hdlr_include (
				$ctx, {'module' => $args->{regexp_module}}, $cond);
	} else {
		;# it may be the performance issue when too many tags
		;# and be called with using all args as default values :(
		$re = eval tags_as_regexp ($ctx, $args);
	}
	$ctx->error('no regexp is specified.') if (! $re);

	;# build contents within container tag
	defined (my $content = $ctx->stash('builder')->build ($ctx, $ctx->stash ('tokens')))
			or return $ctx->error ($ctx->errstr);

	;# convert $content to EUC temporally because regexp is written in EUC
	use Jcode;
	my $j = jcode ($content);
	my $original_charset = $j->icode;
	$j->can("fallback") and $j->fallback (Jcode::FB_HTMLCREF());
	$content = $j->euc;

	;# should not keywordnize in these tags
	my %ignore_tags = ();
	map { $ignore_tags{lc $_} = 1; } split /,/, 'a,blockquote,pre,textarea';
	defined $args->{ignore_tags}
			and map { $ignore_tags{lc $_} = 1; } split /,/, $args->{ignore_tags};
	defined $args->{apply_tags}
			and map { $ignore_tags{lc $_} = 0; } split /,/, $args->{apply_tags};

	;# Parse the $content for retrieving the text areas that should be given the keywords
	my $output = '';
	my $keywordnize = 0;
	my %tags = ();

	use HTML::Parser;
	my $html_parser = HTML::Parser->new (
		start_h => [ sub {
				my ($tagname, $text) = @_;
				$ignore_tags{$tagname} && $keywordnize++;
				$output .= $text;
		} => 'tagname,text'],
		end_h => [ sub {
				my ($tagname, $text) = @_;
				0 < $keywordnize && $ignore_tags{$tagname} && --$keywordnize;
				$output .= $text;
		} => 'tagname,text'],
		text_h => [ sub {
				my ($text) = @_;
				;# Replace the found keywords with $kw_pattern
				if ($keywordnize == 0) {
					$text =~ s{($re)}{
							my $kw = $1;
							if ($kw_repeat and $kw_repeat < ++$tags{$kw}) {
								$kw;
							} else {
								use MT::Util;
								my $pattern = $kw_pattern;
								$pattern =~ s/%e/MT::Util::encode_url(Jcode::convert ($kw, $original_charset, 'euc'))/oge;
								$pattern =~ s/%k/$kw/og;
								MT::Util::decode_html ($pattern);
							}
					}egx;
				}
				$output .= $text;
		} => 'text'],
		default_h => [""]);
	$html_parser->parse ($content);
	$html_parser->eof;

	;# return contents in the original charset
	Jcode::convert ($output, $original_charset, 'euc');
}
;# ++piroli, '05/09/26

Jcode.pm のアップグレード new

 MovableType のパッケージに添付されている Jcode.pm (3.2ja2 のものはバージョン 0.88)では、 UTF8 などの環境で文字化けが発生する例が報告されています。 Tagwire plugin w/ TrieEUC 0.11 でこの問題は解決されていますが、 これと併せて Jcode.pm のバージョンを 1.99 以上にアップグレードする必要があります。
  1. 新しいバージョンの Jcode.pm パッケージを取得します
  2. MovableType の(インストールディレクトリ)/etlib に含まれる Jcode.pm と Jcode ディレクトリに、取得したアーカイブに含まれる同名のファイル/ディレクトリで上書きします

参考リンク:Shift_JIS に含まれない文字をエスケープ (Jcode.pm編)

追加されるテンプレートタグ

 今回、Tagwire plugin に以下のテンプレートタグを追加しました。
MTTagsAsRegexp
 Tagwire plugin に登録されている全てのタグから、 後述するオプションで指定された条件に合致するタグを表す正規表現式を生成するためのテンプレートタグです。 生成された正規表現式は MTTagsAsKeyword で使用することができます。
cutoff_score
 指定されたスコア未満のタグを正規表現式に含みません。 タグの使用頻度に基づいてフィルタをしたい場合に使用します。 0 から 100 までの整数値を指定します。 指定が無い場合のデフォルト値は 0 で、スコアが 0 点以上のタグ、即ち全てのタグが対象になります。
cutoff_length
 指定された文字列長さ未満のタグを正規表現式に含みません。 0 以上の整数値を指定します。 指定が無い場合のデフォルト値は 0 で、0 文字以上のタグ、即ち全てのタグが対象になります。
case_sensitive
 タグの大文字/小文字の区別を 0 か 1 で指定します。 指定が無い場合のデフォルト値は 1 で、大文字と小文字を区別します。
MTTagsAsKeyword
 文字列中にタグが出現した場合、それをパターンで置き換えるためのコンテナタグです。
repeat_count
 タグを発見した場合、そのタグの repeat_count 回数目の登場までをキーワードリンクとします。 指定がない場合のデフォルト値は 0 で、登場する全ての場合がキーワードリンクになります。
keyword_pattern
 発見したタグを指定されたパターンで置き換えます。 置換パターン中の HTML エンティティはエスケープしておいてください。 パターン中で以下の文字列を使用することができます。
%k
該当するタグそのものに置換されます。
%e
該当するタグを URL エンコードしたものに置換されます。
keyword_pattern="&lt;a href=&quot;/tag/%e&quot;&gt;%k&lt;/a&gt;"
regexp_file
 指定されたファイルの内容をタグ検索のための正規表現式として使用します。 その動作は <$MTInclude file="..."$> に準じます。
regexp_template
 指定されたインデックステンプレートによって生成されたファイルの内容をタグ検索のための正規表現式として使用します。 その動作は <$MTLink template="..."$> に準じます。
regexp_module
 指定されたテンプレートモジュールの構築結果を、タグ検索のための正規表現式として使用します。 その動作は <$MTInclude module="..."$> に準じます。 構築のたびに正規表現式の生成が行われるため、タグ数が増えた場合にパフォーマンス上の問題となる可能性があります。

regexp_file、regexp_template、regexp_module が何れも指定されていない場合、 内部的にデフォルトパラメータを用いて MTTagsAsRegexp を呼び出します。 そのため、構築のたびに正規表現式の生成が行われるため、 タグ数が増えた場合にパフォーマンス上の問題となる可能性があります。

ignore_tags
 デフォルトで a,blockquote,pre,textarea,script 内部に含まれるテキストにはタグリンクを生成しません。 他に追加していしたい HTML タグがある場合、それらのタグをコンマで区切って指定することができます。
ignore_tags="span,address"
apply_tags
 ignore_tags とは逆に、その内部でタグリンクを生成させたい HTML タグをコンマで区切って指定することができます。
apply_tags="a,textarea"

これらの指定はデフォルトのignore_tags < ignore_Tags < apply_tags の順に優先されます。

正規表現式の生成とテンプレートタグの修正

 タグを抽出するための正規表現式を生成し、この正規表現式を使って文章中のタグにリンクを与えます。 これには MTTagsAsRegexpMTTagsAsKeyword を使用します。

case 1. 正規表現式が外部ファイルの場合

 別途ファイルとして用意された正規表現式の、そのファイル名を指定する方法です。 この場合は MTTagsAsRegexp を使用しません。 MTTagsAsKeyword の regexp_file オプションでファイル名を指定します。
<MTTagsAsKeyword regexp_file="tags.rx"
		keyword_pattern="&lt;a href=&quot;/tag/%e&quot;&gt;%k&lt;/a&gt;">
<$MTEntryBody$>
</MTTagsAsKeyword>

case 2. インデックステンプレートを使用する場合

 MTTagsAsRegexp をインデックステンプレート内で使用し、テンプレートを構築することで正規表現式をファイルに書き出します。 書き出されたファイルは MTTagsAsKeyword から regexp_template オプションで、 そのテンプレート名を指定することで使用することができます。
<$MTTagsAsRegexp cutoff_score="50" cutoff_length="2" case_sensitive="1"$>
<MTTagsAsKeyword regexp_template="Tags Regexp Template"
		keyword_pattern="&lt;a href=&quot;/tag/%e&quot;&gt;%k&lt;/a&gt;">
<$MTEntryBody$>
</MTTagsAsKeyword>

case 3. モジュールテンプレートを使用する場合

 MTTagsAsRegexp をモジュールテンプレート内で使用し、再構築毎に正規表現式を生成します。 正規表現式は MTTagsAsKeyword の regexp_module オプションで指定します。 MTTagsAsKeyword が出現するたびに、内部的に MTTagsAsRegexp が呼ばれるため、 パフォーマンス上の問題となる場合があります。
<$MTTagsAsRegexp cutoff_score="50" cutoff_length="2" case_sensitive="1"$>
<MTTagsAsKeyword regexp_module="Tags Regexp Module"
		keyword_pattern="&lt;a href=&quot;/tag/%e&quot;&gt;%k&lt;/a&gt;">
<$MTEntryBody$>
</MTTagsAsKeyword>

既知の不具合

  • 単語境界を判別しません。そのため "IE" タグは "BOOGIE-WOOGIE(ヴギウギ)" に反応します

ダウンロード

ダウンロード / MD5バージョン日付サイズ(Bytes)動作環境備考
Tagwire plugin w/ TrieEUC
0.11 new '06/02/07 MovableType 文字化け対策
要Jcode 1.99以上
3.151-ja で動作確認

Tagwire plugin; Copyright 2005, Hirotaka Ogawa (hirotaka.ogawa at gmail.com)
This code is released under the Artistic License. The terms of the Artistic License are described at http://www.perl.com/language/misc/Artistic.html

mk_trie_regexp.pl is the original of TrieEUC. Copyright 2005, dankogai

雑記

 とりあえず動くところまでは何とかなったものの、もしかすると効率の悪いことをやっているかも知れず。 何かありましたら遠慮なくツッ込んで頂けると有り難いです。
 今回、自分で作った部分は大した事をしていなくて、 コードの骨格や重要な部分はネット上の様々な先輩方の功績から拝借させて頂いたものです。 有用なリソースを精力的に公開されている方々に改めて感謝をいたしますm(_ _)m
 しかし、登録されているタグが高々 150 個程度では、それほど面白いことになりませんでした。 そういう意味ではハテナの有する20万を超えるキーワードは非常に貴重なコンテンツなんだと思い知らされました。
このエントリーをはてなブックマークに追加  



関連記事/トラックバック (全 1 件中、最新 5 件まで表示しています)

Open MagicVox のスクリーンショット
タイトル
Tagwire Pluginで記事内容からタグ候補を自動的に抽出する
Trackbacked at
2005/11/15 18:11
from
Open MagicVox
概要
 新しく記事を書いていて、以前に使ったタグと同じものを紐付けたいと思った時、 過...

この記事にトラックバックを送るには?

寄せられたコメント (全 16 件中、最新 5 件まで表示しています)

Posted by
footbrainfootbrain
at
2007/04/28 18:47
ID
vRLqL1rs
Tagwire pluginが0.26にアップしたので、Tagwire plugin の改造をやり直しているのですがどうも上手くいきません。
Tagwireのコードが変わってしまっているせいだろうとは思うのですが、当然素人が手を出せるものでは無さそうです。
どこをどうしたらいいのでしょうか?
とりとめのない質問で申し訳ありませんが、(今更という気もしますが)対応をお願いします。
Posted by
footbrainfootbrain
at
2006/11/24 12:28
ID
0ITXtwYA
13:やすひささんと同様に、
keyword_pattern="<a href=&quot;/tag/%e&quot;&gt;%k</a&gt;"
の%kが文字化けを起こして正しく表示されません。
その後、この問題は解決されたのでしょうか?
解決方法があれば、教えてください。お願いします。
Posted by
ぴろりぴろり
at
2006/03/06 10:38
ID
83Szf2uk
内部で文字コードの変換をしている部分があるのですが、
ここでの処理に失敗しているのでしょうか??
# (MTの文字コード)→EUC-JP→(MTの文字コード)という感じ
この部分、ちょっと(かなり?)いい加減なところがあるので少し見直してみますね…
Posted by
やすひさやすひさ
at
2006/03/05 02:16
ID
dX1azhSg
こんにちは。ご無沙汰しております。
試しにPerl5.8.7のサーバ(前と同じくxrea.comですが、サーバ違い)に設置してみると、「〜」の文字化けはなくなりました。
ただ、キーワードパターンの「%k」に限って、2バイト文字のタグだと化ける用になってしまいました。
※「%k」でも1バイト文字のタグと、「%e」では問題ありませんでした。
また、Perl4.6.1のサーバではこの現象が起きていませんでした。
どうぞ、よろしくお願いします。
Posted by
やすひさやすひさ
at
2006/02/26 09:52
ID
k.7DQQQI
パッチを当てることはできました。
あとはコンパイルできれば何とかなりそうなのですが、環境になくコンパイルできません。
次元の低い話で申し訳ありません…。
ちなみに、Perl 4.8.1 にアップしようとサーバを変えてみようとか思いましたが、エントリーがうまく移行できなかったのであきらめました。

コメントを投稿する

 
 (必須, 匿名可, 公開, トリップが使えます)
 (必須, 匿名可, 非公開, Gravatar に対応しています)
 (必須)
スパム コメント防止のため「投稿確認」欄に ランダムな数字 CAPTCHAについて を入力してから送信してください。お手数ですがご協力のほど宜しくお願いいたします。