4 / 26

2015

RubyのMechanizeとNokogiriで読書メーターをスクレイピング

Rubyで読書メーターをスクレイピングしたい

読書系SNSの読書メーターを利用しています。

読書メーター - あなたの読書量をグラフで管理

ユーザー間のコミュニケーションも活発で、お気に入り・お気に入られ(Twitterでいうフォロー・フォロワーの関係)のユーザーとどのくらい同じ本を読んでいるか(このことを共読と呼んでいます)、なんて話題で盛り上がったりしてます。

ユーザー毎のマイページでそういう情報は確認できますが、数字は日々変化しますし、何十人、何百人といると楽な方法が欲しくなります。

あるお気に入りユーザーの方がPerlで共読数などを一括取得するスクリプトを書かれていて、以前それをいただいたんですが、自分の環境ではうまく動かなかったのでどうせなら自分が勉強中のRubyで同じものを書いちまえ、ということで書いてみました。

共読解析ツールのソースコード

スクリプトのソースコードはGistにアップロードしましたので、そちらをご確認ください。

読書メーターにログイン状態でアクセスし、お気に入りユーザーの読書状況を一括取得するRubyスクリプト

使い方

使い方、またプログラム作成過程でどういう処理をしたのか、解説していきます。

前提条件

  • 読書メーターユーザーであること

基本、使用者の代わりにログイン状態で読書メーターにアクセスし、欲しい情報を抜き取るというものです。ユーザーのメールアドレスとパスワードがないと使えません。あしからず。

事前準備(1) Rubyインタープリタのインストール

お手持ちのPCにRubyインタープリタ(Rubyプログラムを解釈・実行するソフトウェア)がインストールされていることが前提です。

まだ入っていない方は、各自の環境に合わせてインストールしてください。

Windowsの場合

Windowsマシンの方はRubyインストーラーが一番てっとり早いと思います。

Rubyダウンロード及びインストール

RubyInstaller | Downloads

上記などを参照にして、RubyInstallerからrubyをインストールしてください。 RubyInstallerは基本的に最新のものを選んでおけば問題ないです。インストーラーの表示が英語ですがガイドにしたがってクリックしていけば難しいことはないはずです。 インストール時にRubyの実行が可能なコマンドプロンプトも一緒にインストールするようにチェックを入れて、実行時はそれを起動すると楽です。

Macの場合

最近のMacであれば、最初からrubyは入っていると思います。

ターミナル.appを起動して、

ruby -v

とコマンドを入力して、rubyのバージョンが表示されればそのまま使えます。

もし、 bash: command not found: ruby などと表示されたらrubyは入っていないので、Homebrewやrbenv、rvmなどのツールでインストールしてください。

Linuxの場合

デスクトップ用途でLinuxを使っているような方はここに書くまでもないでしょうから省略。

事前準備(2) NokogiriとMechanizeのインストール

今回のスクリプトはgemと呼ばれる外部ライブラリを使っています。

使用したのは、HTML構文解析ライブラリであるNokogiriと、Webサイトのスクレイピング補助ライブラリであるMechanizeです。

どちらも、Ruby 1.9.3以上のrubyインタープリタがインストールされていれば、gemコマンドでインストールすることができます。

gem install nokogiri
gem install mechanize

しばらく時間がかかるかも知れません。気長にお待ちください。

"successfully installed xxxx." といったメッセージがコマンドラインに表示されればOKです。

事前準備(3)

kyodoku.rb ファイルをテキストエディタで開いて、32行目、38行目、39行目の3箇所の大文字部分を編集します。YOUR_IDはマイページのURLの数字部分、EMAIL_ADDRESSはログイン用のメールアドレス、PASSWORDはログイン用のパスワードに変更してください。

実行

上記の準備が終われば、ダウンロードしたkyodoku.rbファイルのあるフォルダ(ディレクトリ)内で以下のコマンドを入力します。

ruby kyodoku.rb >> sample.txt

スクリプトは自分のお気に入り登録しているユーザーのID、名前、共読数、読んだ冊数、読んでる冊数、積読の冊数、読みたい本の数を文字列として出力します。そのままだとコマンドラインに出力されるだけなので、>> で任意のファイルにリダイレクトします。sample.txtは任意のファイル名でかまいません。ただ同じフォルダにすでにあるファイル名にはしない方がいいです。

プログラムの実行から処理完了まではそれなりに時間がかかります。ここでも焦らずまったりお待ちください。

エラーが表示されず、コマンドプロンプトが表示されれば処理完了です。

あとは保存されたテキストファイルを開けば、お気に入りユーザーの情報が1行ずつずらっと並んでいるはずです。お疲れ様でした。

解説

ここから先はスクリプトの処理の解説をしていきます。

スクリプトが試せればいいやという方はどうぞこのページを閉じてください。

Overview

  • 2-4行目 必要なライブラリ(mechanize,nokogiri,kconv)を読み込む

  • 6-23行目 Hashで初期化可能な構造体クラスUserを定義

  • 25行目 Userクラスのインスタンスを格納するためのハッシュusersを初期化

  • 27-28行目 Mechanizeのインスタンスを変数agentに代入、ユーザーエージェントをSafariに偽装

  • 35-44行目 Mechanizeで読書メーターにログイン

  • 52-86行目 スクリプトのメイン部分。お気に入り一覧のページを1ページ目から最終ページまで繰り返し取得し、Nokogiriで解析して必要な情報を抜き出す

  • 88-90行目 ハッシュusersに格納したユーザー毎に取得した情報を整形して出力する

ハッシュで初期化可能な構造体クラス

複数のユーザー情報をまとめて扱いたいとき、ハッシュをよく使うと思いますが、Rubyには構造体を作れるStructクラスという組み込みクラスがあるので今回はそれを使います。

Structクラスのインスタンスを初期化するとき、属性をハッシュで与えてやると扱いやすいので、ハッシュで初期化するためのモジュールを定義し(6-12行目)、Structクラスに組み込み(14-20行目)ます。

モジュールHashInitializableinitializeメソッドに引数としてハッシュをわたし、その値をキーで参照できるようアクセサを定義しています。

HashInitializableStructクラスは、Struct#newメソッドでインスタンスを作る際に引数に (*arguments)とすることでハッシュを引数に取れるようにし、sendメソッドでモジュールHashInitializableをincludeしています。

あとは、HashInitializable#newでハッシュを引数にとることで任意の属性をもつUserクラスを作成することができます。

# -*- coding: utf-8 -*-
require 'mechanize'
require 'nokogiri'
require 'kconv'

module HashInitializable
  def initialize(attributes={})
    attributes.each do |name,value|
      send("#{name}=",value)
    end
  end
end

class HashInitializableStruct
  def self.new(*arguments)
    struct = Struct.new(*arguments)
    struct.send(:include, HashInitializable)
    struct
  end
end

properties = [ # 実際にはここから
:id,
:name,
:user_url,
:kyodoku_cnt,
:yonda_cnt,
:yonderu_cnt,
:tsundoku_cnt,
:yomitai_cnt
]              # ここまで一行
User = HashInitializableStruct.new(*properties)

Mechanizeでログイン処理

MechanizeはWebサイトとの対話を自動化するためのライブラリです。 ここでは、ログインのためのフォーム入力、ページ遷移しての処理をMechanizeのインスタンスで行っています。

Mechanize#getで引数にとったURLにアクセスします。form_withメソッドでログインアクションにアクセスし、field_withでメールアドレスとパスワードを入力します。

Mechanizeのインスタンスであるagentでログインしてしまえば、あとはgetメソッドでログイン後のページを取得することができます。

agent = Mechanize.new
agent.user_agent = 'Mac Safari'

base_url = 'http://bookmeter.com'
login_url = base_url + '/login'
my_url = base_url + '/u/YOUR_ID'     # YOUR_IDには自分のIDを入れる
favorite_url = my_url + '/favorite_user'

agent.get(login_url) do |page|
  page.form_with(:action => '/login') do |form|
    formdata = {
      :mail => 'EMAIL_ADDRESS', # 自分のログイン用メールアドレスを入れる
      :password => 'PASSWORD',  # 自分のパスワードを入れる
    }
    form.field_with(:name => 'mail').value = formdata[:mail]
    form.field_with(:name => 'password').value = formdata[:password]
  end.submit
end

お気に入りユーザー一覧ページにアクセス

まず、お気に入りユーザーの一覧ページ http://bookmeter.com/u/YOUR_ID/favorite_user にアクセスします。

agent.getでページのコンテンツを取得(46行目)後、複数ページにわたっている場合に各々のページに対して処理できるよう、最大のページ数を正規表現でHTMLから抜き出します(48-50行目)。

1ページ目から最後のページの各々に対して(52-53行目)、agent.getでページのコンテンツを取得します(54行目)。

55-56行目では、取得したコンテンツのHTMLをNokogiriで解析しています。

NokogiriはXpath記法、CSS記法どちらでも構文解析可能ですが、今回はXpathを使いました。

目的とするユーザーのidと名前がHTMLのどの部分にあるか、Webブラウザで確認して該当箇所のXpathを見つけます。Chromeの場合だと右クリックで「要素を検証」で見ることができます。

今回は

//*[@id="main_left"]/div/div/a

というノードにあるようです。

Nokogiri::HTML#xpathメソッドで上記のXpathに該当するノード一つ一つからお気に入りユーザーのid、名前、URLを取得します(57-59行目)。 idはhref要素の相対パス(/u/xxxxxのところ)から正規表現で数字だけ切り出します。

ここで、 user_property という変数に最初の方で定義したUserクラスの属性に対応するハッシュを初期化してやります。 id、名前、URLは上で取得した値を、それ以外は未取得なので、ひとまず0を代入しておきます。

html = agent.get(favorite_url).content.toutf8

if html =~ %r!<center>1/(\d+)<br />.+</center>!
  page_max = $1.to_i
end

(1..page_max).each do |i|
  each_favorite_url = favorite_url + "&p=" + i.to_s
  page = agent.get(each_favorite_url) 
  doc = Nokogiri::HTML(page.content.toutf8)
  doc.xpath("//*[@id=\"main_left\"]/div/div/a").each do |node|
    id = node['href'].to_s.gsub(/\/u\//, '').to_i
    name = node.text
    url = base_url + node['href'].to_s
    user_property = {id: id, name: name, user_url: url, kyodoku_cnt: 0, yonda_cnt: 0, yonderu_cnt: 0, tsundoku_cnt: 0, yomitai_cnt: 0}

お気に入りユーザーの個別ページにアクセス

お気に入りユーザーの個別ページのURLにagent.getで取得し、HTMLを解析します(62-63行目)。 一覧ページの時と同様、ブラウザで抜き出したい情報のXpathを探します。

//*[@id="main_right"]/div/h3

というXpathに共読本の数、読んだ本の数などがあるようです。

これもNokogiri::HTML#xpathメソッドで該当するノード一つ一つに対して処理をします(64-80行目)。

ノードのHTMLタグ内の文字列に対して、case文でマッチする正規表現があった場合に数字だけをキャプチャし( (\d+) のところ)、user_propertyの各要素に代入します。

    user_content = Nokogiri::HTML(agent.get(user_property[:user_url]).content.toutf8)
    doc = user_content.xpath("//*[@id=\"main_right\"]/div/h3")
    doc.each do |node|
      case node.text.strip
      when /共読本\((\d+)冊\)/
        user_property[:kyodoku_cnt] = $1.to_i
      when /読み終わった本\((\d+)冊\)/
        user_property[:yonda_cnt] = $1.to_i
      when /読んでる本\((\d+)冊\)/
        user_property[:yonderu_cnt] = $1.to_i
      when /積読本\((\d+)冊\)/
        user_property[:tsundoku_cnt] = $1.to_i
      when /読みたい本\((\d+)冊\)/
        user_property[:yomitai_cnt] = $1.to_i
      else
        next
      end
      user_property
    end

各ユーザーの情報を構造体にし、ハッシュにまとめる。

ユーザーの情報をハッシュ化したuser_propertyが1人のユーザーに対して得られたら、それをUser#newの引数にとり、構造体化します(82行目)。

さらに、複数のユーザーの情報をひとつのハッシュにまとめてusers変数に格納してやります(83-84行目)。

    user = User.new(user_property) 
    users.store(user.id,user)
    users
  end
end

取得した情報を出力

全ての繰り返し処理を抜けたら、まとめたハッシュに対してイテレータでユーザー一人一人の情報をputsメソッドで文字列として出力します。

共読数だけでなく読んだ本、読んでいる本、積読本、読みたい本を全て出力していますが、自由に書き換えて必要な情報だけ出力することももちろん可能です。

users.each_value do |user|
  puts "ID: #{user.id}, 名前: #{user.name}, 共読本: #{user.kyodoku_cnt}冊, 読み終わった本: #{user.yonda_cnt}冊, 読んでる本: #{user.yonderu_cnt}冊, 積読本: #{user.tsundoku_cnt}冊, 読みたい本: #{user.yomitai_cnt}冊" 
end

まとめ

大した内容のスクリプトではないですが、長文におつき合いくださってありがとうございました。

自分なりにうんうん考えて書いてみましたが、いろいろ無駄も多いだろうと思います。 ぶっちゃけ、今回の内容を出力するだけであれば構造体を作ったりする必要もないんですよね。

気になる点がありましたら、ご指摘いただければ幸いです。

最後になりましたが、Webサイトへのむやみなクローリング・スクレイピングはサイトに迷惑をかけることになりかねません。用法・用量に注意して行いましょう。

参考にしたサイト・文献

Category

programming

Tags

#programming