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)
    複数配列または多次元配列を同時にソート