birdTwitterでの自分の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から)

image 0テストも兼ねて。鳥乃が作った我が家の愛犬、スー。 http://twitpic.com/28kc0o (22:39 twiccaから)

うおっと家庭内LANからお家サーバには素直には繋がらないんだった。えーとどうするんだっけ?(汗。androidに/etc/hostsってある? (23:18 twiccaから)


  1. かつて一度だけ、自分の過去のつぶやきがごっそり消えたことがありました。 ↩︎

  2. 僕はもっぱらTwitPicを利用しています。 ↩︎

  3. これまで手軽に利用できたBASIC認証が使えなくなり、OAuthという仕組みが新たに推奨される。 ↩︎

  4. jsonを読み書きするためのライブラリはgemsにて入手できました。詳しくはグーグル先生へ。 ↩︎

  5. rubyのHTTPライブラリ、net/httpの利用も検討したんですが、ちょっと今回の用途にはプリミティブ過ぎるライブラリで面倒だったのでやめました。 ↩︎

  6. BASIC認証は早ければ8月には使えなくなるかも、と言う話も…。 ↩︎