Amazon.co.jp ウィジェット Twitterのタイムライン(TL)をnode+MongoDBで保存、Ruby+Sinatraで表示できるようにしてみた。 - PC破壊日記的ブログ

Home > PC破壊日記 > | ソフトウェア > | ソフト公開 > Twitterのタイムライン(TL)をnode+MongoDBで保存、Ruby+Sinatraで表示できるようにしてみた。

Twitterのタイムライン(TL)をnode+MongoDBで保存、Ruby+Sinatraで表示できるようにしてみた。

このエントリーをはてなブックマークに追加

Twitter、私も利用しているのですが、フォロー数が多くなると問題となるタイムラインの流速問題。そうでなくても、公式クライアント含む各種Twitterではタイムラインの保存に限度があるので、過去の物を検索しようとしても不可能か、かなり困難です。
ならばTLを保存すれば良いのではないか?ということで、いろいろ調べた結果、「Twitter APIで取得できるデータはJSON形式、なのでMongoDBと親和性高い」ということで、この方向で進めることに。

参考にしたサイトが多数にわたるため、本記事の最後に記載します。参考にさせて頂いたことに感謝します。

1. 前準備

まずはRuby、node、MongoDBをインストール。私の環境はubuntu16.04 LTSですが、他のLinuxディストリビューションでも同様の環境は準備可能だと思います。ただ、できればOS/MongoDBは64bitを推奨。(データベースのサイズ制限、メモリ制限ほか様々な制限のため)

また、各ソフトウェアの細かい設定はしていない(標準設定のまま)なので、必要に応じ情報を収集して対応して下さい。


sudo apt install ruby
sudo apt install nodejs-legacy
sudo apt install npm

MongoDBについては、ubuntu16.04 LTSで標準のaptリポジトリでインストールされるパッケージが古すぎる(2.6.x系)ので、MongoDBの公式のインストール手順に従って3.x系をインストールして下さい。(2.6.x系だとソート時にソート用バッファメモリ不足が発生し、かつ回避が困難です)

Install MongoDB Community Edition on Ubuntu -- MongoDB Manual 3.4
https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/

一応、MongoDBが64bitでインストールされているか確認。


user@hoge:~$ mongo
> use admin
switched to db admin
> db.runCommand("buildInfo")
(中略)
"bits" : 64,
(後略)

問題無さそう。

ライブラリも必要。(途中で必要になる)

sudo apt install ruby-dev

次はnode用パッケージのインストール。

次はRuby-gemのインストール

2. MongoDBのアクセス制限の設定

MongoDB 3.6はパフォーマンスやセキュリティの関係で色々ワーニングが出ます。公式マニュアルにも記載がありますが、他にも日本語での説明ページがあったのでリンクしておきます。

MongoDB 3系を CentOS7 にインストール - Qiita
https://qiita.com/SOJO/items/dc5bf9b4375eab14991b

これ以外にも公式マニュアル他を参照して設定して下さい。まあデフォルトのままでも動きますけどね・・・。

3. TwitterのAPI KEYを取得する。

これはあちこちに解説記事があるので省略したい。必要な権限はツイートの読み取り権限のみです。書き込みはしないので今回は不要です。
consumer_key, consumer_secret, access_token_key, access_token_secretの4つのキーを後で使います。
一応、API申請ページにリンクしておきます。

4. Twitter TL取得用のスクリプトの作成。(前準備含む)

タイムライン取得にはMongoDBと親和性のあるNode.jsを利用します。まずはスクリプトの保存フォルダの作成と必要なライブラリの入手。


$ mkdir tw2db
$ cd tw2db
$ npm install mongoose
$ npm install twitter

次に、スクリプトの作成。これは下記サイトの内容そのままで動いたので丸パクリです(汗)
ただし、MongoDBのユーザー認証を有効にしているので、そこだけ改変しています。 Twitter の TL を全部 MongoDB にぶち込んでニヤニヤする - 凹みTips


var twitter  = require('twitter')
  , mongoose = require('mongoose')
  , Schema   = mongoose.Schema
;

// typeof で得た文字列を型に変換
var typeMap = {
        number   : Number,
        string   : String,
        boolean  : Boolean,
        object   : Object,
        function : Function
};

// オブジェクト/配列を受け取って Mongoose 用 Schema に変換
function makeSchema(data) {
        var schema = {};
        for (var x in data) {
                var type = typeof data[x];
                if (data[x] === null) {
                        schema[x] = Object;
                } else if (type === 'object') {
                        schema[x] = makeSchema(data[x]) ;
                } else {
                        schema[x] = typeMap[type];
                }
        }
        return schema;
}

// MongoDB へ接続
mongoose.connect('mongodb://dbuser:dbpasswd@localhost/Twitter');

// mongoose のスキーマ
var PostSchema, Post, isSchemaDefined = false;

// Twitter へ接続
new twitter({
        consumer_key        : 'XXXXXXXXXXXXXXXXXXXXXXXX',
        consumer_secret     : 'XXXXXXXXXXXXXXXXXXXXXXXX',
        access_token_key    : 'XXXXXXXXXXXXXXXXXXXXXXXX',
        access_token_secret : 'XXXXXXXXXXXXXXXXXXXXXXXX'
}).stream('user', function(stream) {
        stream.on('data', function(data) {
                // Friends リストのデータはすっ飛ばす
                if ( !('id' in data) ) {
                        return;
                } else {
                        console.log(data.user.screen_name, data.text);
                }

                // 最初のデータで Schema を作成
                if (!isSchemaDefined) {
                        PostSchema = new Schema( makeSchema(data, '') )
                        Post       = mongoose.model('Post', PostSchema)
                        isSchemaDefined = true;
                }

                // Post Schema から保存用のデータを生成して保存
                var post = new Post(data);
                post.save( function(err) {
                        if (err) console.error(err);
                });
        });
});

// 例外処理
process.on('uncaughtException', function (err) {
        console.log('uncaughtException => ' + err);
});

もちろんconsumer_key, consumer_secret, access_token_key, access_token_secretの部分は2.で取得した情報と書き換えて下さいね。
あと、ユーザー認証をする場合とユーザー認証をしない場合で、下記の行の修正が若干異なります。


*ユーザー認証する場合
mongoose.connect('mongodb://dbuser:dbpasswd@localhost/Twitter');
上記のdbuser、dbpasswdを置き換えて下さい。
*ユーザー認証しない場合
mongoose.connect('mongodb://localhost/Twitter');

あとは、動作テスト。


node tw2db.js

これで、自分のタイムラインに流れているツイートと同じものが流れてくるはずです。

5. Node.jsの自動起動と、自動Kill(cronで)

まずはNode.jsの起動を自動化します。これは下記ページを参考にして下さい。私はinit.dでサービス起動する形にしました。

initd-foreverでNode.jsアプリをデーモン化する | Developers.IO
https://dev.classmethod.jp/server-side/daemonize-nodejs-by-initd-forever/

Node.jsが重すぎるのか、一日以上経過すると頻繁にフリーズしているので、自動killするためのスクリプトを作ります。 cronで実行するので、シェルスクリプトに。ファイル名は例えば「autokill.sh」とかで。もちろんchmodで実行可能にするのを忘れずに。

#!/bin/sh

pid=`ps -elf | grep "tw2db.js" | grep -v grep | awk '{print $4}'`
if [ "${pid}" != "" ]; then
kill ${pid}
echo "kill ${pid}"
fi
sleep 2
nohup /usr/bin/node /path/to/tw2db/tw2db.js > /dev/null &

あとはcronに登録しておいて下さい。だいたい1日1回ぐらいでも良いと思います。私は1時間に一回(毎時40分頃)にしていますが・・・。

6. ビューア部分の作成(Ruby+Sinatra)

ある意味ここからが本番。

ビューアに必要なライブラリはこちら。


$ gem install mongo
$ gem install twitter
$ gem install sinatra
$ gem install sinatra-i18n
$ gem install rack-contrib
他にもあったかも・・・

そして、私が作成したスクリプトがこちら。なお、以下5つのファイル/フォルダを作成しています。


~/tw2db/viewer.rb
~/tw2db/views/tweets.erb
~/tw2db/config/application.rb
~/tw2db/config/locales/en.yml
~/tw2db/config/locales/ja.yml
~/tw2db/viewer.rb
require 'rubygems'
require 'mongo'
require 'sinatra'
require 'sinatra/i18n'
require 'rack/contrib'
require 'date'
require 'rack'
use Rack::Deflater
Sinatra.register Sinatra::I18n
#use Rack::Locale

#require './config'
CONNECTION_STRING = "mongodb://dbuser:dbpasswd@localhost/Twitter"
COLLECTION_NAME = "posts"
TAGS = ["mongodb","ruby"]
set :environment, :production

# The Unix epoch is the time 00:00:00 UTC on January 1, 1970
UNIX_EPOCH_TIME = Time.at(0)

# Strict version of +Time.parse+, returns +nil+ when parsing is failed.
def strict_parsetime(string)
  # +Time.parse+ returns localtime "1970/01/01 00:00:00" when parsing is failed.
  # So, ugly, I check whether returned value is UNIX epoch.
  time = Time.parse(string, UNIX_EPOCH_TIME) rescue nil
  if UNIX_EPOCH_TIME == time then
    # Previous +Time.parse+ possibly failed.
    time = nil unless (ParseDate.parsedate(string)[0] rescue nil)
  end
  time
end

def tweet_id2time(id)
  case id
  when Integer
    Time.at(((id >> 22) + 1288834974657) / 1000.0)
  else
    nil
  end
end

def time2tweet_id(time)
  (time.to_f * 1000 - 1288834974657).to_i << 22
end

configure do
  db = Mongo::Client.new(CONNECTION_STRING)
  TWEETS = db[COLLECTION_NAME]
I18n.default_locale = :ja
I18n.locale = :ja
end

get '/' do
    selector = {}

    @search_text=""
    @from_date=Time.at(1288834974657/1000)
    @to_date=Time.now
  @tweets = TWEETS.find({:id => {'$gte'=> (time2tweet_id(@from_date)),'$lte'=>(time2tweet_id(@to_date))}}).sort({"id" => -1}).limit(10000)
  @twcount=@tweets.count()
  erb :tweets
end
post '/' do
  @from_date=strict_parsetime(params[:from_date])
  @to_date=strict_parsetime(params[:to_date])
  if @from_date==nil
    @from_date=Time.at(1288834974657/1000)
  end
  if @to_date==nil
    @to_date=Time.now
  end
  @search_text=params[:search_text]
  @tweets = TWEETS.find({'$and':[{:text => Regexp.new(@search_text)},{:id => {'$gte'=> (time2tweet_id(@from_date)),'$lte'=>(time2tweet_id(@to_date))}}]}).sort({"id" => -1})
  @twcount=@tweets.count()
  erb :tweets
end

今回のスクリプトは外部接続を受け付ける設定「set :environment, :production がそれ」にしています。ここは必要に応じ改変して下さい。
ユーザー認証をする場合とユーザー認証をしない場合で、下記の行の修正が若干異なります。

*ユーザー認証する場合
CONNECTION_STRING = "mongodb://dbuser:dbpasswd@localhost/Twitter"
上記のdbuser、dbpasswdを置き換えて下さい。
*ユーザー認証しない場合
CONNECTION_STRING = "mongodb://localhost/Twitter"
~/tw2db/views/tweets.erb
<!DOCTYPE>
<html>

<head>
  <style>
    body{
      width:1000px;
      margin: 50px auto;
    }
    h2{
      margin-top:2em;
    }
        pre {
            /* Mozilla */
            white-space: -moz-pre-wrap;

            /* Opera 4-6 */
            white-space: -pre-wrap;

            /* Opera 7 */
            white-space: -o-pre-wrap;

            /* CSS3 */
            white-space: pre-wrap;

            /* IE 5.5+ */
            word-wrap: break-word;
        }
  </style>
    <title>Tweet Archive</title>
</head>

<body>
  <h1>Tweet Archive</h1>
  search by Time/Date <form action="/" method="post">
<br>
search string(Regexp)<input type="text" name="search_text" placeholder"文字列を 入れると検索します" value="<%= @search_text %>"><br>
since<input type="text" name="from_date" placeholder="yyyy/mm/dd hh:mm:ss" value="<%= @from_date %>"><br>
until<input type="text" name="to_date" placeholder="yyyy/mm/dd hh:mm:ss" value="<%=
 @to_date %>"><br>
<input type="submit">
</form>
  <% TAGS.each do |tag| %>
    <a href="/?tag=<%= tag %>"><%= tag %></a>
  <% end %>
<br>
search count:<%= @twcount %> <br>
  <% @tweets.each do |tweet| %>
<hr>
    <pre><h2><font size=6><%= tweet['text'] %></font></h2></pre>
    <p>
      <a href="https://twitter.com/<%= tweet['user']['screen_name'] %>">
        <%= tweet['user']['name'] %>
      </a>
      on <a href="https://twitter.com/<%= tweet['user']['screen_name'] %>/statuses/<%= tweet['id_str'] %>" target="_blank"><%= I18n.l Time.parse( tweet['created_at']).to_time , format: :long %></a><%= Time.at(((tweet['id_str'].to_i >> 22 )+1288834974657)/1000.0).strftime("%Y-%m-%d %H:%M:%S.%L %Z") %>

    </p>

    <img src="<%= tweet['user']['profile_image_url'] %>" width="48" />
  <% end %>

</body>

</html>
~/tw2db/config/application.rb

config.time_zone = 'Tokyo'
config.i18n.default_locale = :ja
~/tw2db/config/locales/en.yml、 ~/tw2db/config/locales/ja.yml (どちらも全く同じ内容です)
ja:
  time:
    formats:
      default: ! '%Y/%m/%d'
      long: ! '%Y年%m月%d日 %H時%M分%S秒 %z'
      short: ! '%Y年%m月%d日 %H:%M'

en:
  time:
    formats:
      default: ! '%Y/%m/%d'
      long: ! '%Y年%m月%d日 %H時%M分%S秒 %z'
      short: ! '%Y年%m月%d日 %H:%M'
あとはスクリプトを実行して、ブラウザからアクセスします。

ruby viewer.rb
立ち上げたサーバーのポート4567にアクセスすればOKです。
例:https://192.168.0.1:4567/
検索部分は正規表現ですが、あまり高機能じゃないので過信しないように。

X.参考にしたサイト群

今回は以下のサイトのスクリプトや情報を元に活用しました。本当に感謝です。

* MongoDBのインストール

MongoDBの薄い本(The Little MongoDB Book) - cuspy diary

https://www.cuspy.org/diary/2012-04-17/

* Twitter TLをMongoDBに保存

Twitter の TL を全部 MongoDB にぶち込んでニヤニヤする - 凹みTips

https://tips.hecomi.com/entry/20120908/1347094725

* forever と initrd-foreverで死活監視

Node.js 自動再起動モジュール - Qiita

https://qiita.com/disc99/items/57490f5eef3e2eb685ba

node.js node.jsスクリプトをforeverでデーモン化する -でじうぃき

https://onlineconsultant.jp/pukiwiki/?node.js%20node.js%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92forever%E3%81%A7%E3%83%87%E3%83%BC%E3%83%A2%E3%83%B3%E5%8C%96%E3%81%99%E3%82%8B

initd-foreverでNode.jsアプリをデーモン化する | Developers.IO
https://dev.classmethod.jp/server-side/daemonize-nodejs-by-initd-forever/

 

* node.jsの自動Kill(頻繁に固まるので)

node.jsの自動再起動 | 爆裂健.com

https://bakuretuken.com/node-js%E3%81%AE%E8%87%AA%E5%8B%95%E5%86%8D%E8%B5%B7%E5%8B%95/

* Ruby+Sinatraでログを表示する

Ruby MongoDB イン・アクション のTweetArchiverを作成する - 1.21 jigowatts

https://sh-yoshida.hatenablog.com/entry/2016/08/27/025808

* Ruby+Sinatraでpostデータ処理(検索用)

Sinatraでフォームからデータを受け取る方法

https://ruby.weva.jp/sinatra/2013/09/10/params.html

* Rubyでの日付文字列の処理の厳格化

Ruby の Time.parse で文字列を Time に変換するときのエラーチェック

https://www.metareal.org/2007/06/21/error-checking-in-ruby-time-parsing/

*Ruby+Sinatraの日付のロケール処理

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita

https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c

Hideki SAKAMOTO の雑記 (2010-09-26)

https://www.on-sky.net/~hs/index.cgi?date=20100926

Sinatra Recipes - Development - I18n

https://recipes.sinatrasapporo.org/p/development/i18n?


Comments:0

Comment Form

Trackbacks:0

TrackBack URL for this entry
https://pc-diary.com/movt_direc_post/mt-tb.cgi/1659
Listed below are links to weblogs that reference
Twitterのタイムライン(TL)をnode+MongoDBで保存、Ruby+Sinatraで表示できるようにしてみた。 from PC破壊日記的ブログ

Home > PC破壊日記 > | ソフトウェア > | ソフト公開 > Twitterのタイムライン(TL)をnode+MongoDBで保存、Ruby+Sinatraで表示できるようにしてみた。

2進数時計
※クリックで読みやすくなります。
※この時計の時刻は、閲覧しているパソコンのものであり、必ずしも正確な時間とは限りません
Search
Feeds
Google Adsense
Tag Cloud

このページの最初に戻る