twitter のサーバから、指定したユーザの過去ログを取得し、XML っぽいものに変換する Ruby スクリプト、をRubyGems-twitter-3.6.0 に対応させた。

概要 (及び、このスクリプトについての説明)

 去年(2011年)の12月に、RubyGems-Twitter1.7.2 を用いて、最大3200件の tweets を xml 形式に変換して出力するスクリプトを作成した*1。しかしこのスクリプトは、RubyGems-Twitter3.6.0 では動作しなくなっていた。

 そこで前回のスクリプトを一行だけ書き換えて、RubyGems-Twitter 3.6.0 で動作するようにした。

動作環境

 RubyGems-Twitter3.6.0 がインストールされている環境で動作する。もしかしたら、その前後のバージョンでも動作するかもしれないが、とくに検証はしていない。

前回からの変更箇所

 その修正箇所は、一箇所だけである。具体的には、以下の行を、

nestHashData_to_nestElementsOfXML( tweet, statusElement )

以下のように置き換えた。

nestHashData_to_nestElementsOfXML( tweet.attrs, statusElement )

 つまり "tweet" のところを "tweet.attrs" に書き換えただけである。とくに動作は検証していないので、もしかしたら、なんらかのバグがあるかもしれない。とりあえず、この変更後のスクリプトは wholeLogSaver version 3.6.0 と名づけることにした。以下は、その wholeLogSaver version 3.6.0 スクリプトの全文。

wholeLogSaver Version 3.6.0

#!/usr/local/bin/ruby

require "rubygems"
require "twitter"
require "rexml/document"
require "optparse"

# wholeLogSaver ver 3.6.0
# 更新年月日: 2012. Oct. 25.
# 
# 指定したユーザの最大3200件の statuses を取得し、
# xml に整形して標準出力に出力するスクリプト。
# 実行には、RubyGem twitter が必要。
#
# 取得した statuses の key と value は、xml の要素に変換している。
# そして、1件の status を一行にまとめて出力している。
# つまり1000件の発言があるユーザを指定すれば、
# 全1000行からなるテキスト・データが出力されることになる。
#
# このスクリプトで使用している Twitter の API には制約がある。
# その制約のために、このスクリプトでは、
# 20 件 * 160 = 3200 件分の statuses しか取得できない。
# この制約は、将来、また変更されるかもしれない。
# (とくに根拠はないが、もし変更されるとすれば、おそらく悪いほうに。)
#
#
# history: 開発履歴
#
# VersionUp from ver 3 to ver 3.6.0.
# Date: 2012. Oct. 25.
#
# 前回のバージョン (wholeLogSaver ver 3) は、RubyGem twitter-1.7.2 で動作していた。
# 今回のバージョンアップは、その ver 3 を、RubyGem twitter-3.6.0 で動作するように手を加えただけのものである。
#
# その修正箇所は、一箇所のみ。具体的には、
# wholeLogSaver ver 3 における以下の行を、
# nestHashData_to_nestElementsOfXML( tweet, statusElement )
#
# wholeLogSaver ver 3.6.0 では、以下のように置き換えた。
# nestHashData_to_nestElementsOfXML( tweet.attrs, statusElement )
#
# ver 3 から ver 3.6.0 への今回のバージョンアップは、とくに詳細な動作検証をしていていないし、
# RubyGem twitter-3.6.0 の中身自体も、くわしく確認していない。
# なので、このスクリプトには、なんらかのバグが潜んでいるかもしれない。


# 入れ子状のハッシュデータを、再帰呼び出しによって xml の要素に変換するための関数。
def nestHashData_to_nestElementsOfXML(hashElement, xmlElement)
        hashElement.each { |k, v|

                if ( v.kind_of?(Hash) )
                        nestHashData_to_nestElementsOfXML( v, xmlElement.add_element(k.to_s) )
                else
                        xmlElement.add_element(k.to_s).add_text v.to_s
                end
        }
end


USAGE_TEXT = "usage: wholeLogSaver [-r] [-x] [-l] [-s] [-h] [-v] username [starting page number : Integer]"
Version = "version: 3.6.0"

WATING_TIME = 30

include_rts = 0
statusNumber = 0
page = 1

isVerbose = false
isPrint_XML_decl = false
isLatest20thStatuses = false
isSlowing = false


# OptionParser を用いて、コマンドラインからのオプションをチェック。
opt = OptionParser.new

# このスクリプトの起動時に -v が与えられているのなら、
# 現在どのような処理を行っているかを饒舌に標準出力に出力。(デバッグ用)
opt.on("-v", "Cause this script to be verbose. (for debugging)") {
        isVerbose = true
}

# このスクリプトの起動時に -x もしくは --xml が与えられているのなら、
# <?xml version='1.0' encoding='UTF-8'?> や、root エレメントなども出力する。
opt.on("-x", "--xml", "Generate refined XML document. That includes decl and root element. Default is didn't. (Default is only outputting <status></status> element.)") {
        isPrint_XML_decl = true
}

# このスクリプトの起動時に -r もしくは --rt が与えられているのなら、
# このユーザが公式 RT した tweet も取得する。
opt.on("-r", "--rt", "Get ReTweet. (That is, the output can include other users tweet.) Default is didn't.") {
        include_rts = 1
}

# このスクリプトの起動時に -l もしくは --last が与えられているのなら、
# 最新の20件のみを取得する。
# ただし同時に、何ページ目から取得するかも指定していた場合は、
# その starting page number で指定された20件を取得する。
opt.on("-l", "--last", "Retrieve only latest 20 tweets. But if you specify starting-page-number by command line, then retrieve the page number's 20 tweets.") {
        isLatest20thStatuses = true
}

# このスクリプトの起動時に -s もしくは --slow が与えられているのなら、
# ループ内で、WATING_TIME 秒 (30秒ほど) 休息しつつ、処理を行う。
# Twitter の API は、1時間に150回までのアクセスしか受け付けていないため。
opt.on("-s", "--slow", "Cause this script to be slow, actually to wait #{WATING_TIME} seconds in every time in each loop. This option is for avoiding the restricion of Twitter's API. Twitter's unauthenticated API calls are permitted 150 requests per hour.") {
	isSlowing = true
}

#"Cause this script to be slow, actually to wait 60 seconds in every time in each loop. There are some restrictions on Twitter's API. Unauthenticated API calls are permitted 150 requests per hour. This option is for avoiding this restriction."

opt.parse!(ARGV)

if ARGV[0] == nil then
        puts "Username is not specified. Pleas, enter username. (Username is Twitter's screen name.)"
        puts USAGE_TEXT
        ExitStatus = 0
        exit(ExitStatus)
else
        username = ARGV[0]
end

if ARGV[1] != nil then
        begin
                page = Integer(ARGV[1])
        rescue
                puts "Maybe, this second argument is not integer number. Please, enter page's number as a positive integer."
                puts USAGE_TEXT
                ExitStatus = 0
                exit(ExitStatus)
        end
end

if isVerbose then
        puts "include_rts is #{include_rts}"
        puts "stating page is #{page}"
        puts "username is #{username}"
end


doc = REXML::Document.new
doc << REXML::XMLDecl.new('1.0', 'UTF-8')
statuses = doc.add_element("statuses")

if isPrint_XML_decl then
        puts doc.xml_decl
        puts "<statuses>"
end

tweets = Twitter.user_timeline(username, { "include_rts"=>include_rts, "page"=>page} )

until tweets.empty?

        tweets.each { |tweet|
                statusElement = statuses.add_element("status")
                nestHashData_to_nestElementsOfXML( tweet.attrs, statusElement )
                puts statusElement
                statusNumber = statusNumber + 1
        }

        page = page + 1

        if isLatest20thStatuses then
                break
        end

	if isSlowing then
		sleep(WATING_TIME)
	end

        tweets = Twitter.user_timeline(username, { "include_rts"=>include_rts, "page"=>page} )
end

if isPrint_XML_decl then
        puts "</statuses>"
end

if isVerbose then
        puts "Last page is #{page-1}"
        puts "#{statusNumber} statuses is printed. (statusNumber is #{statusNumber})"
end

*1:[http://d.hatena.ne.jp/r_coppelia/20111203/1322853481:title]