- 天気予報
- マルコフ連鎖で作文
2021/01/01
2012/04/06
原稿ヒイロbot EasyBotter.phpスケルトン
素のEasyBotter.phpはちょっと関数の並び方の基準がよく分からなかったので、自分に分かりやすいように並び替えています。ついでに、アクセス権付加の真似事のようなこともしています。元スクリプトはメソッドは全部public扱いだったような?
<?php
class EasyBotter {
/*=========================================== 変数宣言一式 */
/*=========================== コンストラクタ・デストラクタ */
//コンストラクタ
public function __construct() {
}
//デストラクタ
public function __destruct() {
}
/*============================== bot.phpから利用する関数群 */
//自動フォロー返し
public function autoFollow(){
}
//ランダムにポスト
//bot.php側で何も指定しなければdata.txtを見に行く
public function postRandom($datafile = DIALOG_DATA){
}
//順番にポスト
//bot.php側で何も指定しなければdata.txtを見に行く
public function postRotation($datafile = DIALOG_DATA, $lastPhrase = FALSE){
}
//リプライする
//bot.php側で何も指定しなければDIALOG_REPLY→DIALOG_DATAの順番で見に行く
public function reply($cron = 2, $replyFile = DIALOG_DATA, $replyPatternFile = DIALOG_REPLY){
}
/*===================================== プライベート関数群 */
/*------------------------------------- ツイート生成関数群 */
//通常の発言を作る
//$file:対象ファイル指定
//$number:対象ファイル内$number目の行を取得
private function makeTweet($file, $number = FALSE){
}
//キーフレーズ置換で発言を作る
private function makeSwapKeyPhraseTweet() {
}
//キーフレーズを元にした発言を作る
//$text:キーフレーズ取得対象文字列
private function makeKeyphraseTweet($text) {
}
//ガンダムリストに追加
private function addMemberToList($user_id) {
}
//ガンダムリストから削除
private function removeMemberFromList($user_id) {
}
//リプライを作る
//$replies:リプライ作成元文字列(配列)
//$replyFile:対象ランダムファイル
//$replyPatternFile:対象パターンファイル
private function makeReplyTweets($replies, $replyFile, $replyPatternFile){
}
//タイムラインへの反応を作る
//$timeline:
//$replyPatternFile:対象パターンファイル
private function makeReplyTimelineTweets($timeline, $replyPatternFile){
}
/*------------------------------------- ツイート操作関数群 */
//タイムラインの最近の呟きからランダムに一つを取得
private function getRandomTweet(){
}
//つぶやきの中から$minute分以内のものと、最後にリプライしたもの以降のものだけを返す
private function getRecentTweets($response,$minute){
}
//必要なつぶやきのみに絞る
private function selectTweets($response) {
}
//リプライ一覧から自分が既に返事したものを除く
private function removeRepliedTweets($response){
}
/*----------------------------------- キーワード変換関数群 */
//文章を変換する
private function convertText($text, $reply = FALSE){
}
//日付・時刻を変換
private function convertDateAndTime($text) {
}
//カウントダウンを変換
private function convertCountDown($text) {
}
/*----------------------------------------- HTML出力関数群 */
//HTML冒頭部分出力
private function printHeader() {
}
//HTML末尾部分出力
private function printFooter() {
}
//結果を表示する
private function showResult($response) {
}
/*----------------------------------- 内部データ保存関数群 */
//リプライツイートIDをログファイルに記録(どこまでリプライしたかを覚えておく)
private function saveLog() {
}
//ログの順番を並び替える(というかたぶん使用した行を最下部に移動するだけ)
private function rotateData($file) {
}
/*-------------------------------------- ファイルI/O関数群 */
//つぶやきデータを読み込む
private function readDataFile($file) {
}
//リプライパターンデータを読み込む
private function readPatternFile($file) {
}
/*------------------------------- TwitterAPIアクセス関数群 */
//基本的なAPIを叩く
private function _setData($url, $value = array()) {
}
private function _getData($url) {
}
private function setUpdate($value) {
}
private function getFriendsTimeline() {
}
private function getReplies($page = false) {
}
private function getFriends($id = null) {
}
private function getFollowers() {
}
private function followUser($screen_name) {
}
private function createListMember($list_id, $user_id) {
}
private function destroyListMember($list_id, $user_id) {
}
}
/*=========================================== その他関数群 */
function is_noun($word) {
}
?>
2012/04/05
名詞入れ替えで作文
EasyBotter.phpを以下のように改造して様子見中。あまり正規表現に詳しくないので悲惨なことになっております。
template.csvはあらかじめ作成していたものを、word.csvは、キーフレーズ抽出時にこっそり保存していたものを流用しています。
class EasyBotter {
// キーフレーズ置換で発言を作る
private function makeSwapKeyPhraseTweet() {
$keyphrase = array();
$poem = array();
$table = "。";
$status = "";
// キーフレーズCSVを読み込み
$fp = fopen('word.csv', 'r') or die('ファイルが開けません');
while(($columns = fgetcsv($fp, 4096, ",", '"')) !== FALSE) {
array_push($keyphrase, $columns[0]);
}
fclose($fp);
shuffle($keyphrase);
// 原文CSVを読み込み
$fp = fopen('template.csv', 'r') or die('原文ファイルが開けません');
while(($columns = fgetcsv($fp, 4096, ",", '"')) !== FALSE) {
array_push($poem, array($columns[0], $columns[1]));
}
fclose($fp);
// 名詞を適当に入れ替える
$keyphrase_num = count($keyphrase);
$rd_keys = array_rand(array_filter($poem, 'is_noun'), $keyphrase_num);
for($i=0; $i<$keyphrase_num; $i++) {
$poem[$rd_keys[$i]][0] = $keyphrase[$i];
}
// テーブル作成
for($i=0; $i<count($poem); $i++) {
$table .= $poem[$i][0];
}
// 一致している間は選びなおす
$status_1 = "";
$status_2 = "";
while($status_1 === $status_2) {
// キーフレーズを選出×2
$rd_keys = array_rand($keyphrase, 2);
$matches_1 = array();
$matches_2 = array();
$pattern_1 = '/^(.*)[。?](.*?)'.$keyphrase[$rd_keys[0]].'(.*?[。?])/u';
$pattern_2 = '/^(.*)[。?](.*?)'.$keyphrase[$rd_keys[1]].'(.*?[。?])/u';
preg_match_all($pattern_1, $table, $matches_1);
preg_match_all($pattern_2, $table, $matches_2);
$status_1 = $matches_1[2][0].$keyphrase[$rd_keys[0]].$matches_1[3][0];
$status_2 = $matches_2[2][0].$keyphrase[$rd_keys[1]].$matches_2[3][0];
}
$status = "「".$status_1.$status_2."」";
return $status;
}
// 途中省略
private function makeReplyTweets($replies, $replyFile, $replyPatternFile){
if(empty($this->_replyPatternData[$replyPatternFile]) && !empty($replyPatternFile)){
$this->_replyPatternData[$replyPatternFile] = $this->readPatternFile($replyPatternFile);
}
$replyTweets = array();
foreach($replies as $reply){
$status = "";
// 追加ここから
if(strpos((string)$reply->text, "作文して") !== FALSE) {
// キーフレーズで作文
$status = $this->makeSwapKeyPhraseTweet();
}
// 追加ここまで
// 以下省略
}
}
}
各データファイルの構造
word.csv
1行1カラムの単純なデータです。相手のキーフレーズ+定型文で作文 でこっそり保存していたものです。
"スカイツリー" "お台場" "実績取り" "競合他社" "以下略" "デスマッチ" "中心" "沿岸部"
template.csv
元となる作文をあらかじめ形態素解析しておいたものです。この中から名詞をランダムに取り出して、word.csvの中身と入れ替えています。
"太陽系","名詞" "に","助詞" "生物","名詞" "が","助詞" "生息","名詞" "する","助動詞" "こと","名詞" "を","助詞" "奇跡的","形容動詞" "に","助詞" "可能","名詞" "に","助詞" "し","動詞" "た","助動詞" "惑星","名詞" "、","特殊" "それ","名詞" "が","助詞" "地球","名詞" "で","助動詞" "ある","助動詞" "。","特殊"
特定の日付までのカウントダウン
EasyBotter.phpを以下のように改造します。
例によってあまり賢い方法ではありません……。
class EasyBotter {
// 途中省略
// 文章を変換する
private function convertText($text, $reply = FALSE){
// 途中省略
// 追加ここから
// カウントダウン
if(strpos($text, "{countdown}") !== FALSE){
$text = $this->convertCountDown($text);
}
// 追加ここまで
// フッターを追加
$text .= $this->_footer;
return $text;
}
// カウントダウンを変換
private function convertCountDown($text) {
$return_text = "";
// デリミタで分割
$event_array = explode( ",", $text);
$target_array = explode("-", $event_array[0]);
// 目標の日付
$target_date = mktime(0, 0, 0, $target_array[1], $target_array[2], $target_array[0]);
// 今日の日付
$current_date = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
// 差を計算
$interval = ($target_date - $current_date) / 86400;
// 日付を日本語表記に
$event_array[0] = date("Y年n月j日", $target_date);
if($interval > 0) {
// 日程より前
$return_text = $event_array[0].$event_array[1]."の".$event_array[2]."まで、あと".$interval.$event_array[4]."……。";
} elseif($interval == 0) {
// 当日
$return_text = "今日は".$event_array[1]."の".$event_array[2]."当日だ。任務了解。明朝より作戦行動に入る。";
} elseif($interval == -1) {
// 昨日
$return_text = $event_array[0].$event_array[1]."は、昨日".$event_array[2]."を完了している……任務、完了。";
} else {
// 2日前以前
$return_text = $event_array[0].$event_array[1]."は、".abs($interval).$event_array[4]."前に".$event_array[2]."を完了している……。";
}
return $return_text;
}
// 以降省略
}
パターンファイルやランダムファイルの方では、例えば以下のように記述しています。
2012-4-26,ガンダムエース6月号,発売,{countdown},日
2012-5-3,SUPER COMIC CITY 21 1日目,開催,{countdown},日
電力予報
全面的にTwitterBotPHP掲示板の天気予報機能を参考にしています……いたと思います。
EasyBotter.phpの改造ですが、改造箇所が複数に亘るのでご注意ください。
また、Yahoo!デベロッパーズネットワークの電気予報APIを使用しますので、前もってアプリケーションIDを取得しておいてください。
class EasyBotter {
// 途中省略
// まるっと関数を新造
function makePowerForecastTweet($text){
$api_key_yahoo = '取得したアプリケーションID';
// いつの?
$today = date("Ymd", time());
$tomorrow = date("Ymd", time()+(24*3600));
$dayaftertomorrow = date("Ymd", time()+24*3600*2);
$w_day = array(
"今日"=> $today, "きょう"=> $today,
"明日"=> $tomorrow, "あす"=> $tomorrow, "あした"=> $tomorrow,
"明後日"=> $dayaftertomorrow, "あさって"=> $dayaftertomorrow
);
foreach( $w_day as $w_time => $w_timeNo){
if(preg_match( "/".$w_time."/u", $text)) {
$timeName = $w_time;
break;
}
}
if(empty($timeName)) {
$timeName = $today;
}
$day = $w_day["$timeName"];
// どこの?
$w_city = array(
"東北"=>"tohoku",
"東京"=>"tokyo", "関東"=>"tokyo", "東電"=>"tokyo",
"中部"=>"chubu", "名古屋"=>"chubu",
"大阪"=>"kansai", "関西"=>"kansai", "関電"=>"kansai",
"九州"=>"kyushu", "福岡"=>"kyushu"
);
foreach( $w_city as $w_area => $w_areaNo){
if(preg_match( "/".$w_area."/u", $text)) {
$cityName = $w_area;
break;
}
}
if( empty($cityName) ){
$w_flg = FALSE;
$cityName = "東京";
}
$where = $w_city["$cityName"];
// Yahoo!電力予報APIからPHP切片を取得
$powerforecast_url
= "http://setsuden.yahooapis.jp/v1/Setsuden/electricPowerForecast?appid="
.$api_key_yahoo."&area=tokyo&date=".$day;
$forecastxml = simplexml_load_file($powerforecast_url);
// 配列に押し込む
$i = 0;
$powers = array();
foreach($forecastxml->Forecast as $forecast){
if(!is_array($forecast)) {
$hour[$i] = intval($forecast->Hour);
$percent[$i] = round((intval($forecast->Usage)/intval($forecast->Capacity))*100);
$i++;
}
}
// 最大値検出
array_multisort($percent, SORT_DESC, $hour, SORT_ASC);
// メッセージ整形
$maxs = array();
$times = "";
for($i = 0; $i < count($hour); $i++) {
if($percent[$i] === $percent[0]) {
array_push($maxs, $hour[$i]."時");
} else {
break;
}
}
if($day === $today) {
$timeName .= "のこれから";
}
if($i > 1) {
$times = "のだいたい".$i."回";
}
$status = $timeName."の電力最大使用率は".$percent[0]."%で、ピークは"
.join($maxs, "、").$times."だってさ。";
return $status;
}
// 途中省略
//リプライを作る
function makeReplyTweets($replies, $replyFile, $replyPatternFile){
if(empty($this->_replyPatternData[$replyPatternFile]) && !empty($replyPatternFile)){
$this->_replyPatternData[$replyPatternFile] = $this->readPatternFile($replyPatternFile);
}
$replyTweets = array();
foreach($replies as $reply){
$status = "";
// ここから追加
if(strpos((string)$reply->text, "電力") !== FALSE) {
$status = $this->makePowerForecastTweet((string)$reply->text);
}
// ここまで追加
// リプライパターンと照合
if(empty($status) && !empty($this->_replyPatternData[$replyPatternFile])){
// 以降省略
}
特定のキーワードでリスト追加/削除
お前はガンダムだ。
SRWZ2再世編発売日ですし、EasyBotter.phpを改造して、botが勝手にリスト追加・削除をするようにします。
リストIDはAPIからXMLを確認するなどでご確認ください。
class EasyBotter {
// 最初のプライベート変数宣言群に追加
private $_list_id = リストID;
// 途中省略
//リストに追加
private function addMemberToList($user_id) {
//リストID
$list_id = $this->_list_id;
//API実行
$response = $this->createListMember($list_id, $user_id);
if(empty($response->error)) {
// 追加成功のリプライ
return "分かった……お前はガンダムだ。";
} else {
// 追加失敗のリプライ
return "お前はガンダムにはなれない。";
}
}
//リストから削除
private function removeMemberFromList($user_id) {
//リストID
$list_id = $this->_list_id;
//API実行
$response = $this->destroyListMember($list_id, $user_id);
if(empty($response->error)) {
// 削除成功のリプライ
return "お前はガンダムではない……そういうことか。";
} else {
// 削除失敗のリプライ
return "そうか……。";
}
}
// 途中省略
//リプライを作る
private function makeReplyTweets($replies, $replyFile, $replyPatternFile){
if(empty($this->_replyPatternData[$replyPatternFile]) && !empty($replyPatternFile)){
$this->_replyPatternData[$replyPatternFile] = $this->readPatternFile($replyPatternFile);
}
$replyTweets = array();
foreach($replies as $reply){
$status = "";
// ここから変更
if(strpos((string)$reply->text, "俺がガンダムだ") !== FALSE) {
//リスト追加
$status = $this->addMemberToList((string)$reply->user->id);
} else if(strpos((string)$reply->text, "俺はガンダムにはなれない") !== FALSE) {
//リスト削除
$status = $this->removeMemberFromList((string)$reply->user->id);
} else if(!empty($this->_replyPatternData[$replyPatternFile])){
// ここまで変更
// リプライパターンと照合
foreach($this->_replyPatternData[$replyPatternFile] as $pattern => $res){
if(preg_match("@".$pattern."@u",$reply->text, $matches) === 1){
$status = $res[array_rand($res)];
for($i=1;$i <count($matches);$i++){
$p = "$".$i;
$status = str_replace($p,$matches[$i],$status);
}
break;
}
}
}
// 以下省略
}
// 途中省略
// 基本的なAPIを叩く
// 以下のメソッドを最下部に追加
private function createListMember($list_id, $user_id) {
$url = "https://api.twitter.com/1/lists/members/create.xml";
return $this->_setData($url, array("list_id" => $list_id, "user_id" => $user_id));
}
private function destroyListMember($list_id, $user_id) {
$url = "https://api.twitter.com/1/lists/members/destroy.xml";
return $this->_setData($url, array("list_id" => $list_id, "user_id" => $user_id));
}
}
相手のキーフレーズ+定型文で作文
reply_pattern.phpにキーワードが見つからない場合に、相手の発言の中から重要らしい単語を抜き出して、定型文と組み合わせてリプライを作成します。
EasyBotter.phpの改造ですが、改造箇所が複数に亘るのでご注意ください。
また、Yahoo!デベロッパーズネットワークのWEB APIキーフレーズ抽出を使用しますので、前もってアプリケーションIDを取得しておいてください。
class EasyBotter {
// 最初のプライベート変数宣言群に追加
private $_api_key_yahoo = '取得したアプリケーションID';
private $_api_url_yahoo_keyphrase = 'http://jlp.yahooapis.jp/KeyphraseService/V1/extract?appid=';
// 途中省略
// キーフレーズを元にした発言を作るメソッドを丸ごと追加
// $text:キーフレーズ取得対象文字列(つまり相手のツイート)
private function makeKeyphraseTweet($text) {
$api_key_yahoo = $this->_api_key_yahoo;
$api_url_yahoo_keyphrase = $this->_api_url_yahoo_keyphrase;
$keyphrase = "";
// 文字列から@とハッシュタグとURLを取り去る
$text = preg_replace('/(#)(\w+)/', '', $text);
$text = preg_replace('/(@)(\w+)/', '', $text);
$text = preg_replace('/(https?|ftp)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)/', '', $text);
// 文字列をUTF-8でエンコードし、さらにURLエンコード
$sentence = urlencode(mb_convert_encoding($text, 'utf-8', 'auto'));
//Yahoo!キーフレーズ抽出からXMLを取得
$keyphrase_url = $api_url_yahoo_keyphrase.$api_key_yahoo."&sentence=".$sentence."&output=xml";
$keyphrase_xml = simplexml_load_file($keyphrase_url);
// キーフレーズの個数
$keyphrase_num = count($keyphrase_xml->Result);
for($i=0; $i<$keyphrase_num; $i++){
$result = $keyphrase_xml->Result[$i];
// スコア100のフレーズだけ持って出る
if(htmlspecialchars($result->Score, ENT_QUOTES) == 100) {
$keyphrase = htmlspecialchars($result->Keyphrase, ENT_QUOTES);
break;
}
}
// 返却バリエーションをつける
$fix_sentence = array(
"{keyphrase}について興味が湧いた。少し話を聞かせてくれないか。",
"{keyphrase}……? それはいったい何のことだ?",
"{keyphrase}がどうかしたのか?",
"俺は{keyphrase}に興味なんてない。",
"{keyphrase}については、俺も耳にしたことがある。",
"{keyphrase}に関するデータ……オールクリア……。",
);
if($keyphrase !== "") {
// 別途使うのでword.csvに保存しておく
$fp = fopen('word.csv', 'a') or die('ファイルが開けません');
fwrite($fp, "\"".$keyphrase."\"\n");
fclose($fp);
// 定型文をランダム選択
$key = array_rand($fix_sentence);
return str_replace("{keyphrase}", $keyphrase, $fix_sentence[$key]);
} else {
// キーフレーズがひとつも見つからなかった場合は[[END]]を返す
return "[[END]]";
}
return "[[END]]";
}
// 途中省略
//リプライを作る
private function makeReplyTweets($replies, $replyFile, $replyPatternFile){
// 途中省略
if(empty($status) && !empty($replyFile)){
//パターンになかった場合はランダムに
// ここから変更
if(rand(0,2) == 0){
$status = $this->makeTweet($replyFile);
} else {
$status = $this->makeKeyphraseTweet((string)$reply->text);
// 救済措置
if($status == "[[END]]") {
$status = $this->makeTweet($replyFile);
}
}
// ここまで変更
}
}
// 以降省略
}
よく考えると勝手にfunctionの前にprivateとか足してますね……。
使用関数
- simplexml_load_file(url)
指定したURLにアクセスしてXMLデータを取得する。
相手の場所を含む発言
EasyBotter.phpの下記部分を編集します。
//文章を変換する
private function convertText($text, $reply = FALSE){
// 一部省略
if(strpos($text,"{tweet}") !== FALSE && !empty($reply)){
$tweet = preg_replace("@\.?\@[a-zA-Z0-9-_]+\s@u","",$reply->text);
$text = str_replace("{tweet}",$tweet,$text);
}
//この下から追加
if(strpos($text,"{place}") !== FALSE) {
if(!empty($reply)){
if(!empty($reply->user->location)) {
$text = str_replace("{place}",$reply->user->location,$text);
} else {
$text = str_replace("{place}","目標施設",$text);
}
}else{
$randomTweet = $this->getRandomTweet();
if(!empty($randomTweet->user->location)) {
$text = str_replace("{place}",$randomTweet->user->location,$text);
} else {
$text = str_replace("{place}","目標施設",$text);
}
}
}
//ここまで追加
//フッターを追加
$text .= $this->_footer;
return $text;
}
文中の「目標施設」というのは、相手の場所が取得できなかった場合のデフォルトですので、適宜botのイメージに合うように置き換えるといいと思います。
使用データ
- user->location
相手の「場所」が格納されている。
定刻からずれて動作するようにcronを設定
cron自体が何かについては、Wikipediaなどでお調べください。
毎時0分、3分、6分、9分、12分、15分……57分に動作させる場合は、以下のように設定します。
0-59/3 * * * *
毎時1分、4分、7分、10分、13分、16分……58分に動作させる場合には、以下のように設定します。
1-58/3 * * * *
当botは同一サーバでホストしているので、サーバに一度に負荷がかからないように、上記のように1分ずらして実行させています。かけあい重視のbot同士の設定にも使えるかもしれません。
リプライパターンのシャッフル
EasyBotter.phpの下記部分を編集します。
が、もしかしたら根本的に間違っているかもしれません。
//リプライパターンデータを読み込む
function readPatternFile($file){
$data = array();
// 追加ここから
$num = array();
// 追加ここまで
// パターンファイルをインクルード
require_once($file);
if(count($data) != 0){
// インクルードしたパターンファイルの中に$dataという配列があるので
// その要素数が0でなければ
// 追加ここから
$num = range(0, count($data)-1);
shuffle($num);
array_multisort($num, $data);
// 追加ここまで
return $data;
}else{
// $dataの要素数が0だった場合はこちら
// これ何を返すのかよく分からない……。
return $reply_pattern;
}
}
使用関数
- range(start, end, [step])
最初の値、最後の値、増分を指定して配列を作成 - shuffle(array)
配列をランダムに並べ替え - array_multisort(array1, array2)
複数配列または多次元配列を同時にソート