Tweet |
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でデーモン化する -でじうぃき
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
- Newer: Microsoft OneDrive for Androidでファイルをダウンロードするとデータが化けることがある、その原因とは?
- Older: #ZenTour大感謝祭 でZenfone3を貸与頂いたのでレビューしてみる。その3:Zenfone3の良い点、悪い点(改善して頂きたい点)
Comments:0
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破壊日記的ブログ