Node.jsのSocket.ioでTwitterスクレイピングアプリを作ってみた

# 作ったもの

■メンテ情報まとめったー
https://damp-inlet-36386.herokuapp.com/
https://github.com/yoruno07/mente-checker (ソースコード）

ソーシャルゲームの各公式ツイッターからメンテナンスに関するキーワードを元にスクレイピングし、まとめて表示するアプリです。
各ゲーム表示部分は追加・削除やドラッグによる並べ替えが出来ます。

デプロイはHerokuを今回始めて使用しましたが、サーバーをレンタルせずに手軽にリリースが行えるのは本当に便利だと感じました。

# 使用した技術・サービス等
- Node.js
 - Twit (https://github.com/ttezel/twit)
 - Socket.IO (https://socket.io/)
 - Express
- Heroku
- jQuery UI
- Bootstrap

# 何故作ったのか
半分は自分用です（笑）
もう半分はNode.jsの勉強もかねて、Twitterをスクレイピングするのに便利なライブラリがあるということで、試しに使ってみました。

ソーシャルゲームって大体のところはTwitterで前々から告知して行うのですが、いくつもフォローしていると見落としてしまったり、緊急メンテの際に気づかなかったりする部分があったので、まとめて見れたら便利かと思い作ってみました。

# 技術部分の解説
## 全体の流れ
アプリ全体の流れとしては、

各ゲームの情報（アカウント名やスクレイピング用のキーワード）をJSONファイルから読み込む
↓
Node.jsのTwitライブラリを使用し、TwitterAPIを通して検索
↓
検索結果をフォーマットし、ツイート部分をSocket.IOを通してフロントエンドに送信
↓
フロント側は予めページが開かれた際にSocket.IOを受信状態にしておき、サーバー側からツイートデータが送られたら表示する

といった流れになります。
一度確立したSocketは繋げたままになるので、ページが開かれたままでも追加で公式ツイッターの更新があった場合は反映されます。

## サーバーサイド
Twitライブラリは検索を投げるgetメソッドと繋ぎっぱなしにしてつぶやきを取得するstreamメソッドがあります。アプリの仕様的にはstreamメソッドでずっと取得状態を維持したかったのですが、TwitterAPIの仕様により、**日本語のキーワードが上手く検索で引っ掛けられない**ことが判明しました。
そこで時間ごとに定期的にgetメソッドを回すことで擬似的なstream状態を作っています。
（ただしこのやり方はTwitterAPIの上限を圧迫するので、気をつけないとすぐ上限エラーになります。）


```js
// 3分ごとに最新のツイートを取得し、更新がないか確認
// streamは日本語の検索が未対応のため、searchメソッドを一定時間ごとに叩くことで擬似リアルタイム表示とする
function tstream(checker) {
  setInterval(function(){
                    tGet(checker, 1, null);
                  }, 180000);
}


// Twitterからテキストを取得してsocketで発信
function tGet(checker, count, socket_id) {
  var account = checker.account;
  var keyword =  checker.keywords.join(" OR ");
  var eventname = checker.eventname;
  var last_id = checker.last_id;
  T.get('search/tweets', { q: keyword + ' from:' + account, count: count}, function(err, data, response) {
       if  (err) {
          // APIエラーが発生した場合はその旨を表示
          io.to(socket_id).emit('error', err);
          return console.log("ERROR: " + err);
       } else {
          // APIエラーが発生しない場合は解消された合図を送る
          io.to(socket_id).emit('no-error', err);
          var statuses = data.statuses;
          var max_count = statuses.length;
          // 古いツイートから順に送信するため逆順で配列を回す
          for (i=max_count-1; 0 <= i; i--) {
            // 前回取得したツイートとidが一致した場合は新しいツイートがないとみなし、発信を行わない（setInterval時用）
            if (statuses[i].id === last_id) break;
            // socketでツイート内容を送信（初回接続の場合はその接続先にのみ送るためid指定で個別に送る）
            if (socket_id !== null) {
              io.to(socket_id).emit(eventname, {id_str: statuses[i].id_str, text: statuses[i].text, date: convertTwitDate(statuses[i].created_at)});
            } else {
              io.sockets.emit(eventname, {id_str: statuses[i].id_str, text: statuses[i].text, date: convertTwitDate(statuses[i].created_at)});
            }
            // 最新のツイートのIDを次回比較用に保持
            if (i === 0) checker.last_id = statuses[i].id;
          }
      }
  });
}
```

## フロントエンド
予めページを開いた際にSocketの通信を各ソシャゲごとに確立しておきます。
サーバー側で取得したツイートのデータをsocketで受信し、受信イベントが発生した際に受け取ったデータをフロントの情報表示部分に追加します。

非同期処理のため、接続後にfirst-connectの合図を送り、その後スクレイピングが走るようにしています。
また再接続時に重複で追加されてしまうことを防ぐため、既存の表示データは接続が走った際に削除するようにしています。

```js
  var socket = io.connect();
  socket.on('connect', function() {
    // 接続時に既存の要素は一旦削除
    $('li:not(.no-data)').remove();
    // 初回接続時にサーバー側に合図を送る
    socket.emit('first', 'first-connect');
  });

  var status_url = '';
  $.getJSON('/json/config.json', function(jsondata) {
    $.each(jsondata.games, function(key, val) {
        socket.on(val.eventname, function(info) {
          // tweetへのリンク先を生成
          status_url = jsondata.twitter_url+val.account+'/status/'+info.id_str;
          // tweetを取得できた場合はno-data時のテキストを削除
          $('div#' + val.id).find('li.no-data').remove();
          // listとしてカード内に情報を追加
          $('div#' + val.id).children('ul.info-list').prepend('<li class="list-group-item"> '+info.text+'<br /><a href="'+status_url+'" class="card-link" target=”_blank”>'+info.date+'</a></li>');
        });
    });
  });
```

# 課題
- 接続時にAPIでスクレイピングを走らせる仕様上、接続分だけAPIを使用するため、TwitterAPIの上限に引っかかってしまう。
→解決にはデータを定期的にスクレイピングし、DBに納めるなど接続数にAPIの使用回数が依存しない作りにする必要がある。

- Herokuの無料プランのため、アクセスしないとスリープ状態に入るため立ち上がりが少し遅いときがある。とはいえサーバーを借りずにアプリをリリース出来る手軽さは本当に便利だったので、また使っていきたい。

- ドラッグによる並べ替えは実装したものの、更新したり再アクセスすると元に戻ってしまうため、Cookieなどに表示状態を保存できるといいかもしれない。