twitter apiで会話を取得する

# はじめに
会社の雨宮さんが機械学習をしていて、会話データが欲しいということで、twitter apiを利用して、つぶやきと返信を取得しました。言語は、PHPを使います。

# アプリケーションの登録
twitter apiを利用するには、開発者登録を行い、APIキーを発行する必要があります。
インターネットに情報はたくさん公開されていますが、このサイトは日本語で、よく情報がまとまっていて、参考になります。まずは、アプリケーションの登録をします。
[Twitter REST APIの使い方](https://syncer.jp/Web/API/Twitter/REST_API/)

# ライブラリのダウンロード
アプリケーションの登録で発行したconsumer_keyとcosumer_secretだけではapiは使えません。それらから、署名を生成して、はじめて呼び出しができるのですが、これらの処理をまとめてやってくれる便利なライブラリがありますので、今回はそれを使います。composerで一発です。
[TwitterOAuth](https://twitteroauth.com/)

```php:get_tweet.php
require "vendor/autoload.php";
use Abraham\TwitterOAuth\TwitterOAuth;

$consumer_key = 'xxxxxxxxxx';
$consumer_secret = 'xxxxxxxxxx';

$connection = new TwitterOAuth($consumer_key, $consumer_secret);
```

`$connection`を使って、apiを呼んでいきます。


# 返信の取得方法
API仕様を調べたところ、ツイートから、そのツイートに対する返信の取得はできないことが分かりました。逆に、どのツイートに対する返信であるかという情報は得られるので、返信から返信元ツイートを取得するというやり方で、会話を取得することにしました。
[API reference index](https://developer.twitter.com/en/docs/api-reference-index)

# 取得例
このツイートと返信を例として、取得方法を書いていきます。
![twitter.png](https://qiita-image-store.s3.amazonaws.com/0/45425/d9fa2374-9b21-f902-91f9-20e15e577699.png)

# ツイート検索（返信の取得）
`search/tweets`を利用します。適当な検索語と、取得件数を設定します。ツイートは、たくさん取得できたほうがよいので、取得件数は、最大の100とします。取得結果の`in_reply_to_status_id`に値が設定されている場合、それが、その返信元ツイートのidになりますので、そのツイートを取得します。NULLの場合は、返信ではないということになるので、無視。ツイートの内容は`text`に設定されます。

```php:get_tweet.php
$tweet_list = $connection->get('search/tweets', array('q' => '肩が上に上がらない', 'count' => 100));
```

```txt:取得されたツイート
  ["created_at"]=>
  string(30) "Tue Mar 27 05:03:20 +0000 2018"
  ["id"]=>
  int(978497675413749760)
  ["id_str"]=>
  string(18) "978497675413749760"
  ["text"]=>
  string(109) "@misogram 肩が上に上がらないのはプラモ狂四郎が看破してたくらいですからねw(違)"
～中略～
  ["in_reply_to_status_id"]=>
  int(978463044433948675)
～以下略～
```

# ツイート取得（返信元の取得）
idからツイートを取得する場合、`statuses/show`を利用します。ツイートの内容は同じく`text`に設定されます。

```php:get_tweet.php
$tweet = $connection->get('statuses/show', array('id' => 978497675413749760));
```

```txt:取得されたツイート
  ["created_at"]=>
  string(30) "Tue Mar 27 02:45:43 +0000 2018"
  ["id"]=>
  int(978463044433948675)
  ["id_str"]=>
  string(18) "978463044433948675"
  ["text"]=>
  string(102) "「ゲルググの弱点は肩」講談社ポケット百科に書いてあったから間違いない"
～以下略～
```

# 変換
ツイートの中に、ユーザ名等、会話としては、不要なもの等が含まれるので、以下のように変換しました。

・ユーザ名
@ユーザで表記されるユーザ名は会話としては不要なので、削除

・ハッシュタグ
\#ハッシュタグで表記されるハッシュタグ名は会話としては不要なので、削除

・URL
会話としては不要なので、削除

・改行
１行で取得したいので、削除

・絵文字？
UTF-8でない文字が含まれることがあるので、削除
[Remove non-UTF8 characters from string with PHP](https://magp.ie/2011/01/06/remove-non-utf8-characters-from-string-with-php/)

・html特殊文字
\&amp;等、変換した状態で、取得されるので、`htmlspecialchars_decode`で、元の文字に変換

# 利用回数制限
利用回数の制限があるので、制限を超過しないようにする必要があります。
[TwitterのAPI制限 [2017/05/07現在]](https://qiita.com/mpyw/items/32d44a063389236c0a65)

今回、利用するものだと、
`search/tweets`->15分に180回
`statuses/show`->15分に900回
となっています。
少ないですが、crontabの最小単位で、1分に1回、検索語を変えながら、apiを呼ぶようにしました。

# 作成したプログラム

検索語を引数として、取得したつぶやきと返信をそれぞれ、ファイルに出力します。

```php:get_tweet.php
<?php
require "vendor/autoload.php";
use Abraham\TwitterOAuth\TwitterOAuth;

$consumer_key = 'xxxxxxxxxx';
$consumer_secret = 'xxxxxxxxxx';

$tweet_file_name = './tweet.txt'; // ツイートを出力するファイルパス
$reply_file_name = './reply.txt'; // 返信を出力するファイルパス

$search = $argv[1];
if (empty($search)) {
  // 引数がない場合、おわり
  return;
}

// ファイルオープン
$tweet_fp = fopen($tweet_file_name, 'a');
$reply_fp = fopen($reply_file_name, 'a');

$connection = new TwitterOAuth($consumer_key, $consumer_secret);

// twitter検索
$tweet_list = $connection->get('search/tweets', array('q' => $search, 'count' => 100));

// 返信のものを探す
foreach($tweet_list->statuses as $tweet) {
  $in_reply_to_status_id = $tweet->in_reply_to_status_id;
  if (is_null($in_reply_to_status_id)) {
    // in_reply_to_status_idがない場合は、返信ではない
    continue;
  }
  // 返信tweetの内容
  $reply_text = $tweet->text;
  $reply_text = processTweet($reply_text);

  if (empty($reply_text)) {
    // 加工後、メッセージがない場合は、対象外
    continue;
  }

  // 返信元のtweetを取得
  $tweet = $connection->get('statuses/show', array('id' => $in_reply_to_status_id));

  if(!isset($tweet->text)) {
    // 返信元のtweetが取得できない場合、対象外
    continue;
  }

  // 返信元のtweet
  $parent_text = $tweet->text;
  $parent_text = processTweet($parent_text);

  if (empty($parent_text)) {
    // 加工後、メッセージがない場合は、対象外
    continue;
  }

  // ファイル出力
  fwrite($tweet_fp, $parent_text . PHP_EOL);
  fwrite($reply_fp, $reply_text . PHP_EOL);

}

// ファイルクローズ
fclose($tweet_fp);
fclose($reply_fp);

//
// 関数群
//
// tweet加工
function processTweet($text) {
  $text = deleteUser($text);
  $text = deleteNewLine($text);
  $text = deleteUrl($text);
  $text = deleteHashtag($text);
  $text = deleteNonUtf8($text);
  $text = convertHtmlSpecialCharcter($text);
  return $text;
}
// ユーザ名（@～）を消す
function deleteUser($text) {
  return preg_replace('/@.*\s/', '', $text);
}

// 改行をスペースに変換
function deleteNewLine($text) {
  return str_replace(array("\r\n", "\r", "\n"), ' ', $text);
}

// http引用を消す
function deleteUrl($text) {
  return preg_replace('/(https?)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)/', '', $text);
}

// ハッシュタグ（#～）を消す
function deleteHashtag($text) {
  return preg_replace('/#.*/', '', $text);
}

// 変換不能文字を消す
function deleteNonUtf8($text) {
  //reject overly long 2 byte sequences, as well as characters above U+10000 and replace with ?
  $text = preg_replace(
    '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]'.
    '|[\x00-\x7F][\x80-\xBF]+'.
    '|([\xC0\xC1]|[\xF0-\xFF])[\x80-\xBF]*'.
    '|[\xC2-\xDF]((?![\x80-\xBF])|[\x80-\xBF]{2,})'.
    '|[\xE0-\xEF](([\x80-\xBF](?![\x80-\xBF]))|(?![\x80-\xBF]{2})|[\x80-\xBF]{3,})/S',
    '', $text );

  //reject overly long 3 byte sequences and UTF-16 surrogates and replace with ?
  $text = preg_replace(
    '/\xE0[\x80-\x9F][\x80-\xBF]'.
    '|\xED[\xA0-\xBF][\x80-\xBF]/S',
    '', $text );

  return $text;
}

// html特殊文字を変換する
function convertHtmlSpecialCharcter($text) {
  return htmlspecialchars_decode($text);
}
```