cxxの日記

2008-05-24

[]Dashboardの制限が本当に300人か確かめた 17:07 はてなブックマーク - Dashboardの制限が本当に300人か確かめた - cxxの日記

今は400人までで、順次増やすそうです。

かなり行儀が悪いですが下のスクリプトで1人ずつDashboardに現れるかどうか確かめました*1。最新postがDashboardに存在すれば"found"、存在しなければ"not found"を出力します。

#!/usr/bin/ruby
require 'rubygems'
require 'mechanize'

email = ''
password = ''

class User
  attr_reader :name, :uri, :last_update
  def initialize(doc)
    a = doc.at('div[@class="username"]/a')
    @name = a.inner_text
    @uri = a.attributes['href']
    @last_update = doc.at('div[@class="last_update"]').inner_text.strip
  end
end

agent = WWW::Mechanize.new
doc = agent.get('http://www.tumblr.com/login')
form = doc.forms.first
form.email = email
form.password = password
agent.submit(form)

doc = agent.get('http://www.tumblr.com/following')
following = doc.search('#following/li').collect {|li| User.new(li) }
following.each do |user|
  begin
    print "#{user.name}: "
    doc = agent.get("#{user.uri}api/read?num=1")
    if doc.body =~ /post id="(\d+)"/
      latest = $1.to_i
      doc = agent.get("http://www.tumblr.com/dashboard/1/#{latest+1}")
      if doc.at("\#post#{latest}")
        puts "found"
      else
        puts "not found"
      end
    else
      puts "no post"
    end
  rescue WWW::Mechanize::ResponseCodeError
    puts "error #{$!.response_code}"
  end
end

結果

(前略)
yokochie: found
fuba: error 404
shokai: not found
taroudo: not found
tn: not found
septiblog: not found
ofelia: not found
gatya: not found
vitalsine: not found
010734: not found

308 followingの時点で試したので308-8=300で合ってました*2。followingはなぜか順序が若干入れ替わってることがあるので、間をおいて試せば違う人が漏れると思われます。

*1:実際には途中でソケットの生成に失敗するエラーが出た(一時ポートが足りなくなった?)ので1度だけそこから再開しました

*2:error 404はtumblelog自体がなくて「最新n人」に数えられてるかどうか確かめられないので、299の可能性もなくはない

2008-05-22

[]Tumblrでfollowしているユーザを最終更新順に表示するRubyスクリプト 02:09 はてなブックマーク - Tumblrでfollowしているユーザを最終更新順に表示するRubyスクリプト - cxxの日記

もう動きません

どうもDashboardには最近followした順に300人までのpostしか流れてこないらしい(厳密に調べたわけでないので断言できませんが)ので、followingを最新更新日時順に並べて表示するのと、404になっていないか調べるためのスクリプトをRubyで書きました。HTMLで出力するなり、リストを受け取ってunfollowするスクリプトを用意するなりしないと意味ないような気もしますが、もう寝ます。

起きた後で気づきましたが、多分Greasemonkeyでやってページの要素を直接並べ替えて表示した方がいいし、生存確認で本文取る必要もないですね。

#!/usr/bin/ruby
require 'rubygems'
require 'mechanize'

email = ''
password = ''

class User
  attr_reader :name, :uri, :last_update
  def initialize(doc)
    a = doc.at('div[@class="username"]/a')
    @name = a.inner_text
    @uri = a.attributes['href']
    @last_update = doc.at('div[@class="last_update"]').inner_text.strip
  end
  def last_update_in_sec
    case last_update
    when /(\d*) year/
      $1.to_i * 365 * 24 * 60 * 60
    when /(\d*) month/
      $1.to_i * 30 * 24 * 60 * 60
    when /(\d*) week/
      $1.to_i * 7 * 24 * 60 * 60
    when /(\d*) day/
      $1.to_i * 24 * 60 * 60
    when /(\d*) hour/
      $1.to_i * 60 * 60
    when /(\d*) minute/
      $1.to_i * 60
    when /(\d*) second/
      $1.to_i
    else
      1.0 / 0.0
    end
  end
end

mech = WWW::Mechanize.new
doc = mech.get('http://www.tumblr.com/login')
form = doc.forms.first
form.email = email
form.password = password
mech.submit(form)

doc = mech.get('http://www.tumblr.com/following')
following = doc.search('#following/li').collect {|li| User.new(li) }
following = following.sort_by {|user| user.last_update_in_sec }
following.each do |user|
  puts "#{user.name}: #{user.last_update}"
end
following.each do |user|
  begin
    mech.get(user.uri)
  rescue WWW::Mechanize::ResponseCodeError
    puts "#{user.name}: error #{$!.response_code}"
  end
end

2008-05-17

[]Tumblrのpostにタグ付けするRubyスクリプト 05:55 はてなブックマーク - Tumblrのpostにタグ付けするRubyスクリプト - cxxの日記

現在は使えません。

post内容に一部の文字(「”」、「β」など)が含まれている場合に、その文字を「?」に書き変えてしまうバグが見つかりました。このスクリプトは使用しないでください。→修正しました。Hpricotは&、"、<、>以外の実体参照を皆殺しにするみたいです。直ってませんでした。→修正しました。正規表現を複数行モードにするのを忘れていた箇所がありました。

http://cxx.tumblr.com/tagged/quote
のようなURIで特定のタグが付いたpostにアクセスできるようになったので、とりあえずpost typeとfirst post / reblogだけタグ付けするためのスクリプトをRubyで書きました(Ruby 1.8.6 + WWW::Mechanize 0.7.6)。Rubyは詳しくないのでUTF-8じゃない環境で動くのかどうか分かってないです(追記:素のRuby 1.8.6-p114-i386-mswin32での動作を確認しました。日本語のタグを付けたければなんかしないといけないと思います。)。1 post当たり14秒ぐらいかかってるのですが、俺のTumblrは約10000 postsなので全部完了するのに2日ほどかかります(今も走らせてる)。アホか。

途中で中断してしまった場合でも

page = dashboard.filter('everything', 'me')
page = dashboard.get('http://www.tumblr.com/show/everything/by/me/100')
などと置き換えれば途中から再開できます。タグ付けの最中に通常通りTumblrを使用しても問題ありません。ただし、他のブラウザでのログイン操作を合計5回以上行なうと強制的にログアウトされると思います。このスクリプトを使用して何か問題が起きても責任は取れません。

2008-05-19

時間帯によっては1件当たり3〜5秒と、比較的実用的な速度で動いています。現在のコードは定期実行に適さず、あんまり意味がないのでもう少し書き直します。

2008-05-20

定期実行ができるように、既にタグを追加済みのpostに到達した時点で終了するようにしました。途中から再開する場合には

ruby tagging.rb [post ID]
として起動してください。最初のpostに限ってはタグが追加済みでも終了しません。深いページから再開する場合には
page = dashboard.filter('everything', 'me')
page = dashboard.get('http://www.tumblr.com/show/everything/by/me/100')
などに置き換えてください(書き換えた場合でも[post ID]を忘れるとすぐに終了してしまいます)。

2008-05-20

Post.idというメソッド名はまずかったのでPost.post_idにしました。

#!/usr/bin/ruby
require 'rubygems'
require 'mechanize'

email = ''
password = ''

module Tumblr
  class Dashboard
    PREFIX = 'http://www.tumblr.com'

    def initialize(email, password)
      @mech = WWW::Mechanize.new
      login(email, password)
    end

    def login(email, password)
      @emall = email
      @password = password
      doc = @mech.get('http://www.tumblr.com/login')
      form = doc.forms.first
      form.email = email
      form.password = password
      @mech.submit(form)
    end

    def logout
      @mech.get('http://www.tumblr.com/logout')
    end

    def get(uri)
      doc = @mech.get(uri)
      Page.new(doc, @mech)
    end

    def page(pos=1)
      get("http://www.tumblr.com/dashboard/#{pos}")
    end

    def filter(type='everything', author='everyone', pos=1)
      get("http://www.tumblr.com/show/#{type}/by/#{author}/#{pos}")
    end

    class Page
      attr_reader :posts

      def initialize(doc, mech)
        @doc = doc
        @mech = mech
        @posts = doc.search('#posts/li[@id]').collect do |post|
          Post.new(post, mech)
        end
      end

      def older
        elem = @doc.at('#pagination/a[@title="Go to older posts"]')
        elem ? Page.new(@mech.get(elem.attributes['href']), @mech) : nil
      end

      def newer
        elem = @doc.at('#pagination/a[@title="Go to older posts"]')
        elem ? Page.new(@mech.get(elem.attributes['href']), @mech) : nil
      end

      def older_link
        elem = @doc.at('#pagination/a[@title="Go to older posts"]')
        elem ? PREFIX + elem.attributes['href'] : nil
      end

      def newer_link
        elem = @doc.at('#pagination/a[@title="Go to newer posts"]')
        elem ? PREFIX + elem.attributes['href'] : nil
      end
    end

    class Post
      def initialize(elem, mech)
        @elem = elem
        @mech = mech
      end

      def post_id
        @elem.attributes['id'] =~ /post(\d*)/
        $1.to_i
      end

      def post_type
        @elem.attributes['class'] =~ /regular|photo|quote|link|conversation|video|audio/
        case $&
        when 'regular': 'text'
        when 'conversation': 'chat'
        else $&
        end
      end

      def is_reblog?
        (@elem.attributes['class'] =~ /is_reblog/) ? true : false
      end

      def edit
        elem = @elem.at('//a[@class="edit_link"]')
        elem ? EditForm.new(@mech.get(elem.attributes['href']), @mech) : nil
      end

      def edit_link
        elem = @elem.at('//a[@class="edit_link"]')
        if elem
          elem.attributes['href'] =~ /\?/
          PREFIX + $`
        else
          nil
        end
      end
    end

    class EditForm
      def initialize(doc, mech)
        @doc = doc
        @mech = mech
        @form = doc.forms.first
      end

      def submit
        @mech.submit(@form)
      end

      def tags
        csv = @form['post[tags]']
        (csv == 'comma, separated, tags') ? [] : csv.split(/\s*,\s*/)
      end

      def tags=(new_tags)
        @form['post[tags]'] = new_tags.join(', ')
      end
    end
  end
end

module Hpricot
  def Hpricot.uxs(str)
    str.to_s.
      gsub(/&(\w+);/) { [NamedCharacters[$1]].pack('U') }.
      gsub(/&#(\d+);/) { [$1.to_i].pack('U') }
  end
end

dashboard = Tumblr::Dashboard.new(email, password)
page = dashboard.filter('everything', 'me')
last = page.posts.first.post_id + 1
first = ARGV[0] ? ARGV[0].to_i : page.posts.first.post_id

begin
  loop do
    page.posts.each do |post|
      next if post.post_id > first or post.post_id >= last
      last = post.post_id
      new_tags = [post.post_type, post.is_reblog? ? 'reblog' : 'first post']
      edit = post.edit
      sum = edit.tags | new_tags
      if sum == edit.tags
        if last == first
          next
        else
          exit
        end
      end
      edit.tags = sum
      edit.submit
    end
    break unless page.older_link
    puts page.older_link
    page = page.older
  end
rescue
  $stderr.puts "exception at post id #{last}: type 'ruby tagging.rb #{last}' to resume"
end