twitter のサーバから、指定したユーザの過去ログを取得し、XML っぽいものに変換する Ruby スクリプト。
まずはじめに RubyGems twitter のインストール。
先週、FreeBSD の ports に何か面白そうなものがないかなと、
> ls -d /usr/ports/*/*[Tt]witter*
とか、
> ls -d /usr/ports/*/*[Tt]weet*
などと入力して遊んでいたら、/usr/ports/net/rubygem-twitter/ というものを見つけた。Ruby による twitter API のラッパーのよう。
https://github.com/jnunemaker/twitter
とりあえず、せっかくインストールしてみたので、これを用いて、指定した user の過去ログを取得して、xml に整形して、標準出力に出力するスクリプトを書いてみた。
私は ruby には不慣れなので、とくに xml 関連の処理の部分などは、なんか変なことをやっているような気がしないでもない。
私は、インストールは FreeBSD の ports を用いたけれど、そうでない場合は、root とかになって、
> gem install twitter
とでも入力すれば、たぶん良いんじゃないかなと思う。
で、以下が、その RubyGems twitter を用いて作ってみたログ保存用のスクリプト。
#!/usr/local/bin/ruby require "rubygems" require "twitter" require "rexml/document" require "optparse" # 指定したユーザの最大3200件の statuses を取得し、 # xml に整形して標準出力に出力するスクリプト。 # 実行には、RubyGem twitter が必要。 # # 取得した statuses の key と value は、xml の要素に変換している。 # そして、1件の status を一行にまとめて出力している。 # つまり1000件の発言があるユーザを指定すれば、 # 全1000行からなるテキスト・データが出力されることになる。 # (はずであったが、改行を含む tweet を取得した場合には、 # その出力されるテキスト・データの行数も多くなる。) # # このスクリプトで使用している Twitter の API には制約がある。 # その制約のために、このスクリプトでは、 # 20 件 * 160 = 3200 件分の statuses しか取得できない。 # この制約は、将来、また変更されるかもしれない。 # (とくに根拠はないが、もし変更されるとすれば、おそらく悪いほうに。) # 入れ子状のハッシュデータを、再帰呼び出しによって 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.rb [-r] [-x] [-l] [-s] [-h] [-v] username [starting page number : Integer]" Version = "version: 3" 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 } opt.parse!(ARGV) if ARGV[0] == nil then puts "Username is not specified. Please, 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, 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
で、このスクリプトの使用方法。
RubyGem Twitter をインストールしたら、上のスクリプトをコピー・ペーストして*1、たとえば wholeLogSaver.rb とか、好きな名前をつけて保存し、chmod とかで実行権限を付与。
あとは、ログを取得したいユーザの名前を第1引数に与えて、以下のように実行してやれば、そのユーザのログを延々と twitter のサーバから取得しては、それを延々と標準出力に吐き出し続ける。
> ./wholeLogSaver.rb username
ただし、twitter のサーバ側には制限があって、3200件より以前のログは、返してくれない。なので、3201件以上 tweet しているユーザのログは、全部取得することはできない。かなしいですね。
いずれにしても、出力はかなり膨大になるので、リダイレクトとかして、ファイルに保存するなりしたほうが良いと思う。というか xml にしないほうが良かったような気もしないでもない。
そんなに膨大なログとか欲しくない場合は、以下のように -l というオプションも一緒に指定しておけば、とりあえず最新の20件のみを取得し、その20件だけを出力する。なので、はじめにいろいろ試してみる場合や、使い方を忘れた場合は、とりあえずこのオプションをつけて使ったほうが良いかもしれない。
> ./wholeLogSaver.rb -l username
また、第2引数に、数字を入力すれば、最新の投稿から数えて、その数字をだいたい20倍した件目からの tweet を延々と取得する。たとえば以下の例では、5 を指定しているので、20 * 5 -19 = 81 となるので、最新から数えて81件目からの tweets を延々と取得して、出力し続ける。
> ./wholeLogSaver.rb username 5
この場合も、先ほどの -l オプションを併用することができる。併用すれば、81件目〜100件目までの20件のみを表示する。
あと、そのユーザが公式 RT した他人の tweets は、デフォルトでは取得しないようにしている。それもあわせて取得したい場合は、下記のように -r オプションを指定して実行。
> ./wholeLogSaver.rb -r username
それとあと、このスクリプトは、完全な XML ファイルを出力してはいない。もともとこのスクリプトの目的が、取得したログの件数と、保存したログファイルの行数を、きっかり同じにそろえたかったので、私にとって余分な xml の宣言とか、ルートの要素とかは、デフォルトでは出力していない*2。完全な xml ファイルを出力したい場合は、-x オプションを指定すれば良いはず。
> ./wholeLogSaver.rb -x username
他に、-v というオプションを渡せば、スクリプト内部の引数に、どんな値が入力されたかとか、何件分の tweet を xml に整形して出力したかとかも、標準出力にいっしょに出力するけれど、これは私がデバッグ中にとりあえず使っていたオプションなので、削除しても良かったかもしれない。当然のように -x オプションといっしょに指定すると、その出力される xml ファイルは、わけのわからないものになっちゃうので、併用しないほうが絶対に良いね。
> ./wholeLogSaver.rb -v -x username
諸注意。
あまり短時間に大量に API を呼び出し続けていると、twitter のサーバ側から、アクセスを半永久的に拒否される可能性もあると思う。なので上記のスクリプトも、実際に用いる場合は、繰り返し処理の部分に、待ち時間をはさみこんだほうが良いと思う。一応、上記のスクリプトで使用している API は、ユーザ認証不要のものではあるけれど。
追記
実際にこのスクリプトを実行してみてわかったけれど、Twitter の 認証不要の API は、ひとつのグローバル IP アドレスに対して、1時間あたり150件までしかリクエストを受け付けてくれないそう*3。
なので、3000件以上のログのあるユーザのアカウントに対して実行すると、途中でエラーが返ってきてスクリプトが中途半端なところで終了してしまうので、二回に分けて実行するか、あるいは API の呼び出しが百十数回を越える場合は、途中で sleep などの処理を挟んで、だんだんと遅くしていくか、あるいは、エラーが返ってきた場合には、一度、休息して、その後、自動的に処理を再開するなどするように書き直したほうが良さそう。
とりあえず上記のスクリプトには、-s というオプションを追加しておいた。このオプションを指定すれば、ループのたびごとに、30秒間の休息を取る。なので全3200件のログを取得するには、約80分ほどの時間がかかることになる。
-s オプションの使用例
> ./wholeLogSaver.rb -s username
*1:このスクリプトの一行目の #!/usr/local/bin/ruby の部分は、書き換える必要があるかもしれない。私のマシンでは、ruby は /usr/local/bin/ruby にインストールされているので、そのように書いたけれど、OS などによってインストール場所は異なるので、 which ruby とかシェルから入力してみて、たしかめておくと良いと思う。
*2:なお、実際には取得した tweet に改行文字が含まれる場合もあるので、その保存されるログファイルの行数と、ログの件数は一致しない場合がある。もうがっかり。
*3: https://dev.twitter.com/docs/rate-limiting より引用。 "Unauthenticated calls are permitted 150 requests per hour. Unauthenticated calls are measured against the public facing IP of the server or device making the request." (訳: 非認証の API 呼び出しは、1時間あたり150回まで許されている。その非認証呼び出しの回数は、グローバルな IP アドレスか、もしくはその API を呼び出したデバイスごとに、計測される。)