Rubyで書くはてブを使った推薦

書籍の例では,del.icio.usからソーシャルブックマークのデータを取得して推薦を行っています.(p.20-p.24を参照)
これをこのままRubyで書こうかな...とも思ったのですが,何か面白くないので
はてなブックマーク(以下 はてブ)のデータを取ってくることにします.

はてブのデータを取得する

はてブのデータはRSSで配信されているので,全てRSS経由でデータを取ってくることにします.
RSSライブラリを使えばパースも楽ですし.
なお,任意のID(下の例ではkj-ki)のブックマークデータも強制的にマージできるようにしました.
これで被推薦者を決め打ちできます.

require 'rss'

module My
  class HatenaBookmark
    def initialize(tag, how_many, options = {})
      @hot_entries = "http://b.hatena.ne.jp/t/#{URI.escape(tag)}?mode=rss&sort=hot"
      @how_many = how_many
      @my_hatena_id = options[:my_hatena_id]
    end

    #
    # mainループ
    # ホットエントリ取得からID取得、BookmarkされているURL取得まで一気に行う
    #
    def make_users_and_bookmarks
      hot_urls = get_hot_urls
      users = get_all_bookmarked_users(hot_urls)
      # 指定したブックマークデータも収集対象にする
      users << @my_hatena_id if @my_hatena_id

      users_and_bookmarks = {}
      users.each do |user|
        bookmarked_urls = get_bookmarked_urls(user)
        users_and_bookmarks[user] = critics_of(bookmarked_urls)
      end
      pad_all_urls_into_all_users(users_and_bookmarks)
    end

    #
    # ホットエントリのURLを抽出する
    #
    def get_hot_urls
      rss = RSS::Parser.parse(@hot_entries)
      rss.items.map { |item| item.link }.slice(0...@how_many)
    end

    #
    # 複数URLでブックマークしているIDを全て抽出する
    #
    def get_all_bookmarked_users(urls)
      urls.map { |url| get_bookmarked_users(url) }.flatten.uniq
    end

    #
    # エントリをブックマークしているIDを抽出する
    #
    def get_bookmarked_users(url)
      entry = "http://b.hatena.ne.jp/entry/rss/#{url}"
      # falseを入れないとパース時にエラーになってしまう
      rss = RSS::Parser.parse(entry, false)
      rss.items.map { |item| item.title }
    end

    #
    # 任意のIDがブックマークしているURLを抽出する
    # ページング処理を加味して最大150件まで
    #
    def get_bookmarked_urls(user)
      offsets = [0, 30, 60, 90, 120]
      offsets.map { |offset| get_offset_bookmarked_urls(user, offset) }.flatten.compact
    end

    #
    # 指定したオフセットのブックマークしているURLを抽出する
    # RSSがWellFormedでない場合があるので、その時はnilを返す
    # offsetが大きすぎるとMissingTagErrorになるので、その時もnilを返す
    #
    def get_offset_bookmarked_urls(user, offset)
      bookmark = "http://b.hatena.ne.jp/#{user}/rss?of=#{offset}"
      begin
        rss = RSS::Parser.parse(bookmark)
      rescue RSS::NotWellFormedError, RSS::MissingTagError
        return nil
      end
      rss.items.map { |item| item.link }
    end

    #
    # URLのリストを評価値1.0としたハッシュに変換する
    #
    def critics_of(urls)
      url_and_critic = urls.map { |url| [url, 1.0] }.flatten
      Hash[*url_and_critic]
    end

    #
    # 各IDに対して、ブックマークしていないURLを0点で登録する
    #
    def pad_all_urls_into_all_users(users_and_bookmarks)
      users = users_and_bookmarks.keys
      all_urls = users.map { |user| users_and_bookmarks[user].keys }.flatten.uniq

      users.each do |user|
        (all_urls - users_and_bookmarks[user].keys).each do |url|
          users_and_bookmarks[user][url] = 0.0
        end
      end
      users_and_bookmarks
    end
  end
end

実行結果

今回は「はてな」タグが付加されているエントリを10個選んで,少なくともどれか1つをブックマークしているユーザのデータを取ってきます.
後で使いやすいように,一旦ファイルに落とします.

hatebu = My::HatenaBookmark.new('はてな', 10, { :my_hatena_id => 'kj-ki' })

File.open('hatena.dump', 'w') do |file|
  Marshal.dump(hatebu.make_users_and_bookmarks, file)
end

いざ,実行!

% ./hatena_bookmark.rb
% ls -la hatena.dump
-rw-r--r--   1 user  staff  37013296  8 26 00:12 hatena.dump

時間は掛かりますが,そこそこの大きさのファイル(35MBくらい)ができました.

推薦してもらいましょう

では,このデータを使って推薦させてみましょう.

require 'fast_recommender'
hatena_critics = Marshal.load(File.open('hatena.dump'))
recommender = My::FastRecommender.new
puts '----------top_matches'
pp recommender.top_matches(critics, 'kj-ki', { :how_many => 10 })
puts '----------get_recommendations'
pp recommender.get_recommendations(critics, 'kj-ki').slice(0...10)

いざ,実行!

% ./hatena_bookmark.rb
----------top_matches
[[0.0201632277399705, "uva"],
 [0.020112244545605, "a_tsu_shi"],
 [0.0121706163873039, "Kirito"],
 [0.0116497107279458, "zackle"],
 [0.0116497107279458, "yorihito_tanaka"],
 [0.0116497107279458, "rinou"],
 [0.0116497107279458, "juniper"],
 [0.0116497107279458, "fk_2000"],
 [0.00470556061444605, "kisiritooru"],
 [0.00337173183496513, "lamich"]]
----------get_recommendations
[["http://d.hatena.ne.jp/shibata616/20080825/1219627264", 0.455297428020195],
 ["http://q.hatena.ne.jp/tanakahideo/questionlist?page=1", 0.371765774979091],
 ["http://news.livedoor.com/article/detail/3790598/", 0.332950525317097],
 ["http://www.ibm.com/developerworks/jp/linux/library/l-10sysadtips/?ca=drs-jp", 0.262391110294427],
 ["http://www.net.c.dendai.ac.jp/~takumi/", 0.24428021683129],
 ["http://d.hatena.ne.jp/KZR/20080808/p1", 0.196793332720821],
 ["http://d.hatena.ne.jp/asami81/20080821/masuda", 0.189161546206615],
 ["http://www.itmedia.co.jp/news/articles/0808/19/news051.html", 0.179899305187593],
 ["http://itpro.nikkeibp.co.jp/article/COLUMN/20080417/299353/", 0.178682439257683],
 ["http://www.j-cast.com/tv/2008/08/19025331.html", 0.169956516751988]]

という訳で,get_recommendationsがkj-kiに推薦してくれたURL第1位は,


増田さんへ。久しぶりにグレーターみのもんたが現れました - ls@usada’s Backyard」でした!


これだけやってみのもんた...