ちょっと前やけどtwitterで自分のbot(ほぼ分身)を作るというのが一部で流行っていたみたいだったので便乗して作ってみた。
botを作るのは3体目だったけど今までOAuthを使ったbotを作ったことがなかったのでいい機会ということでOAuthを利用。
twitterでのOAuth利用については詳しく書いてくれてるサイトがいっぱいあるのでググってみればいいんじゃないかな!!
流れとしては、
1.twitterのwebページからOAuth利用アプリケーションを登録
2.登録後に発行されるconsumer_keyとconsumer_secretを取得
3.consumer_keyとconsumer_secretからauthorization_urlを取得
4.authorization_urlをブラウザで開きそこに書いてあるPINコードを取得
5.PINコードからaccess_tokenとconsumer_key_secretを取得
6.consumer_key、consumer_secret、access_token、consumer_key_secretを使ってOAuthでなんやかんや。
こんな感じでIDとパスワードを入力することなくtwitterにpostしたりできます。
# 自分用botなのでOAuthでやる必要全くないけどな!!
それでどんなPOSTをさせようか考えたところ、丸々コピーだと面白みが無いので自分の過去のPOST履歴からランダムでマルコフ連鎖文字列を生成してPOSTしようと。
自分の過去のPOSTはRSSから取得するようにした。そこはOAuthではやらない。うん。
そんなこんなでなんとなーく作って稼働中。
まさに俺得bot!!
仕様
・twitterのRSSを指定するとそれからマルコフ連鎖文字列を生成してPOSTします。
・"@","#","http"を含むPOSTはマルコフ連鎖の対象にしない。
・過去100POSTを元データとして使用。
・形態素解析はyahooのAPIを利用。(mecabと仲違いしたためw)
・replyを投げると投げた人のPOST履歴からマルコフ連鎖文字列を生成してreplyし返します。
・起動したら確実にPOSTするのではなく確率でPOSTするようにしてます。
replyには起動ごとに反応します。
ソースコード
githubにも上げてます。
1 #!/usr/bin/perl
2
3 # マルコフ連鎖POST
4 # OAuth ver
5
6 use strict;
7 use Net::Twitter;
8 use LWP::Simple;
9 use XML::Simple;
10 use File::Basename;
11 use Cwd 'abs_path';
12 use Dumpvalue;
13
14 my $d = Dumpvalue->new(); # デバッグ用
15
16 # OAuth settings
17 use constant CONSUMER_KEY => 'CONSUMER_KEY';
18 use constant CONSUMER_KEY_SECRET => 'CONSUMER_KEY_SECRET';
19 use constant ACCESS_TOKEN => 'ACCESS_TOKEN';
20 use constant ACCESS_TOKEN_SECRET => 'ACCESS_TOKEN_SECRET';
21
22 # XML settings
23 use constant TWITTER_XML_URL => 'http://twitter.com/statuses/user_timeline/--USER_ID--.xml?count=100';
24 use constant OWNER_USER_ID => 'OWNER_USER_ID';
25 use constant APPID => 'APPID';
26 use constant API_BASE_URL => 'http://jlp.yahooapis.jp/MAService/V1/parse';
27 use constant API_FIX_PARAM => '&result=ma';
28
29 # other settings
30 use constant SINCE_ID_FILENAME => '.since_id';
31 use constant ROOP_COUNT => 50;
32 use constant RAND_PROB => 10; # 確率の分子
33 use constant RAND_PARAM => 100; # 確率の分母
34
35 # OAuthの準備(global)
36 my $twitter = Net::Twitter->new(
37 traits => ['API::REST', 'OAuth'],
38 consumer_key => CONSUMER_KEY,
39 consumer_secret => CONSUMER_KEY_SECRET,
40 );
41 $twitter->access_token(ACCESS_TOKEN);
42 $twitter->access_token_secret(ACCESS_TOKEN_SECRET);
43
44 # main
45 {
46 # 前回のmentionsの最大のIDをファイルから読み込み
47 my $since_id = &read_since_id(SINCE_ID_FILENAME);
48
49 # 新規mentionsに対してreplyを返す
50 my $recent_since_id = &reaction_to_mentions($since_id);
51
52 # 今回のmentionの最大のIDをファイルに書き込み
53 &write_since_id(SINCE_ID_FILENAME,$recent_since_id);
54
55 # 確率でPOSTする
56 my @rand_val = 1 .. RAND_PARAM;
57 if(rand @rand_val < RAND_PROB){
58 my $owner_xml_url = TWITTER_XML_URL;
59 my $owner_user_id = OWNER_USER_ID;
60 $owner_xml_url =~ s/--USER_ID--/$owner_user_id/;
61
62 # XMLを取得しマルコフ連鎖文字列を生成
63 my $tweet_str = &make_markov_chain_str($owner_xml_url);
64
65 # 返ってきた文字列がundefでなければPOSTする
66 if(defined $tweet_str){
67 my $res = $twitter->update({ status => "$tweet_str" });
68 }
69 }
70 }
71
72 # 新着mentionに対して相手のPOST履歴からマルコフ連鎖文字列を生成しreplyを返す
73 #
74 # ARGV:
75 # $since_id : 前回のmentionsの最大のID
76 #
77 # return:
78 # $recent_since_id : 今回のmentionの最大のID
79 #
80 sub reaction_to_mentions{
81 my $since_id = my $recent_since_id = shift;
82 my %uniq_user;
83
84 # mentions取得
85 my $mentions = $twitter->mentions();
86
87 foreach(@{$mentions}){
88 my $user_xml_url = TWITTER_XML_URL;
89
90 # 今回のmentionの最大のIDが$recent_since_idに入るようにする
91 $recent_since_id = &max($recent_since_id,$_->{'id'});
92
93 # 以下の条件に一致するものにはreplyしない
94 # ・前回のmentionsの最大のIDより前のIDのもの
95 # ・先頭以外に@があるもの
96 # ・プロテクトアカウント
97 # ・今回の起動で既に1回replyしたユーザー
98 if($_->{'id'} <= $since_id or $_->{'text'} =~ m/^(.)(.*)\@/ or $_->{'user'}->{'protected'} == 1 or exists $uniq_user{$_->{'user'}->{'id'}}){
99 next;
100 }
101
102 # $since_idが0だった場合今回のIDを入れておく
103 unless($since_id){
104 $since_id = $_->{'id'};
105 }
106
107 # 今回replyしたユーザーのリスト
108 $uniq_user{$_->{'user'}->{'id'}} = 1;
109
110 # 取得するXMLのURLにuser_idをセット
111 $user_xml_url =~ s/--USER_ID--/$_->{'user'}->{'id'}/;
112
113 # XMLを取得しマルコフ連鎖文字列を生成
114 my $tweet_str = &make_markov_chain_str($user_xml_url);
115
116 # 返ってきた文字列がundefでなければ@、in_reply_toをつけてPOSTする
117 if(defined $tweet_str){
118 $tweet_str = '@'.$_->{'user'}->{'screen_name'}.' '.$tweet_str;
119 my $res = $twitter->update({ status => "$tweet_str", in_reply_to_status_id => $_->{'id'} });
120 }
121 sleep(10);
122 }
123 return $recent_since_id;
124 }
125
126 # 指定XMLからマルコフ連鎖文字列を生成
127 #
128 # ARGV:
129 # $xml_url : 取得対象XMLのURL
130 #
131 # return:
132 # undef or $tweet_str : 生成したマルコフ連鎖文字列
133 #
134 sub make_markov_chain_str{
135 my $xml_url = shift;
136 my $markov_table;
137 my $tweet_list;
138 my @head_word;
139
140 # XML取得
141 my $xml_result = get($xml_url);
142 my $xs = new XML::Simple();
143 my $ref = $xs->XMLin($xml_result);
144
145 # textキーのみハッシュへ
146 foreach my $key (sort keys %{$ref->{'status'}}){
147 $tweet_list->{$key} = $ref->{'status'}->{$key}->{'text'};
148 }
149
150 # 1POSTごとに形態素解析しマルコフテーブルを作成
151 foreach my $key (keys %{$tweet_list}){
152 my @markov_list;
153
154 # # @ http を含むPOSTの場合はスキップ
155 if($tweet_list->{$key} =~ m/http/ or $tweet_list->{$key} =~ m/\@/ or $tweet_list->{$key} =~ m/@/ or $tweet_list->{$key} =~ m/\#/ or $tweet_list->{$key} =~ m/#/){
156 next;
157 }
158
159 # apiを叩いて形態素解析
160 my $api_param = '?appid='.APPID.'&sentence='.$tweet_list->{$key}.API_FIX_PARAM;
161 my $api_url = API_BASE_URL.$api_param;
162 my $api_res = get($api_url);
163 chomp($api_res);
164
165 # 切り出された単語をリストにpushする
166 while($api_res =~ m/<surface>(.+?)<\/surface>/g){
167 unless(scalar(@markov_list)){
168 push (@head_word,$1);
169 }
170 push (@markov_list,$1);
171 }
172
173 # マルコフテーブルを作成
174 foreach my $num (0 .. $#markov_list){
175 push (@{ $markov_table->{$markov_list[$num]} },$markov_list[$num+1]);
176 }
177 }
178
179 # 要素数が0の場合何もしない
180 unless(scalar(keys(%{$markov_table}))){
181 return;
182 }
183
184 # 最初の単語を決定
185 my $tweet_str = my $chain_word = $head_word[rand @head_word];
186
187 # 無限ループしないよう最大回数を決めてマルコフ連鎖
188 foreach (0 .. ROOP_COUNT){
189 my $add_word = $markov_table->{$chain_word}->[rand @{$markov_table->{$chain_word}}];
190 $tweet_str .= $chain_word = $add_word;
191 }
192 return $tweet_str;
193 }
194
195 # IDをファイルに書き込み
196 #
197 # ARGV:
198 # $filename : 書き込み対象ファイル名
199 # $id : 書き込みID
200 #
201 # return:
202 # undef
203 #
204 sub write_since_id{
205 my $filename = shift;
206 my $id = shift;
207 my $since_id_file = dirname(abs_path($0)) . '/' . $filename;
208
209 open (OUT, ">$since_id_file");
210 print OUT $id;
211 close(OUT);
212
213 return;
214 }
215
216 # IDをファイルから読み込み
217 #
218 # ARGV:
219 # $filename : 読み込み対象ファイル名
220 #
221 # return:
222 # $id : ファイルから読み込んだID
223 #
224 sub read_since_id{
225 my $filename = shift;
226 my $since_id_file = dirname(abs_path($0)) . '/' . $filename;
227 my $id = 0;
228
229 if(open IN,$since_id_file){
230 $id = <IN>;
231 chomp($id);
232 }
233 close(IN);
234
235 return $id;
236 }
237
238 # 最大値判定処理
239 #
240 # ARGV:
241 # $val1 : 比較する値
242 # $val2 : 比較する値
243 #
244 # return:
245 # $val1 or $val2 : $val1と$val2を比較し大きい方の値を返す
246 #
247 sub max{
248 my $val1 = shift;
249 my $val2 = shift;
250
251 if($val1 >= $val2){
252 return $val1;
253 }
254 else{
255 return $val2;
256 }
257 }