TwitterAPIからXMLを取得してMakeYourDayを動かせるようにするまで

03月18日修正:写ツの発言を取得するように修正しました。
03月23日追記:写ツのその日一個目の発言がうまく投稿されません。メール送信までは正常なので、はてなグループの問題?
05月31日追記日本語を文字コードの参照で投稿しているため(はてなグループ内で)うまく検索できません。時間があれば改善します。アドバイスがあればよろしくお願いします。
2009-07-20修正:上の問題を解決しました。また、LoudTwitterが停止したようです。twitter2Blog 何ができるの? つぶやきをブログへ - twitter2Blog を つくるぞーを参照ください。


いつのまにかTwitterのHTMLからタイムスタンプがなくなっていたのでMakeYourDayスクリプトが動かなくなりました。APIしかないなあという感じなので修正。個人のログ収集ならAPI制限も問題ないでしょう。
以下は個人的なメモです。修正版は最後に載せました。ちゃんと動くかはわかりません。
惰性で修正しているだけなので単にログを取りたいならLoudTwitterでも使うのがよいです。twitter2Blog(試験中)

さくらインターネット

関係ないですが、さくらインターネットTelnetが使えなくなったのでサポートの内容に従ってSSHを試してみました。だいたい同じですね。
スクリプトをcronで動かす方法についてはさくらインターネットのCRON設定を可能な限り簡単に解説したいがわかりやすいです。具体的にはコマンド「/usr/local/bin/ruby /home/アカウント/www/スクリプト.rb」を「3」時「0」分に実行します。

TwitterAPI

今回は自分のログを収集することが目的なので「user_timeline」APIを使います。
http://twitter.com/statuses/user_timeline/アカウント.xml?page=ページ数」を辿っていきます。
参考:Twitter API 仕様書 (日本語) [最新版]
以下のようなXMLが返ってきます。必要であればReply先の情報なども取得できますね。

<statuses type="array">
- <status>
  <created_at>Sun Mar 16 10:10:10 +0000 2009</created_at> 
  <id>0000000</id> 
  <text>こんにちは。</text> 
  </status>
</statuses>

BASIC認証

TwitterAPIの利用にはBASIC認証が必要です。ID(メールアドレス)&パスワードとのことですが、メールアドレスでなくTwitterIDでも通るような?
よくわかりませんが、以下のようにXMLを(正規表現を使うので)文字列として取得しました。リクエストのあたりとか、これでいいのかなあ。
参考:Rubyist Magazine - 標準添付ライブラリ紹介 【第 7 回】 net/http

req = Net::HTTP::Get.new('/statuses/user_timeline/'+id+'.xml?page='+p.to_s)
req.basic_auth("kiwofusi", "パスワード><")
xml = Net::HTTP.start("twitter.com").request(req)
body = xml.body

データの取得

本来ならXMLをパースするのが自然でしょうが、ソースをあまりいじらくて済むためパターンマッチを使いました。「\w+ (\w+) (\d\d) (\d\d):(\d\d):\d\d \+\d\d\d\d (\d\d\d\d)<\/created_at>\s*(\d+)<\/id>\s*(.*)<\/text>」から日時、ステータスID、発言を取り出します。あとはMakeYourDay任せ。
日時の取り方はもっとスマートにできそう。日時に関するオブジェクトの扱いに慣れず戸惑った。UNIX時間をいろんなかたちで表現できるもの、と考えるとちょっとわかる。
参考:正規表現 - RubyリファレンスマニュアルTime - Rubyリファレンスマニュアル

修正版ソース

2009-07-20版。
問題:116文字を超える発言の投稿がおかしくなる。はてなグループ側の問題? 手作業で修正できるレベル。数値文字参照のデコードによって解決されたかも。
問題2:写ツの発言「ほげほげ http://f.hatena〜」を取得できない。過去のMakeYourDayでも同様?改行を含む発言に正規表現で対応しました。ただし3行(改行2箇所)以上の取得は未対応です。

require 'net/smtp'
require 'net/http'
require 'nkf'
require 'time'
require 'kconv'

# 原作 id:worris2
# http://d.hatena.ne.jp/worris2/20081012/1223828088

# 改変 id:kiwofusi
# http://d.hatena.ne.jp/kiwofusi/20090316/1237147695
# 更新履歴
# 2009-07-20 数値文字参照のデコードに対応
# 2009-03-18 写ツの投稿に対応
# 2009-03-16 公開
# 参考
# Rubyで数値文字参照をUTF-8にする - くふんを狙え
# http://d.hatena.ne.jp/eclipse-a/20070905/1189001865

mailserver="さくらID.sakura.ne.jp"
maildomain="さくらID.sakura.ne.jp"
mailid="さくらメールID@さくらID.sakura.ne.jp"
mailpass="さくらメールIDパスワード"
mail="さくらメールID@さくらID.sakura.ne.jp"
backupmail="バックアップ送信用メールアドレス"

hatenad={"はてなID" => "はてなダイアリー投稿用アドレス"}	#"@d.hatena.ne.jp"と".g.hatena.ne.jp"は省く
twitter={"はてなID" => "ツイッターID"}
twittertitle={"はてなID" => "はてなダイアリー投稿時のタイトル"}	#"[ライフログ]今日のツイッター"とか

t=Time.now-60*60*24
$yesterday=Time::mktime(t.year,t.month,t.day,3,0,0)		#午前3時から現在までのログを投稿

### スクレイピング

def fetch(p,id)
  rt=0
  begin
    req = Net::HTTP::Get.new('/statuses/user_timeline/'+id+'.xml?page='+p.to_s)
    req.basic_auth("【ID・メールアドレス】", "【パスワード】")
    xml = Net::HTTP.start("twitter.com").request(req)
    body = xml.body
  rescue Exception => err
    sleep 10
    rt+=1
    retry if rt<3
    $stderr.print err,":",id,"\n"
    $yesterday
  else

    $stderr.print "404:That page doesn't exist!:",id,"\n" if /That page doesn't exist!/=~body

    body.split(/<\/status>/).each{|line|
      if match = /<created_at>\w+ (\w+) (\d\d) (\d\d):(\d\d):\d\d \+\d\d\d\d (\d\d\d\d)<\/created_at>\s*<id>(\d+)<\/id>\s*<text>(.*\s*.*)<\/text>/.match(line)
        $comment << match[7].gsub(/&#(\d+?);/) { [$1.to_i].pack('U') } # 数値文字参照をutf-8にデコードする
        $link << match[6]
        $date << Time::mktime(match[5].to_i,Time.parse(match[1]).month.to_i,match[2].to_i,match[3].to_i,match[4].to_i)+60*60*9 # 60*60*9→日本時間に換算
      end
    }
    $date[-1] ? $date[-1] : $yesterday
  end
end

### メイン

hatenad.each{|user,mailto|

  next if mailto.to_s=="" || twitter[user].to_s==""

  $mailout=""
  page=1
  $last=""
  $comment=[]
  $link=[]
  $date=[]

  while page<11 && fetch(page,twitter[user])>$yesterday 	#11ページから先のpostは破棄
    page+=1
  end
  $comment.each_with_index{|c,i|
    mailbody=""
    if $date[i]>$yesterday
      mailbody << "[http://twitter.com/"+twitter[user]+"/statuses/"+$link[i]+":title="+("0"+$date[i].hour.to_s).slice(-2,2)+":"+("0"+$date[i].min.to_s).slice(-2,2)+"] "
      mailbody << c.gsub(/<a href="([^\/][^"]+)"[^>]*>[^<]*<\/a>/,'\1').gsub(/<[^>]*>/,'').gsub("&gt;",">").gsub("&lt;","<").gsub("&quot;",'"').gsub("&amp;","&")
      mailbody << "\n"
    end
    $mailout=mailbody+$mailout
  }

### メール送信

  if $mailout!=""
    retry_count=0
    begin
      address=mailto+'@d.hatena.ne.jp'
      address=mailto+'.g.hatena.ne.jp' if mailto=~/@/
      Net::SMTP.start( mailserver, 25, maildomain, mailid, mailpass, :login ) {|smtp|
        smtp.send_mail <<EOM, mail, address, backupmail
From: #{mail}
To: #{address}
Subject: =?iso-2022-jp?B?#{NKF.nkf('-EjMB',twittertitle[user])}?=
Content-Transfer-Encoding: 7bit
Content-Type: Text/Plain; charset=iso-2022-jp

#{NKF.nkf('-Wj',$mailout)}
EOM
      }								#↑はてなダイアリー投稿時のタイトルがEUCなので'-EjMB'
    rescue => err
      $stderr.print user,":",address,":",err,"\n"
      retry_count+=1
      if retry_count<2
        sleep 10
        retry
      end
    end
  end
}