読書メータースクレイピング用の gem を書いた

読書メーターは読書記録をつけたり本のレビューを書いたりできる SNS 風味の Web サービスです。

今回、読書メーターの情報をスクレイピングして Ruby で扱えるようにするための gem を書きました。現状 v0.1.1 です。

概要

Ruby読書メーターのデータを取得できます。

# デフォルトでは 'config.yml' からメールアドレスとパスワードを読み込む
bookmeter = BookmeterScraper::Bookmeter.log_in
# bookmeter = BookmeterScraper::Bookmeter.log_in('example@example.com', 'your_password') でも OK

books = bookmeter.read_books    # ログインユーザの「読んだ本」取得
books[0].name          # 書名
books[0].author        # 著者
books[0].read_dates    # 読了日の配列(初読了日と再読日)

# 2016年3月に「読んだ本」に限定して取得可能
bookmeter.read_books(2016, 3)

# それぞれログインユーザの「読んでる本」「積読本」「読みたい本」を取得
bookmeter.reading_books
bookmeter.tsundoku
bookmeter.wish_list

上記以外にも、ユーザプロフィールなどを取得できます。使いかたの詳細については以下を読んでください。

書いた動機

長いことサービスを使っていると、いろいろとログがたまってきます。すると、例えば、ローカルにデータをエクスポートしたり、過去の各月にどんな本を読んでいたかの調査など、過去の読書に関するデータを自動で処理したくなってきます。

こういうときに Web API が用意されていて、それを通じて各種データのやりとりができるとよいと思うのですが、残念ながら読書メーターには Web API が存在しません。そのため、読書メーターのデータを取得しようとすると、スクレイピングが必要です。しかし、スクレイピングで細かいデータを取ろうとすると、XPath などを使って、HTML から必要な部分をがんばって抽出する必要があり、若干面倒です。

というわけで、スクレイピング周りの処理は隠蔽しつつ、再利用可能にするために、簡単なメソッド呼び出しで読書メーターから情報を取得できるような gem を書きました。

導入方法

gem の導入

以下のコマンドで導入できます。

$ gem install bookmeter_scraper

Bundler を使っているのであれば、Gemfile に bookmeeter_scraper を追記してから、bundle を実行してください。

ログイン情報の入力

ログイン情報の入力方法は 2 通りあります。

  1. Bookmeter.log_in, Bookmeter#log_in に引数として渡す
  2. config.yml に記述する

1. 引数として渡す

以下のように Bookmeter.log_in へメールアドレスとパスワードを引数として渡すことで、ログインできます。

bookmeter = BookmeterScraper::Bookmeter.log_in('example@example.com', 'password')
bookmeter.logged_in?    # true

Bookmeter#log_in でもログイン可能です。

bookmeter = BookmeterScraper::Bookmeter.new
bookmeter.log_in('example@example.com', 'password')

2. config.yml へ記述しておく

まず、以下のように YAML ファイル config.yml を記述し、実行する Ruby スクリプトと同じディレクトリに置きます。

mail: example@example.com
password: your_password

次に、引数なしで Bookmeter.log_in または Bookmeter#log_in を呼ぶと、config.yml からログイン情報を読みとり、ログインできます。

bookmeter = BookmeterScraper::Bookmeter.log_in
bookmeter.logged_in?    # true

注意

過度なスクレイピングはサービスに負担をかけるので、常識の範囲内での実行をお願いします。読書メーターのサーバーへ故意に著しい負荷をかける行為は、利用規約の第 9 条で禁止されています。

おわりに

読書メーターのスクレイピングに rubyXL, parallel が便利だった - Qiita のように、これまでの読書記録が残る読書メーターのデータを処理したいというニーズはあるようです。この gem でも、そのニーズに若干は応えられるかと思います。

また気が向いたら拡張していきたいと思います。

IRB の評価値出力を抑える

小ネタです。

問題

要素の多い Hash などの中身を確認するために、IRB (irb) を使って、その Hash の中身を表示したいときがあります。

このとき、例えば Mechanize::AGENT_ALIASES を表示するために、コマンドを

irb(main):002:0> pp Mechanize::AGENT_ALIASES

のように実行すると、コマンドによる出力と、IRB 自体が表示するコマンドの評価値の出力(ここでは最後の => 以降)の両方が表示されます。

irb(main):002:0> pp Mechanize::AGENT_ALIASES
{"Mechanize"=>
  "Mechanize/2.7.4 Ruby/2.3.0p0 (http://github.com/sparklemotion/mechanize/)",
 "Linux Firefox"=>
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) Gecko/20100101 Firefox/43.0",
 ...
 "Android"=>
  "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2$26.76 Safari/537.36"}
=> {"Mechanize"=>"Mechanize/2.7.4 Ruby/2.3.0p0 (http://github.com/sparklemotion/mechanize/)","Linux Firefox"=>"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) Gecko/20100101 Firefox/43.0", ..., "Android"=>"Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.76 Safari/537.36"}

コンソールがテキストを右端で折り返すようになっていると、IRB の評価値である改行なしの大量のテキストでコンソールが埋まり、読みにくいです。

解決法

実行したい処理の末尾に ; nil をつけ足します。

pp Mechanize::AGENT_ALIASES; nil

すると、IRB によるコマンド自体の評価値が nil になるので、以下の最終行のように、IRB による評価値の出力が nil になり、pp の出力だけ表示されます。よって、表示が見やすくなります。

irb(main):003:0> pp Mechanize::AGENT_ALIASES; nil
{"Mechanize"=>
  "Mechanize/2.7.4 Ruby/2.3.0p0 (http://github.com/sparklemotion/mechanize/)",
 "Linux Firefox"=>
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) Gecko/20100101 Firefox/43.0",
 ...
 "Android"=>
  "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.76 Safari/537.36"}
=> nil

参考

『メタプログラミング Ruby 第 2 版』を読んだ

数週間前になりますが『メタプログラミング Ruby 第 2 版』を読みました。

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

Ruby でのメタプログラミング技術がわかるのはもちろん、Ruby 自体の動作原理の理解にもつながる、とても有用な本だと思ったので、書評を書いておきます。

概要

この本は 2 部構成になっています。

I 部では、Ruby でのメタプログラミング技術について、同僚プログラマとの対話という形式で解説しています。

II 部では、I 部で紹介された技術をもとに、Rails の内部でどのようにメタプログラミングが利用されているかを解説しています。

I 部「メタプログラミング Ruby」

I 部を読むことで、メタプログラミングについて理解が進むとともに、関連する Ruby の言語機能(クラス、メソッドなど)の仕組みが深く理解できます。また、メタプログラミングを使う前と後のコード例を示しながら、その効果が説明されています。これにより、メタプログラミングの有効性が腑に落ちやすいです。

個人的に参考になった部分について、キーワードを列挙しておきます。

  • オブジェクトモデル
    • オブジェクト、クラス、モジュール、特異クラスの関係性
    • メソッド探索
      • 右に行って上に登る
    • Refinements
  • メソッド
    • 動的メソッド (define_method) とゴーストメソッド (method_missing) の利点欠点
    • ブランクスレート (BasicObject) の使いかた
  • ブロック
    • スコープのフラット化
    • eval 族 (instance_eval, class_eval, etc.)
    • Proc と lambda の違い
  • フックメソッド (Module#included) を使ったクラスメソッドの定義

II 部「Rails におけるメタプログラミング」

II 部では、Rails の中で使われているメタプログラミング技術について、実際の例を通じて解説しています。例として利用されているのは以下の機能です。

  • ActiveRecord
  • ActiveSupport::Concern
  • alias_method_chain
  • アトリビュートメソッド

メタプログラミングは徐々に複雑になるソフトウェアの設計を最適なものにするための道具として有用みたいです。例えば、クラスへインスタンスメソッドとクラスメソッドの両方を取り込むために、includeextend によるトリックを使っていた部分が include の連鎖などで複雑化してしまったとき、より DRY にするために ActiveSupport::Concern を導入したそうです。

逆に言うと、最初からあまり凝ったことをしてしまうと可読性を落とすだけという結果になりそうなわけですが、その点についても以下のように言及されています。

“私がコードを書くときは、最初から完璧な設計は目指さない。
必要になるまで複雑なメタプログラミングの魔術は使わない。
コードをシンプルに保ち、その仕事を成し遂げるための最も明白な技法を使う。
おそらくある時点から、コードが複雑になったり、排除しにくい重複が見つかったりするだろう。
メタプログラミングのような切れ味の鋭いツールに手を伸ばすのはそのときだ。”

感想

13 章の章タイトルとなっている「メタプログラミングはただのプログラミング」の言葉通り、Ruby という言語では、コードを書くときに自然にメタプログラミングとされる技術に触れることになります。よって、それらに対する知識をつけておいたほうが、Ruby らしい、可読性が高くメンテしやすいコードを書けるようになるでしょう。そのためにも『メタプログラミング Ruby』は役立つと思いました。

余談ですが、この本では、includeprepend での継承チェーンへのオブジェクト挿入や、特異クラスの話など、オブジェクトモデルについての話が多くあります。その点で、Ruby の内部動作について解説した本である『Ruby のしくみ』とも少し関連する内容となっていると思います。

『Ruby のしくみ』は Ruby の内部(C の構造体)や YARV のコードも含めた解説になっているので、『メタプログラミング Ruby』よりも詳細に踏み込んでいるといえます。ですので、Ruby の内部動作について知りたい場合は、『メタプログラミング Ruby』→『Ruby のしくみ』の順番で読めば、だんだんブレイクダウンしていく感じになり、理解しやすいのではと思いました。私は逆の順で読みましたが……

というわけで、普段のプログラミングに使える技術と Ruby の動作の両方を知ることができる『メタプログラミング Ruby 第2版』はおすすめです。

Webmock でリダイレクトが絡む HTTP 通信をスタブ化する

テストのために、リダイレクトが絡む HTTP 通信をスタブ化したい場合があります。

例えば、Web スクレイピングをするときに、Mechanize を使うと以下のようにログイン処理を書けます。HTTPS になっていないとかは気にしないでください。

require 'mechanize'

def log_in(mail, password)
  agent = Mechanize.new
  next_page = nil
  agent.get('http://www.example.com/login') do |page|
    next_page = page.form_with(action: '/login') do |form|
      form.field_with(name: 'mail').value = mail
      form.field_with(name: 'password').value = password
    end.submit
  end
  next_page # -> ログイン後のページが入っている
end

この処理の裏での HTTP 通信に着目すると、以下のような実装になっていることがあります。

  1. ログインフォームによって http://www.example.com/login へ POST して認証に成功する
  2. 302 リダイレクトがレスポンスとして返ってくる
  3. リダイレクト先であるログイン後のページを GET する
  4. ログイン後のページがレスポンスとして返ってくる

この通信をスタブ化する方法を調べました。

Webmock によるリダイレクトのスタブ化

Webmock を使って、以下のようにコードを書くとできました。

stub_request(:post, 'http://www.example.com/login')
  .to_return(status: 302, headers: { 'Location' => '/', 'Content-Type' => 'text/html' })
stub_request(:get, 'http://bookmeter.com/')
  .to_return(body: 'contents', headers: { 'Content-Type' => 'text/html' })

一つ目の stub_request では、例えばログインのために http://www.example.com/login に POST すると、ログインが成功したとして 302 リダイレクトが帰ってくるようにスタブを設定しています。このとき、to_return の引数で与えているように、レスポンスヘッダ内の Location'/' とすることで、リダイレクト先が http://www.example.com/ であることを示しています。

二つ目の stub_request では、リダイレクトされた先の http://www.example.com/ を GET することで、正常なレスポンスが返ってくるようにスタブを設定しています。

以上のスタブの設定によって、

  1. リクエスト:/login へ POST(ログイン処理) -> レスポンス:/ へのリダイレクト
  2. リクエスト:/ へ GET -> レスポンス:/ の内容(ログイン後のページ)

という流れをスタブ化することができます。

余談

302 が返ってきたときに GET で自動リダイレクトするのは、実は RFC に違反しているらしいです。

Note: RFC 1945 and RFC 2068 specify that the client is not allowed to change the method on the redirected request. However, most existing user agent implementations treat 302 as if it were a 303 response, performing a GET on the Location field-value regardless of the original request method. The status codes 303 and 307 have been added for servers that wish to make unambiguously clear which kind of reaction is expected of the client.

訳) RFC 1945 と RFC 2068 では、リダイレクト時のリクエストにおけるメソッドの変更は許されてないとしている。にも関わらず、ほとんどの既存ユーザエージェントの実装は、302 をあたかも 303 のようにみなし、元々のリクエストメソッドに関わらず Location ヘッダで示される値(URI)に GET リクエストをしかけている。303 や 307 は、クライアントに期待する振舞いがどちらかをサーバがはっきり示すために追加された。

Rails 4.1 以降のアプリを Heroku デプロイ時に Internal Server Error が発生したら

問題

Rails 4.1 以降のアプリを

$ git push heroku master
$ heroku open

で Heroku へのデプロイとアプリへのアクセスをおこなうと、Internal Server Error が発生し、以下のメッセージが表示されることがあります。

app error: Missing secret_key_base for ‘production’ environment, set this value in config/secrets.yml (RuntimeError)

「本番環境で secret_key_base が見つからないから、config/secrets.yml の中で値を設定しろ」と言っています。

原因

セッション使用時のクッキー改竄防止に利用する secret_key_base という変数があります。この変数は、Rails 4.0 までは config/initializer/secret_token.rb で設定されていました。

一方、Rails 4.1 からは、新しく導入された設定ファイル config/secrets.ymlsecret_key_base の値を管理するようになっています。このファイルは Web API の secret key なんかを保存しておくのにもつかえます。また、デフォルトでは、本番環境用に以下の要領で環境変数による設定をしています。これでファイルに値を直書きせずにすんでいます。

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

その用途から、本来はバージョン管理下に置かないものだと思いますが、Heroku ではこのファイルが必須となっています。

私の場合だと、GitHub が提供している Rails.gitignore.gitignore として使っていました。これには config/secrets.yml のパスが入ってしまっていました。よって、config/secrets.yml が Git の管理対象になっておらず、Heroku にデプロイしたときに、この config/secrets.yml は本番環境に配置されなくなっていました。結果として、secret_key_base が見つからず、エラーになったようです。

解決法

config/secrets.yml.gitignore に入っている場合、.gitignore から外して、git push heroku master でアプリを Heroku にデプロイします。これで、Heroku 上の環境にも config/secrets.yml が配置されて、エラーは出なくなるはずです。