Twitter のバックアップをCouchDBへ。

今年になってからtwitterを使い始めたのですが、気がついたら3200ポストに到達していました。というわけでバックアップ。どうせなら、うちのバキュームカーなんでもDBになりつつある、ESXi 上のCouchDBに放り込んでおく。


はじめてのtwitter APIプログラム。正しいのかどうか不明。というか3200件全部メモリに展開しなくても;;

#!/usr/bin/env ruby
require 'rubygems'
require 'json'
require 'optparse'
require 'twitter'

mail     = nil
pass     = nil
$verbose  = false

opt = OptionParser.new
opt.on("-u [VAL]") { |v| mail = v }
opt.on("-p [VAL]") { |v| pass = v }
opt.on("-d")       { |v| $verbose = true }
opt.parse!(ARGV)

def log(msg)
  $stderr.puts "[INFO] #{msg}" if $verbose
end

# -- main
PER_PAGE = 200

unless mail
  $stderr.print "Type your address for Twitter: "
  mail = $stdin.gets().chomp
end
unless pass
  $stderr.print "Type your password for Twitter: "
  pass = $stdin.gets().chomp
end

log "Start to fetch timeline with (#{mail}, #{pass})."
log "--"

auth = Twitter::HTTPAuth.new(mail, pass, :ssl => true)
client = Twitter::Base.new(auth)
tl = client.user_timeline(:count => PER_PAGE)
first = tl.first
unless first
  $stderr.puts "No timeline exists!"
  exit 1
end

screen_name = first["user"]["screen_name"]
log "Screen name is '#{screen_name}'"

tls = tl
if tls.length == PER_PAGE
  # fetch with pagination
  page = 2
  while page * PER_PAGE <= 3200
    log "Fetch page(#{page})."
    tl = client.user_timeline(:page => page, :count => PER_PAGE)
    tls = tls + tl
    if tl.length < PER_PAGE
      log "Detected end of timeline (requested #{PER_PAGE} statuses but fetched #{tl.length} statuses."
      break
    else
      page = page + 1
    end
  end
end
log "All pages have been fetched completely."
log "Timeline length = #{tls.length}"
log "--"

doc = {
  "docs" => tls
}

puts doc.to_json

で、とる。

$ ./backup.rb -u XXXX -p PPPP -d > tweets.json

で、あげる。

$ curl -X PUT http://localhost:5984/twitter_backup
$ curl -X POST -d @tweets.json http://localhost:5984/twitter_backup/_bulk_docs

これだけじゃなんなので、ViewとList作った。

// views/by_date/map.js
function(doc){
  emit(new Date(doc.created_at), doc);
}
// lists/timeline.js
// API _list/timeline/by_date
function(head, req){
  // !code vendor/couchapp/date.js
  // !code vendor/ejs/ejs_production.js
  // !code lib/helper.js
  // !json templates.timeline.head
  // !json templates.timeline.row
  // !json templates.timeline.tail

  provides("html", function(){
             send(template(templates.timeline.head, {}));
             while(row = getRow()){
               send(template(templates.timeline.row, {doc: row.value}));
             }
             send(template(templates.timeline.tail, {}));
           });
}
// lib/helper.js
function template(t, b){
  return new EJS({text: t}).render(b);
}

function html_escape(s){
  return s.toString().replace(/&/g, "&amp;").
    replace(/\"/g, "&quot;").
    replace(/\'/g, "&#039;").
    replace(/</g, "&lt;").
    replace(/>/g, "&gt;");
}

function h(s){
  return html_escape(s);
}

function json_escape(s){
  return s.toString().relace(/&/g, "\\u0026").
    replace(/</g, "\\u003c").
    replace(/>/g, "\\u003e");
}

function j(s){
  return json_escape(s);
}
<!-- templates/timeline/head.html -->
<html>
<body>
<ul>

<!-- templates/timeline/row.html -->
<li>
  <span class="created_at"><%= h(doc.created_at) %></span>
  <span class="screen_name"><%= h(doc.user.screen_name) %></span>
  <span class="text">
    <%= h(doc.text) %>
  </span>
</li>

<!-- templates/timeline/tail.html -->
</ul>
</body>
</html>

あ、couchapp 以外にもお気に入りEJS使ってますね。lib/helper.js はローカルでよく使う関数ライブラリで、couchapp generateのときに自動生成するようにCouchApp自体を書き換えています。

http://localhost:5984/twitter_backup/_design/app/_list/timeline/by_date?descending=true でアクセスすると ul/li で3200件全部表示されます。CouchApp で10分作業ですね。

TODO

次は6000件で実行するとして、差分で抽出するようにしないと3200から400件分ぐらい重複しそうなので、その辺を考えよう。6000ポスト当たりになったら。

でも、せっかくのネタなので。

12・1月予定のCouchDB ハッカソン => Relaxon (named by id:z_ohnami) のネタにしようかと思います。(私はもう満足しちゃいましたが。)

Raindrop は?

さすがに現バージョンでバックアップとして機能するわけではないですね・・・。