Twitterでの自分のTweetsをtDiaryにまとめて投稿する - その1. 過去ログ編, Twitterでの自分のTweetsをtDiaryにまとめて投稿する - その2. 日次バッチ編, きょうのつぶやき
Twitterでの自分のTweetsをtDiaryにまとめて投稿する - その1. 過去ログ編
久々にこのブログを見た人は、左側の「最近の話題」を見てびっくりしてしまったかもしれませんが、この週末はいろいろ忙しい中、Twitterに溜まっていた自分の過去のつぶやき(Tweets)を、この自分のブログ(tDiary)に毎日まとめて投稿するスクリプトを作成したりしていました。忙しいとかえってそういうことをしたくなったりしますよね(笑。
どうしてそんなことしたくなったのか、というといくつか理由があって、1)Twitterを使うようになってブログの更新頻度が激減してしまっているのを何とかしたいのが第一(笑、2)一つの会社に管理されているTwitter内の自分の書き込みのバックアップを念のため取っておきたかった1、3)さらにTwitterに投稿している写真データ2もバックアップしておきたい、4)Twitterは利用しないがこっちは見ている人もいるかもしれない、などなどでした。Twitterを単なる利用者としてではなく、開発者として叩いてみておきたかった、という面もあります。
さて、Twitterでの自分のつぶやきをtDiaryへ登録する、という今回僕がやりたかったことと同じことをやっている人は、ググッてみると結構たくさんいらっしゃいます。しかし、僕がググった限り、僕のニーズにピッタリ合った方法は見つかりませんでした。主に問題だったのが、TwitPicに登録された写真の扱いで、写真へのTwitPicサイトへのリンクを作成したりしている人もたくさんいらっしゃったのですが、投稿された写真そのものをローカルにダウンロードしてバックアップしておく、という方法を取っている人は見つかりませんでした。また、同じようなことをしていても一部の処理を手動で行っていたり(僕はcron起動で毎日勝手に投稿してくれるような形にしたいと思っていました)、投稿されるTweetsのフォーマット・中身がイマイチだったり、そもそも投稿スクリプトが公開されていなかったりと、どうもぴったり来るものはなさそうだということがわかりました。
既存のソリューションをググッて探す一方で、自分でTwitter/TwitPicからつぶやきが写真を取り出して投稿するのがどのくらい難しいのかも調べ始めていたんですが、アクセスのための材料(ドキュメント・ライブラリ)も既に豊富にそろっており、そんなに難しくなさそうなことが伺えました。そんなわけで自分で作ってみることにしてしまったわけです。
一口に「自分のTweetsをまとめてtDiaryに再投稿する」といっても、この問題には大きく2つの側面があります。一つが既に1400tweetsあまりたまった自分の過去のつぶやきをどうするか、もう一つが今後毎日まとめ投稿をするための仕組み作りです。もちろん、両者に共通する技術要素は多いですが、異なる部分も多いと思いました。例えば前者(過去ログ)の問題は、ワンショットの処理となりますからテキトーなスクリプト+ある程度のマニュアル処理でも問題ありません。また今この瞬間に動きさえすれば良いので、最近話題の「Twitterの認証問題3」も関係ありません。そんなわけで、まずは前者の問題に取り組むことにしました。基本的な方針としては、後者の問題向けの知識を蓄えること+とにかく手間をかけないこと、としました。また、上でも書きましたが、Twitterに投稿されている自分のつぶやきをこのtDiary側へコピーすること、個人宛つぶやき(@〜で始まるつぶやき)は含めないこと、自分のTwitPicへ投稿した写真はローカルにダウンロードしたのち、tDiaryのimage_exプラグインを使って表示すること、あたりを最低限の要求仕様とすることにしました。
さてまず、前者後者共通してサーバ側での処理のために使うツールとしては、今回はrubyを選択することにしました。個人的にはまだperlやshほどrubyのコードは書いたことはなく、少し不安な(というかめんどくさい)面もあったんですが、特にネット系のツールをお手軽に書くための材料が豊富に揃っていること、個人的にそろそろ(というか遅過ぎるけど・汗)rubyにも慣れておきたいと思ったという辺りが主な理由です。結果的にはrubyの選択は大正解でしたね。とにかく何をやるにも楽で、短い時間でささっとコードを書かないといけない今回の僕のようなケースにはピッタリでした。
Twitterにはいろいろ公開されているAPIがありますが、その中で今回僕が使ったのは、id指定(スクリーンネーム指定)でつぶやきを時系列順で取得する、user_timeline APIだけです。まず最初に、自分のプログラムのデバッグでTwitter側に負荷をかけるのも忍びなかったため、とりあえず過去の自分のつぶやきを全て、JSON形式でダウンロードしておくことにしました。ワンショットでダウンロード出来ればよかったので、今回はwgetコマンドとrubyを使い、以下のようにして行ないました。
-
まず、wgetコマンドで最初(新しい方から)200件のつぶやきをJSON形式でダウンロードします。この時、既にTwitterへログイン済みの(cookieのついた)ブラウザならそのまま実行できますが、wgetのようなcookieを持たないクライアントを使う場合、BASIC認証を指定する必要があります。
$ wget –user=SCREEN_NAME –password=PASSWORD -O 1.json http://api.twitter.com/1/statuses/user_timeline/SCREEN_NAME.json?count=200
-
次にダウンロードされたJSONファイルを例えば下記のようなスクリプトを使ってrubyで読み、最後の要素が含む"id"値を調べます4。
#!/usr/bin/env ruby
require 'rubygems'
require 'json/pure'
json_doc = ""
open("#{1.json", "r") do |file|
json_doc = file.read
end
JSON.parse(json_doc).each do |status|
puts "id = #{status['id']}"
end
-
さらに、上で調べた最後の要素(=一番古い要素)のidを使って、以下のように「それよりも古いつぶやき」を再びJSON形式でダウンロードします。
$ wget –user=SCREEN_NAME –password=PASSWORD -O 2.json “http://api.twitter.com/1/statuses/user_timeline/SCREEN_NAME.json?count=200&max_id=[上で調べた一番古いつぶやきのid]”
-
以上を、古いつぶやきがなくなるまで繰り返します。
ちなみに上記処理はとても単純な処理なので、上の処理自体を一つのrubyスクリプトにすることも簡単なわけですが、僕が試していたところ、上記のようなAPI呼出を間髪入れず実行してしまうと、時々TwitterがJSON形式のデータではなく何だか分からないHTMLデータを返すことがあり(もしかしたら鯨が出てたのかなぁ?)、今回はたかだか8回程度の繰り返しだったのでマニュアルで処理してしまいました。
さて、そうやって手元にJSON形式の過去つぶやきデータがダウンロードされたあとは、tDiaryへそれらを適当に整形してPOSTするだけです。最初、tDiaryへHTTP POSTでつぶやきを投稿するのではなく、直接ローカルにある日記データに追記してしまおうかとも考えたのですが、キャッシュやRSSを生成しているプラグインなどへの影響を考えて、またしてもwgetコマンドを使ってHTTP POSTする、という選択を取ることにしました5。
過去ログPOSTに使ったスクリプトは下記の通りです。自分にとってスクラッチから書いた初めてのrubyスクリプト&ワンショットしか動かさないつもりだったので超テキトーな点はご容赦くだされ。
#!/usr/bin/env ruby
require 'rubygems'
require 'json/pure'
require 'parsedate'
require 'kconv'
require 'cgi'
require 'uri'
SCREEN_NAME = 'SCREEN_NAME'
TDIARY_UPDATE_URI = 'http://memo.digitune.org/update.rb'
TWITTER_URI = 'http://twitter.com/'
TWITPIC_URI = 'http://twitpic.com/'
IMAGE_PATH = '/home/kazawa/src/tdiary/images/'
all_tweets = {}
$image_seq = 0
(1..8).each do |i|
json_doc = ""
open("#{i}.json", "r") do |file|
json_doc = file.read
end
JSON.parse(json_doc).each do |status|
all_tweets[status['id']] = status
end
end
def link_uri(tweet)
URI.extract(tweet, ["http", "https"]) do |uri|
tweet = tweet.gsub(uri, "<a href=\"#{uri}\">#{uri}</a>")
end
tweet
end
def link_twitter(tweet)
tweet = tweet.gsub(/@(\w+)/, "@<a href=\"#{TWITTER_URI}\\1\">\\1</a>")
tweet
end
def download_twitpic(tweet, date)
URI.extract(tweet) do |uri|
if /^#{TWITPIC_URI}/ =~ uri
res = `wget -O - #{uri} 2>/dev/null`
own_photo = nil
res.each_line do |line|
if /photo_username.*\">(\w+)<\// =~ line
puts "photo_username = #{$1.strip}"
own_photo = 1 if $1.strip == SCREEN_NAME
end
end
break if ! own_photo
url = ""
res = `wget -O - #{uri}/full 2>/dev/null`
res.each_line do |line|
if /img.*src=\"([^\"]+)\".*alt/ =~ line
url = $1
puts "url = #{url}"
end
end
puts "wget -O #{IMAGE_PATH}#{date}_#{$image_seq}.jpg --referer=#{uri}/full \"#{url}\""
system "wget -O #{IMAGE_PATH}#{date}_#{$image_seq}.jpg --referer=#{uri}/full \"#{url}\""
puts "convert #{IMAGE_PATH}#{date}_#{$image_seq}.jpg -resize 60x80 #{IMAGE_PATH}s#{date}_#{$image_seq}.jpg"
system "convert #{IMAGE_PATH}#{date}_#{$image_seq}.jpg -resize 60x80 #{IMAGE_PATH}s#{date}_#{$image_seq}.jpg"
tweet = "<%=image_right #{$image_seq}%>" + tweet
$image_seq += 1
end
end
tweet
end
def post_tweets(body, last_t)
body = "きょうのつぶやき\n" + body
puts "body => #{body}"
post_data = "old=#{last_t.strftime('%Y%m%d')}&year=#{last_t.year}&month=#{last_t.month}&day=#{last_t.day}&title=&body=#{CGI.escape(body.toeuc)}&makerss_update=false&append=#{CGI.escape(' 追記 '.toeuc)}"
puts "wget -d --user TDIARY_USER --password TDIARY_PASS --referer #{TDIARY_UPDATE_URI} --post-data '#{post_data}' #{TDIARY_UPDATE_URI}"
system "wget -d --user TDIARY_USER --password TDIARY_PASS --referer #{TDIARY_UPDATE_URI} --post-data '#{post_data}' #{TDIARY_UPDATE_URI}"
end
body = ""
last_t = nil
last_date = nil
all_tweets.keys.sort.each do |id|
status = all_tweets[id]
d = ParseDate::parsedate(status['created_at'])
t = Time.gm(*d[0..5]).localtime
ts = t.strftime("%F")
if last_date != nil && last_date != ts
post_tweets(body, last_t) if body != ""
$image_seq = 0
body = ""
end
if /^@/ =~ status['text']
puts ("skip: #{status['text']}")
next
end
tweet = download_twitpic(status['text'], t.strftime('%Y%m%d'))
tweet = link_twitter(link_uri(tweet))
body = body + "<p>#{tweet} <font size=-2>(#{t.strftime('%H:%M')} #{status['source']}から)</font></p>\n"
last_t = t
last_date = ts
end
post_tweets(body, last_t) if body != ""
随所に乱れ飛ぶputsはデバッグ表示ですので鬱陶しければ消しても大丈夫です。上記スクリプトはまるでrubyっぽくなかったり(汗、TwitPicのHTMLを超適当に読み込んでいたり、Twitterにハッシュタグに未対応だったり、サムネイルイメージのサイズが元画像にかかわらず固定だったりといろいろショボい箇所があるんですが、まぁとりあえず動いたのでこれはこれで良いのでは、と<ヲ。さて、その2. 日次バッチ編へ続きます。
Twitterでの自分のTweetsをtDiaryにまとめて投稿する - その2. 日次バッチ編
さて、過去ログの処理が終わったので、次は今後のための日次バッチの作成です。やるべきことは過去ログ編とほとんど変わらないため、スクリプトももうほとんどできている、といっても過言ではないのですが、上で書いたとおり、一点新しいつぶやきをJSON形式で取得するための、Twitter APIの認証部分については、上でやったようなテキトーなことではすぐに動かなくなってしまいますので6、もう少し真面目に考える必要がありました。
しかしまぁ最近は良い時代になったもので、そのような技術的課題に直面しても、ことオープンな世界の出来事であれば、ほぼ確実に先達が解決済みだったりします。今回のケースもグーグル先生にちょっと聞いてみさえすればすぐに、ruby+OAuthを実現しておられる方のページが見つかりました。僕が今回主に参考にしたのはこちらのページです。どうもありがとうございました>しばそんさん。
そんなわけで上記ページをベースに元のスクリプトをOAuthに対応させ、またTwitterのハッシュタグに対応させたり多少コードを整理したりした結果の、日次バッチ用スクリプトを以下に貼っておきます。僕はこのスクリプトを夜中の0:00に実行されるよう、cronに設定しています。あ、実行前に、最後に投稿した過去つぶやきのidを、last_id.txtファイルに書いておかないと上手く動きません。いろいろテキトー過ぎるスクリプトで申し訳ないッス…。
#! /usr/bin/ruby
# encoding: utf-8
require 'time'
require 'rubygems'
require 'oauth'
require 'rubytter'
require 'kconv'
require 'parsedate'
require 'cgi'
require 'uri'
require 'fileutils'
# constants
SCREEN_NAME = 'SCREEN_NAME'
TDIARY_UPDATE_URI = 'http://memo.digitune.org/update.rb'
TWITTER_URI = 'http://twitter.com/'
SEARCH_PATH = 'search?q='
TWITPIC_URI = 'http://twitpic.com/'
IMAGE_PATH = '/home/kazawa/src/tdiary/images/'
LAST_ID_PATH = '/home/kazawa/src/tweets2tdiary/last_id.txt'
TITLE = "きょうのつぶやき\n"
# secret information
CONSUMER_KEY = 'CONSUMER_KEY'
CONSUMER_SECRET = 'CONSUMER_SECRET'
ACCESS_TOKEN = 'ACCESS_TOKEN'
ACCESS_TOKEN_SECRET = 'ACCESS_TOKEN_SECRET'
# global variables
$image_seq = 0
# functions
def link_uri(tweet)
URI.extract(tweet, ["http", "https"]) do |uri|
tweet = tweet.gsub(uri, "<a href=\"#{uri}\">#{uri}</a>")
end
tweet
end
def link_twitter(tweet)
tweet = tweet.gsub(/@(\w+)/, "@<a href=\"#{TWITTER_URI}\\1\">\\1</a>")
tweet = tweet.gsub(/#(\w+)/, "<a href=\"#{TWITTER_URI}#{SEARCH_PATH}%23\\1\">#\\1</a>")
tweet
end
def download_twitpic(tweet, date)
URI.extract(tweet) do |uri|
if /^#{TWITPIC_URI}/ =~ uri
res = `wget -O - #{uri} 2>/dev/null`
own_photo = nil
res.each_line do |line|
if /photo_username.*\">(\w+)<\// =~ line
puts "DEBUG:photo_username = #{$1.strip}"
own_photo = 1 if $1.strip == SCREEN_NAME
end
end
break if ! own_photo
url = ""
res = `wget -O - #{uri}/full 2>/dev/null`
res.each_line do |line|
if /img.*src=\"([^\"]+)\".*alt/ =~ line
url = $1
puts "DEBUG:url = #{url}"
end
end
puts "DEBUG:wget -O #{IMAGE_PATH}#{date}_#{$image_seq}.jpg --referer=#{uri}/full \"#{url}\" 2>/dev/null"
system "wget -O #{IMAGE_PATH}#{date}_#{$image_seq}.jpg --referer=#{uri}/full \"#{url}\" 2>/dev/null"
puts "DEBUG:convert #{IMAGE_PATH}#{date}_#{$image_seq}.jpg -resize 60x80 #{IMAGE_PATH}s#{date}_#{$image_seq}.jpg"
system "convert #{IMAGE_PATH}#{date}_#{$image_seq}.jpg -resize 60x80 #{IMAGE_PATH}s#{date}_#{$image_seq}.jpg"
tweet = "<%=image_right #{$image_seq}%>" + tweet
$image_seq += 1
end
end
tweet
end
def post_tweets(body, last_t)
body = TITLE + body
puts "DEBUG:body => #{body}"
post_data = "old=#{last_t.strftime('%Y%m%d')}&year=#{last_t.year}&month=#{last_t.month}&day=#{last_t.day}&title=&body=#{CGI.escape(body.toeuc)}&append=#{CGI.escape(' 追記 '.toeuc)}"
puts "DEBUG:wget -O /dev/null --user USERNAME --password PASSWORD --referer #{TDIARY_UPDATE_URI} --post-data '#{post_data}' #{TDIARY_UPDATE_URI} 2>/dev/null"
system "wget -O /dev/null --user USERNAME --password PASSWORD --referer #{TDIARY_UPDATE_URI} --post-data '#{post_data}' #{TDIARY_UPDATE_URI} 2>/dev/null"
end
# connect
consumer = OAuth::Consumer.new(
CONSUMER_KEY,
CONSUMER_SECRET,
:site => 'http://twitter.com'
)
access_token = OAuth::AccessToken.new(
consumer,
ACCESS_TOKEN,
ACCESS_TOKEN_SECRET
)
client = OAuthRubytter.new(access_token)
# read last_id
last_id = 0
open(LAST_ID_PATH, "r") do |file|
last_id = file.gets.chomp.strip
end
puts "DEBUG:last_id = #{last_id}"
# read tweets
body = ""
last_t = nil
last_date = nil
client.user_timeline('digitune', {:since_id => last_id, :count => 200}).reverse_each do |status|
puts "DEBUG:id = #{status.id}"
last_id = status.id
d = ParseDate::parsedate(status.created_at)
t = Time.gm(*d[0..5]).localtime
ts = t.strftime("%F")
if last_date != nil && last_date != ts
post_tweets(body, last_t) if body != ""
$image_seq = 0
body = ""
end
if /^@/ =~ status.text
puts "DEBUG:skip: #{status.text}"
next
end
tweet = download_twitpic(status.text, t.strftime('%Y%m%d'))
tweet = link_twitter(link_uri(tweet))
body = body + "<p>#{tweet} <font size=-2>(#{t.strftime('%H:%M')} #{status.source}から)</font></p>\n"
last_t = t
last_date = ts
end
post_tweets(body, last_t) if body != ""
# write last_id
FileUtils.cp(LAST_ID_PATH, LAST_ID_PATH + ".old")
open(LAST_ID_PATH, "w") do |file|
file.puts last_id
end
きょうのつぶやき
あーテステス #bot (19:20 webから)
TwitterのつぶやきをまとめてtDiaryに投稿するテキトーなスクリプトを書きました。あまりにテキトー過ぎでも驚かぬよう。>「Twitterでの自分のTweetsをtDiaryにまとめて投稿する」http://memo.digitune.org/?date=20100725 (22:29 webから)
テストも兼ねて。鳥乃が作った我が家の愛犬、スー。 http://twitpic.com/28kc0o (22:39 twiccaから)
うおっと家庭内LANからお家サーバには素直には繋がらないんだった。えーとどうするんだっけ?(汗。androidに/etc/hostsってある? (23:18 twiccaから)