4 / 26
2015
読書系SNSの読書メーターを利用しています。
ユーザー間のコミュニケーションも活発で、お気に入り・お気に入られ(Twitterでいうフォロー・フォロワーの関係)のユーザーとどのくらい同じ本を読んでいるか(このことを共読と呼んでいます)、なんて話題で盛り上がったりしてます。
ユーザー毎のマイページでそういう情報は確認できますが、数字は日々変化しますし、何十人、何百人といると楽な方法が欲しくなります。
あるお気に入りユーザーの方がPerlで共読数などを一括取得するスクリプトを書かれていて、以前それをいただいたんですが、自分の環境ではうまく動かなかったのでどうせなら自分が勉強中のRubyで同じものを書いちまえ、ということで書いてみました。
スクリプトのソースコードはGistにアップロードしましたので、そちらをご確認ください。
読書メーターにログイン状態でアクセスし、お気に入りユーザーの読書状況を一括取得するRubyスクリプト
使い方、またプログラム作成過程でどういう処理をしたのか、解説していきます。
基本、使用者の代わりにログイン状態で読書メーターにアクセスし、欲しい情報を抜き取るというものです。ユーザーのメールアドレスとパスワードがないと使えません。あしからず。
お手持ちのPCにRubyインタープリタ(Rubyプログラムを解釈・実行するソフトウェア)がインストールされていることが前提です。
まだ入っていない方は、各自の環境に合わせてインストールしてください。
Windowsマシンの方はRubyインストーラーが一番てっとり早いと思います。
上記などを参照にして、RubyInstallerからrubyをインストールしてください。 RubyInstallerは基本的に最新のものを選んでおけば問題ないです。インストーラーの表示が英語ですがガイドにしたがってクリックしていけば難しいことはないはずです。 インストール時にRubyの実行が可能なコマンドプロンプトも一緒にインストールするようにチェックを入れて、実行時はそれを起動すると楽です。
最近のMacであれば、最初からrubyは入っていると思います。
ターミナル.appを起動して、
ruby -v
とコマンドを入力して、rubyのバージョンが表示されればそのまま使えます。
もし、 bash: command not found: ruby
などと表示されたらrubyは入っていないので、Homebrewやrbenv、rvmなどのツールでインストールしてください。
デスクトップ用途でLinuxを使っているような方はここに書くまでもないでしょうから省略。
今回のスクリプトはgemと呼ばれる外部ライブラリを使っています。
使用したのは、HTML構文解析ライブラリであるNokogiriと、Webサイトのスクレイピング補助ライブラリであるMechanizeです。
どちらも、Ruby 1.9.3以上のrubyインタープリタがインストールされていれば、gemコマンドでインストールすることができます。
gem install nokogiri
gem install mechanize
しばらく時間がかかるかも知れません。気長にお待ちください。
"successfully installed xxxx." といったメッセージがコマンドラインに表示されればOKです。
kyodoku.rb ファイルをテキストエディタで開いて、32行目、38行目、39行目の3箇所の大文字部分を編集します。YOUR_ID
はマイページのURLの数字部分、EMAIL_ADDRESS
はログイン用のメールアドレス、PASSWORD
はログイン用のパスワードに変更してください。
上記の準備が終われば、ダウンロードしたkyodoku.rb
ファイルのあるフォルダ(ディレクトリ)内で以下のコマンドを入力します。
ruby kyodoku.rb >> sample.txt
スクリプトは自分のお気に入り登録しているユーザーのID、名前、共読数、読んだ冊数、読んでる冊数、積読の冊数、読みたい本の数を文字列として出力します。そのままだとコマンドラインに出力されるだけなので、>>
で任意のファイルにリダイレクトします。sample.txt
は任意のファイル名でかまいません。ただ同じフォルダにすでにあるファイル名にはしない方がいいです。
プログラムの実行から処理完了まではそれなりに時間がかかります。ここでも焦らずまったりお待ちください。
エラーが表示されず、コマンドプロンプトが表示されれば処理完了です。
あとは保存されたテキストファイルを開けば、お気に入りユーザーの情報が1行ずつずらっと並んでいるはずです。お疲れ様でした。
ここから先はスクリプトの処理の解説をしていきます。
スクリプトが試せればいいやという方はどうぞこのページを閉じてください。
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行目)ます。
モジュールHashInitializable
はinitialize
メソッドに引数としてハッシュをわたし、その値をキーで参照できるようアクセサを定義しています。
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は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サイトへのむやみなクローリング・スクレイピングはサイトに迷惑をかけることになりかねません。用法・用量に注意して行いましょう。