はてなハイクAPIを使用して、指定キーワードのタイムラインを取得してからゴニョゴニョしてみる

kujoo2009-05-15



んーと、先週「あとでやろう」と思ってたんだけど、全くやってなかったハイクAPI関連の続き。


指定したキーワードのタイムラインにいた人のタイムラインを拾ってそいつをヤフーの形態素解析APIに渡して何らかの特徴を抽出する、っていう処理。


↓にだらだらと書いてみましたが、コードが汚すぎてヤル気ダウン〜。動けばいいんだろって最低ですね。もうケイゾク無理っす。汚すぎる。自分で書いてて嫌になってきた・・・。キレイなコードは生産性を上げるって本当だよね。どうもPerlってどういう風に書くとキレイになるのか掴めてない。って言っても今回のコレはソレ以前の話。ひどす・・・。


誰か綺麗にしてください・・・orz

#!perl
use strict;
use warnings;
use Encode;
use URI::Escape;
use XML::TreePP;
use Data::Dumper;
use Time::Local; # これは書かなくても良い?


 # グローバルな値の設定

 my $self_id = 'kujoo'; # 何でもいいや
 my $datetime_filename = 'datetime';
 my $api_uri_base = 'http://h.hatena.ne.jp/api/';
 my $api_method_keyw = 'statuses/keyword_timeline/';
 my $api_method_user = 'statuses/user_timeline/';
 my $api_format = '.xml';
 my $yahoo_api_parse = 'http://jlp.yahooapis.jp/MAService/V1/parse';
 my $yahoo_api_appid = '○○○'; # yahooのAppID


 # タイムライン取得
 my $tlex = &get_timeline_from_keyword('ひとりごと'); # 


 # なんか処理
 print Dumper $tlex;


 exit;


####


 # 指定したキーワードのタイムラインを取得
sub get_timeline_from_keyword {
 my $keyword = shift;

 # 前回取得した時間を読込む
 my $param_since;
 open(my $datetimefile, "<$datetime_filename-$self_id") or $param_since = '2000-01-01T00:00:00Z';
 if(! $param_since) {
  while(<$datetimefile>) {chomp; $param_since = $_;}
  close($datetimefile);
 }

 # タイムラインを取得
 $keyword = uri_escape($keyword);
 my $api_uri = $api_uri_base.$api_method_keyw.$keyword.$api_format;
 $api_uri .= "?count=200"; # 取得する最大件数(最大200)
 $api_uri .= "&since=$param_since" if($param_since);

 # ハイクAPIにGET
 my $timeline = XML::TreePP->new(force_array => [qw(status)])->parsehttp(GET => $api_uri)->{statuses}->{status};
 # ちなみにハイク側のキャパを超える?とエラーになる

 my $statuslist;
 my $date_time;

 foreach my $p (@$timeline) {
  $date_time = $p->{created_at} if(! $date_time);

  # IDからそのユーザーの発言内容をチェック
  if($p->{user}->{id}) {
   my $mw = &get_majorword_from_userid($p->{user}->{id});
   $p->{word} = $mw->{word};
   $p->{status_last} = $mw->{status_last};
   $p->{status_average} = $mw->{status_average};
   $p->{stars} = $mw->{stars};
  }

  # 本文からキーワードを除く
  my $regex = "^($p->{keyword}=)";
  $p->{text} =~ s|$regex||g;

  # 本文内のurl的な文字列をwithin_linkに入れる
  while($p->{text} =~ s!((http|https):/[/=%?\&\.\w]+)!!g) {
   push(@{$p->{within_link}}, $1);
  }

  # 本文内のキーワード的な文字列をwithin_keywordに入れる
  while($p->{text} =~ m|\[\[([^\s\[]+)\]\]|g) {
   push(@{$p->{within_keyword}}, $1);
  }

  # 面倒くさそうな文字を削除
  $p->{text} =~ s|[<>"'\[\]]||g; # '"
 }

 # タイムライン取得した日時を保存
 if($date_time) {
  open(my $datetimefile, ">$datetime_filename-$self_id") or undef($date_time);
  if($date_time) {
   print $datetimefile $date_time;
   close($datetimefile);
  }
 }

 return($timeline);
}


 # 指定したユーザーの頻出ワードを取得
sub get_majorword_from_userid {
 my $userid = shift;
 my($text, $dt_last, $dt_average, $stars) = &get_timeline_from_userid($userid);
 return( {
  'id' => $userid,
  'word' => &get_parse_pos_from_text($text),
  'status_last' => $dt_last,
  'status_average' => $dt_average,
  'stars' => $stars,
 } );
}


 # 指定IDのタイムラインを取得する
sub get_timeline_from_userid {
 my $userid = shift;
 my $max_len = 90000; # 100KB制限用
 my $text = '';
 my $date_time_average = 0;
 my $date_time_last = 0;
 my $date_time_old;
 my $item = 0;
 my $stars = 0;

 # ハイクAPIにGET
 my $status = XML::TreePP->new(force_array => [qw(status)])->parsehttp(GET => $api_uri_base.$api_method_user.$userid.$api_format.'?count=20')->{statuses}->{status};
 # ちなみにハイク側のキャパを超える?とエラーになる

 foreach my $p (@$status) {
  $date_time_last = $p->{created_at} if(! $date_time_last);
  $date_time_old = $p->{created_at};
  $item++;
  $stars += $p->{favorited};

  # 諸々、不要っぽい文字を削除
  my $regex = "^($p->{keyword}=)";
  $p->{text} =~ s|$regex||g;
  $p->{text} =~ s!((http|https):/[/=%?\&\.\w]+)!!g;
  $p->{text} =~ s|[<>"'\[\]]||g; # '"

  # APIに渡す容量を気にしておく
  if(length($text . $p->{text}) < $max_len) {
   $text .= ($p->{text} . ' ');
  }
 }

 # 平均更新間隔(秒数)
 if($item > 1) {
  $date_time_old =~ m|(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z|i;
  my $dt1 = Time::Local::timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
  $date_time_last =~ m|(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z|i;
  my $dt2 = Time::Local::timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
  $date_time_average = int(($dt2 - $dt1) / ($item - 1));
  if($date_time_average < 0) {$date_time_average = 0;}
 }
 # 1更新当たりのスター数(端数切り捨て?)
 if($item > 0) {$stars = int($stars / $item);}

 return( $text, $date_time_last, $date_time_average, $stars );
}


 # Yahoo!デベロッパーネットワーク - テキスト解析 - 日本語形態素解析
 # http://developer.yahoo.co.jp/webapi/jlp/ma/v1/parse.html
 # を利用した形態素解析でよく使う単語を抜きだす
sub get_parse_pos_from_text {
 my $text = shift;
 my $pos = [
  {'surface' => undef, 'count' => 0, 'pos' => '名詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '動詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '形容詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '形容動詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '助詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '助動詞', },
  {'surface' => undef, 'count' => 0, 'pos' => '副詞', },
 ];

 # 日本語形態素解析APIにPOST(GETでも良い?)
 $text = URI::Escape::uri_escape($text);
 my $uniq = XML::TreePP->new()->parsehttp(POST => $yahoo_api_parse,
  'appid='.$yahoo_api_appid.'&results=uniq&sentence='.$text
 )->{ResultSet}->{uniq_result}->{word_list}->{word};

 # 使用頻度の高い3文字以上の言葉を抽出(同じ出現関数の場合は初出を優先)
 foreach my $u (@$uniq) {
  foreach my $p (@$pos) {
   if($p->{'pos'} eq $u->{'pos'}) {
    if($p->{'count'} < $u->{'count'}) {
     if(length(Encode::decode('utf8', $u->{'surface'})) >= 3) {
      $p->{'surface'} = $u->{'surface'};
      $p->{'count'} = $u->{'count'};
     }
    }
   }
  }
 }

 return( $pos );
}


とりあえず何らかの特徴を抽出は、品詞毎に出現数の多いものを取得する、ということにしてみました。それを元のタイムラインに追加します。

同じ人が繰り返し発言してると、無駄にその人のタイムラインを繰り返し取得しちゃいます。これもひどいな・・・。


はてなAPIの使用制限て今のところ無いのかな。これ↑、最初にタイムライン読んだ後、最大200回連続してAPIにアクセスするけど・・・絶対良くない気がする・・・。つか実際count=200でずっとアクセスしてるとそのうちエラーになっちゃうんだけど(なのでユーザーのタイムラインはcount=20にしてます。まー200とか読んでたら実際時間かかりすぎるけど)。タイミングの問題かも? ヤフーの方は制限されてるけどこれくらいの使い方だったら大丈夫ポイ気が。


あとねーぶっちゃけ、これ使って何しようか、も決まってないんですよねー。これがモチベーション的につらい。
まー、全く無いわけでもないんですが・・・。



ゆっくりやっていきましょうw=