普段は OpenURL 、 JS が必要な場合には Watir-Webdriver を使ってサイトへアクセスするのですが、今回は Cookies が必要なだけだったので、初めて Mechanize を使ってみました。記録しておきます。
今回の目的
Qiita にログインして http://qiita.com/api/notifications (通知)の JSON を取得
Mechanizeインスタンス生成
agent = Mechanize.new
ログインページへ
トップからもログイン出来ますが、ログインページだけは https なのでこちらのほうが好ましいでしょう。
ページ取得は get メソッドを使います。
> page = agent.get('https://qiita.com/login') => #<Mechanize::Page {url #<URI::HTTPS:0x007faf6f9915f8 URL:https://qiita.com/login>} {meta_refresh} {title "Login - Qiita"} {iframes #<Mechanize::Page::Frame nil "//www.googletagmanager.com/ns.html?id=GTM-TBQWPN">} {frames} {links #<Mechanize::Page::Link "Login with GitHub" "https://qiita.com/auth/github?"> #<Mechanize::Page::Link "Login with Twitter" "https://qiita.com/auth/twitter?"> #<Mechanize::Page::Link "sign up." "/signup"> #<Mechanize::Page::Link "Forget Password?" "https://qiita.com/sessions/forgot_password">} {forms #<Mechanize::Form {name nil} {method "POST"} {action "/login"} {fields [hidden:0x3fd7b80503a0 type: hidden name: utf8 value: ✓] [hidden:0x3fd7b805024c type: hidden name: authenticity_token value: ZP5/wLWqeCZwS6rGFBjsRqd6Im6zbSA4ceZSiWTQyv7663JGQE1tLH8PG9Ie+R6qlv/eW4rPiii9USow4+2tug==] [text:0x3fd7b80500e4 type: text name: identity value: ] [field:0x3fd7b8055f6c type: password name: password value: ]} {radiobuttons} {checkboxes} {file_uploads} {buttons [submit:0x3fd7b8055e40 type: submit name: commit value: Log In]}>}>
Mechanize::Pageインスタンスのメソッド
> ls page Mechanize::Parser#methods: [] canonical_each code= extract_filename find_free_name key? response= uri= []= code each fill_header header response uri Mechanize::File#methods: body body= content filename filename= save save! save_as Mechanize::Page#methods: / encoding_error? iframe labels_hash reset at encodings iframe_with link response_header_charset base form iframe_with! link_with root base_with form_with iframes link_with! search base_with! form_with! iframes_with links select_bases bases forms image links_with select_forms bases_with forms_with image_urls mech select_frames canonical_uri frame image_with mech= select_iframes content_type frame_with image_with! meta_charset select_images detected_encoding frame_with! images meta_refresh select_links encoding frames images_with parser title encoding= frames_with labels pretty_print instance variables: @bases @encoding @forms @iframes @links @meta_refresh @title @body @encodings @frames @labels @mech @parser @uri @code @filename @full_path @labels_hash @meta_content_type @response
ヘッダ
header メソッド。エイリアスとして response メソッド。
> page.header => {"cache-control"=>"max-age=0, private, must-revalidate", "content-encoding"=>"gzip", "content-type"=>"text/html; charset=utf-8", "date"=>"Tue, 24 Mar 2015 04:42:35 GMT", "server"=>"nginx", "set-cookie"=> "_qiita_login_session=YjliL0RBUTBpN2xNQ25xWG1qUTBSL2NpeHFVUEs1bnZQNGxWZ3VLemI3YkxCUG10OTNqK0N5TXk1S3RkUWpja09OYW5xT2EzaU4zTGNNcFJORWQxQmVVamN0cU80ZDBoQlRaYllScHFidnhpLzc2RHRid2ZpNDJCcnVHQW5UQUk5c2ZHS1d2N2hTUzExaHpSQ0N2TSt2SklTb0VOZFVnU3lTUWNxd2I2bW1jbVM1NkwrM1d1U3dCWHdCQWgySi9CLS1CQmE4QzlBQUl5aE9rOU5nU3l5UmR3PT0%3D--cbffe82afceb3d796bd573499c21f423a83fca8c; domain=.qiita.com; path=/; expires=Tue, 07 Apr 2015 04:42:35 -0000; HttpOnly", "status"=>"200 OK", "x-content-type-options"=>"nosniff", "x-frame-options"=>"SAMEORIGIN", "x-request-id"=>"837d74cf-0340-4c58-adb3-df0bb57b4c72", "x-runtime"=>"0.057129", "x-xss-protection"=>"1; mode=block", "transfer-encoding"=>"chunked", "connection"=>"keep-alive"}
each メソッドでも取得可能。
> page.each { |k, v| puts k + " | " + v } cache-control | max-age=0, private, must-revalidate content-encoding | gzip content-type | text/html; charset=utf-8 date | Tue, 24 Mar 2015 04:42:35 GMT server | nginx set-cookie | _qiita_login_session=YjliL0RBUTBpN2xNQ25xWG1qUTBSL2NpeHFVUEs1bnZQNGxWZ3VLemI3YkxCUG10OTNqK0N5TXk1S3RkUWpja09OYW5xT2EzaU4zTGNNcFJORWQxQmVVamN0cU80ZDBoQlRaYllScHFidnhpLzc2RHRid2ZpNDJCcnVHQW5UQUk5c2ZHS1d2N2hTUzExaHpSQ0N2TSt2SklTb0VOZFVnU3lTUWNxd2I2bW1jbVM1NkwrM1d1U3dCWHdCQWgySi9CLS1CQmE4QzlBQUl5aE9rOU5nU3l5UmR3PT0%3D--cbffe82afceb3d796bd573499c21f423a83fca8c; domain=.qiita.com; path=/; expires=Tue, 07 Apr 2015 04:42:35 -0000; HttpOnly status | 200 OK x-content-type-options | nosniff x-frame-options | SAMEORIGIN x-request-id | 837d74cf-0340-4c58-adb3-df0bb57b4c72 x-runtime | 0.057129 x-xss-protection | 1; mode=block transfer-encoding | chunked connection | keep-alive
応答だけならば code メソッドで。
> page.code => "200"
ソース
body メソッド。エイリアスとして content メソッドもあり。
> page.body => "<!DOCTYPE html><html xmlns:og=\"http://ogp.me/ns#\"><head><meta charset=\"UTF-8\" /><title>Login - Qiita</title><meta content=\"width=device-width,height=device-height,initial-scale=1\" name=\"viewport\" /><meta content=\"Qiita is a technical knowledge sharing and collaboration platform for programmers. You can record and post programming tips, know-how and notes here.\" name=\"description\" /><meta content=\"summary\" name=\"twitter:card\" /><meta content=\"@Qiita\" name=\"twitter:site\" /><meta content=\"Login - Qiita\" property=\"og:title\" /> (以下略)
ファイル名
> page.filename => "login.html"
タイトル
> page.title => "Login - Qiita"
リンク
links メソッド。配列で返す。 link メソッドだと一つ目だけ。
> page.links => [#<Mechanize::Page::Link "Login with GitHub" "https://qiita.com/auth/github?">, #<Mechanize::Page::Link "Login with Twitter" "https://qiita.com/auth/twitter?">, #<Mechanize::Page::Link "sign up." "/signup">, #<Mechanize::Page::Link "Forget Password?" "https://qiita.com/sessions/forgot_password">]
links_with メソッド*1を使うと条件を満足するインスタンスのみを配列で返す。 link_with メソッドだと一つ目だけ。
条件に出来るキーは
有効なメソッドと値のペアは Mechanize::Page::Link のメソッドと返り値になります。
Mechanize::Page - Ruby Mechanize wiki (ja)
class Mechanize::Page::Link
具体的には :href, :text 。(:rel も?)
> page.links_with(text: /Login/) => [#<Mechanize::Page::Link "Login with GitHub" "https://qiita.com/auth/github?">, #<Mechanize::Page::Link "Login with Twitter" "https://qiita.com/auth/twitter?">] > page.links_with(href: /sign/) => [#<Mechanize::Page::Link "sign up." "/signup">]
Mechanize::Page::Link#click メソッドでリンクをクリックできる。
いわゆる history.back したい(前のページへ戻りたい)場合には Mechanize#back メソッド
> agent.back
で OK 。
history は Mechanize#history メソッド。
> agent.history => [#<Mechanize::Page {url #<URI::HTTPS:0x007faf6fad7818 URL:https://qiita.com/login>} (以下略)
フォーム
forms メソッド。配列で返す。 form メソッドだと一つ目だけ。
このページにはユーザネーム/パスワードを入れるフォームが1つだけあるので form メソッドで取得。
> auth = page.form => #<Mechanize::Form {name nil} {method "POST"} {action "/login"} {fields [hidden:0x3fd7b80503a0 type: hidden name: utf8 value: ✓] [hidden:0x3fd7b805024c type: hidden name: authenticity_token value: ZP5/wLWqeCZwS6rGFBjsRqd6Im6zbSA4ceZSiWTQyv7663JGQE1tLH8PG9Ie+R6qlv/eW4rPiii9USow4+2tug==] [text:0x3fd7b80500e4 type: text name: identity value: ] [field:0x3fd7b8055f6c type: password name: password value: ]} {radiobuttons} {checkboxes} {file_uploads} {buttons [submit:0x3fd7b8055e40 type: submit name: commit value: Log In]}>
Mechanize::Formインスタンスのメソッド
> ls auth Mechanize::Form#methods: [] dom_class has_key? radiobuttons_with []= dom_id has_value? request_data action elements hidden_field? reset_button? action= encoding hiddens resets add_button_to_query encoding= ignore_encoding_error save_hash_field_order add_field! enctype ignore_encoding_error= select_buttons build_query enctype= keygens select_checkboxes button field keys select_fields button_with field_with method select_file_uploads button_with! field_with! method= select_radiobuttons buttons fields method_missing set_fields buttons_with fields_with name submit checkbox file_upload name= submit_button? checkbox_with file_upload_with page submits checkbox_with! file_upload_with! pretty_print text_field? checkboxes file_uploads radiobutton textarea_field? checkboxes_with file_uploads_with radiobutton_with textareas click_button form_node radiobutton_with! texts delete_field! has_field? radiobuttons values instance variables: @action @clicked_buttons @fields @ignore_encoding_error @name @buttons @encoding @file_uploads @mech @page @checkboxes @enctype @form_node @method @radiobuttons
フィールド
fields メソッド。 Mechanize::Form::Field を要素とする配列で返す。 field メソッドだと一つ目だけ。
auth.fields => [[hidden:0x3fd7b80503a0 type: hidden name: utf8 value: ✓], [hidden:0x3fd7b805024c type: hidden name: authenticity_token value: ZP5/wLWqeCZwS6rGFBjsRqd6Im6zbSA4ceZSiWTQyv7663JGQE1tLH8PG9Ie+R6qlv/eW4rPiii9USow4+2tug==], [text:0x3fd7b80500e4 type: text name: identity value: ], [field:0x3fd7b8055f6c type: password name: password value: ]]
fields_with メソッドだと(links_with メソッドと同様に)条件を満足するインスタンスのみを配列で返す。 field_with メソッドだと一つ目だけ。
> auth.fields_with(type: "text") => [[text:0x3fd7b80500e4 type: text name: identity value: ]] > auth.fields_with(name: "identity") => [[text:0x3fd7b80500e4 type: text name: identity value: ]]
:name キーだけはキー指定しなくても OK 。
> auth.fields_with("identity") => [[text:0x3fd7b80500e4 type: text name: identity value: ]] > auth.field_with("password") => [field:0x3fd7b8055f6c type: password name: password value: ]
フィールドに値を設定
Mechanize::Form::Field#value メソッドで設定。
> auth.field_with("identity").value = "riocampos" => "riocampos" > auth.field_with("password").value = "password" => "password"
確認。
> auth.field_with("identity") => [text:0x3fd7b80500e4 type: text name: identity value: riocampos] > auth.field_with("password") => [field:0x3fd7b8055f6c type: password name: password value: password]
ログインボタンを押してSubmit(その1)
Mechanize::Form インスタンス auth にユーザネームとパスワードを設定したので、Mechanize::Form#click_button メソッドで送信する。
> auth.click_button => #<Mechanize::Page {url #<URI::HTTP:0x007faf6fa58310 URL:http://qiita.com/>} {meta_refresh} {title "ホーム - Qiita"} {iframes #<Mechanize::Page::Frame nil "//www.googletagmanager.com/ns.html?id=GTM-TBQWPN">} {frames} {links #<Mechanize::Page::Link "" "/"> #<Mechanize::Page::Link "Markdownによる情報共有サービス、Qiita:Team" "https://teams.qiita.com?utm_source=qiita&utm_medium=header_news"> #<Mechanize::Page::Link "新規作成" "/drafts"> #<Mechanize::Page::Link "投稿する" "/drafts/new"> #<Mechanize::Page::Link "下書き一覧" "/drafts"> #<Mechanize::Page::Link "ストック一覧" "/riocampos/stock"> #<Mechanize::Page::Link "通知一覧を見る" "/notifications"> #<Mechanize::Page::Link "" "#"> #<Mechanize::Page::Link "マイページ" "/riocampos"> #<Mechanize::Page::Link "編集リクエスト管理" "/patches"> #<Mechanize::Page::Link "プロフィール設定" "/settings/profile"> #<Mechanize::Page::Link "設定" "http://qiita.com/settings/account"> #<Mechanize::Page::Link "ログアウト" "http://qiita.com/logout"> #<Mechanize::Page::Link "ノウハウ・Tipsを投稿する" "/drafts/new"> #<Mechanize::Page::Link "フィード" "/"> #<Mechanize::Page::Link "すべての投稿" "/public"> #<Mechanize::Page::Link "自分の投稿 64" "/mine"> #<Mechanize::Page::Link "ストック" "/stock"> #<Mechanize::Page::Link "riocampos" "/riocampos"> #<Mechanize::Page::Link "フィード" "/"> #<Mechanize::Page::Link "Advent Calendar" "/advent-calendar"> #<Mechanize::Page::Link "Organization一覧" "/organizations"> #<Mechanize::Page::Link "hibikiw" "/hibikiw"> #<Mechanize::Page::Link "ytsuboi" "/ytsuboi"> #<Mechanize::Page::Link "tkusano" "/tkusano"> #<Mechanize::Page::Link "takayama" "/takayama"> #<Mechanize::Page::Link "yataro" "/yataro"> #<Mechanize::Page::Link "もっと見る" "/recommended_users"> #<Mechanize::Page::Link "Tweets by @Qiita" "https://twitter.com/Qiita"> #<Mechanize::Page::Link "返信の必要なお問い合わせはこちら" "https://increments.zendesk.com/anonymous_requests/new"> #<Mechanize::Page::Link "Qiitaとは" "http://qiita.com/about"> #<Mechanize::Page::Link "タグ一覧" "http://qiita.com/tags"> #<Mechanize::Page::Link "Advent Calendar一覧" "http://qiita.com/advent-calendar"> #<Mechanize::Page::Link "Organization一覧" "http://qiita.com/organizations"> #<Mechanize::Page::Link "ユーザー一覧" "http://qiita.com/users"> #<Mechanize::Page::Link "Developer API" "http://qiita.com/api/v2/docs"> #<Mechanize::Page::Link "Webhook ドキュメント" "http://qiita.com/api/webhook/docs"> #<Mechanize::Page::Link "JavaScript License" "/license.txt"> #<Mechanize::Page::Link "公式ブログ" "http://blog.qiita.com"> #<Mechanize::Page::Link "利用規約" "http://qiita.com/terms"> #<Mechanize::Page::Link "プライバシーポリシー" "http://qiita.com/privacy"> #<Mechanize::Page::Link "特定商取引法に基づく表記" "http://qiita.com/asct"> #<Mechanize::Page::Link "サポート" "http://support.qiita.com"> #<Mechanize::Page::Link "お問い合わせ" "https://increments.zendesk.com/anonymous_requests/new"> #<Mechanize::Page::Link "運営会社" "http://increments.co.jp"> #<Mechanize::Page::Link "Kobito - プログラミングのメモやスニペットの記録に最適なMacアプリケーショ ン" "http://kobito.qiita.com"> #<Mechanize::Page::Link "Qiita:Team - シンプル、スマートかつクローズドな情報共有サービス" "http://teams.qiita.com"> #<Mechanize::Page::Link "Qiita:Career - プログラマのためのキャリア構築支援サービス" "http://career.qiita.com">} {forms #<Mechanize::Form {name nil} {method "GET"} {action "/search"} {fields [hidden:0x3fd7b8154058 type: hidden name: utf8 value: ✓] [hidden:0x3fd7b8159e68 type: hidden name: sort value: rel] [text:0x3fd7b8159cec type: text name: q value: ] [selectlist:0x3fd7b8158ea0 type: name: sort value: rel]} {radiobuttons} {checkboxes [checkbox:0x3fd7b8159990 type: checkbox name: stocked value: false]} {file_uploads} {buttons [submit:0x3fd7b8159814 type: submit name: commit value: 検索]}> #<Mechanize::Form {name nil} {method "POST"} {action "/feedbacks"} {fields [hidden:0x3fd7b7e7b5c0 type: hidden name: utf8 value: ✓] [hidden:0x3fd7b7e7b46c type: hidden name: authenticity_token value: 2+wUxiggBcUxJXHYsInoC1Os4BYM7Sw6dYctJKuHOaAzhisClUU4a1YMTfVmil+vxiOpc01V/rsZTAFUnejQBw==] [text:0x3fd7b7e7afd0 type: text name: feedback[name] value: ] [textarea:0x3fd7b7e7aa80 type: name: feedback[message] value: ]} {radiobuttons} {checkboxes [checkbox:0x3fd7b7e7b1b0 type: checkbox name: feedback[send_user_info] value: true]} {file_uploads} {buttons [submit:0x3fd7b7e7b318 type: submit name: commit value: 運営者に意見を送る]}>}>
ログイン出来ました。
ログインボタンを押してSubmit(その2)
agent インスタンスをレシーバとして Mechanize#submit メソッドを使う。その際の引数として Mechanize::Form インスタンスとそのインスタンス中の Mechanize::Form::Submit インスタンスを渡す。
> agent.submit(auth, auth.button) => #<Mechanize::Page {url #<URI::HTTPS:0x007faf6fa84f50 URL:https://qiita.com/>} {meta_refresh} {title "Qiita - A technical knowledge sharing platform for programmers."} (以下同)
Nokogiriオブジェクトへの変換
XPath を指定しての Mechanize::Page#at メソッドで Nokogiri オブジェクトに変換できます。Mechanize::Page#root メソッド及びエイリアスの Mechanize::Page#parser メソッドでルートの Nokogiri オブジェクトが得られます。 Nokogiri 扱いのほうが Mechanize よりも得意な私は、目的のページに辿り着いたら Nokogiri オブジェクトにしてしまったほうが良さそう。
コレクション(NodeSet)を取得したい場合には Mechanize::Page#search メソッドで。
Cookies
確認
Qiitaにログインした状態で Cookies を確認してみます。 Mechanize#cookie_jar メソッドを使います。
> agent.cookie_jar => #<Mechanize::CookieJar:0x007faf6f96b650 @store= #<HTTP::CookieJar::HashStore:0x007faf6c2ec980 @gc_index=13, @gc_threshold=150, @jar= {"qiita.com"=> {"/"=> {"_qiita_login_session"=> #<HTTP::Cookie:name="_qiita_login_session", value="RVVMYzM1WGd2YjFwdVdGWHFXcSt6ajgvSDgrWkxUSVhoTzRtbGk4SkhoSUVJZ1cyZHNiMkVheDNDR2pxdHB0cWI3a0RLQ0wwSHlsODJsTGk2bk91aUluNDNsLzF2UWw0Q0R3cm5xK2RJLytJSTRmdGoyM0hESkUwWWZ5bmFScTNWRWI2Z3MvSWh0dlJLeGRLVXB1aVN5VG9xNlFJRmVZUjlEWXg2aE5ESUhPcmNXR200RTZab1FUV21YbnpRQWU1L2VoTkFsNXNNY1l4OERFckxwRDh5UT09LS1TL1p5RHllSHIvRnpVSE5FU2Z0ZktnPT0%3D--1f3a62190ba2dd206c15b6747bf8ef84c125dc74", domain="qiita.com", for_domain=true, path="/", secure=false, httponly=true, expires=2015-04-07 06:56:12 UTC, max_age=nil, created_at=2015-03-24 15:56:13 +0900, accessed_at=2015-03-24 15:56:13 +0900 origin=http://qiita.com/>, "secure_token"=> #<HTTP::Cookie:name="secure_token", value="24fb42e01282a333b5d7ea32642bca3457a1b84684759fa5621054a6c4d38fec", domain="qiita.com", for_domain=true, path="/", secure=true, httponly=true, expires=2035-03-24 06:56:11 UTC, max_age=nil, created_at=2015-03-24 15:56:13 +0900, accessed_at=2015-03-24 15:56:13 +0900 origin=https://qiita.com/login>}}}, @logger=nil, @mon_count=0, @mon_mutex=#<Mutex:0x007faf6c2ec930>, @mon_owner=nil>>
返ってくるのは Mechanize::CookieJar インスタンス。
> agent.cookie_jar.class => Mechanize::CookieJar
Mechanize::CookieJar インスタンスのメソッドを確認。
> ls agent.cookie_jar Enumerable#methods: all? detect each_with_object grep max_by one? sort any? drop entries group_by member? partition sort_by chunk drop_while find include? min reduce take collect each_cons find_all inject min_by reject take_while collect_concat each_entry find_index lazy minmax reverse_each to_a count each_slice first map minmax_by select to_h cycle each_with_index flat_map max none? slice_before zip HTTP::CookieJar#methods: << cleanup clear cookies delete each empty? parse store Mechanize::CookieJarIMethods#methods: add add! clear! dump_cookiestxt jar load_cookiestxt save_as Mechanize::CookieJar#methods: load save instance variables: @store
Cookiesの保存
Cookies の永続化には Mechanize::CookieJar#save メソッドを使って YAML 形式で保存するのが一般的と思われます。ですが、この save メソッドは引数が IO かファイル名である必要があります。 Heroku などではローカルファイルが cycle 時に消えてしまいますので、このメソッドを使っても消えてしまいます。これでは役に立ちません。ということで Mechanize::CookieJar#save メソッドに StringIO インスタンスを渡すことにより Cookies を YAML 形式にした文字列に変換します。
def cookies_to_yaml_string(agent) cookies_io_write = StringIO.new("", 'r+') agent.cookie_jar.save(cookies_io_write) cookies_io_write.string end
また Mechanize::CookieJar#load メソッドで Mechanize インスタンスに Cookies をセットします。
def set_cookies(agent, cookies_yaml) cookies_io_read = StringIO.new(cookies_yaml, 'r') agent.cookie_jar.clear agent.cookie_jar.load(cookies_io_read) end
Cookies を Redis To Go に保存してみます。
既に Redis To Go の登録が済んでおり、 Redis のアクセス URL が "redis://redistogo:cdcddcfedd5ac469f64f4c44f7cc77bf@xxx.redistogo.com:9999/" であるとします。
redis_url = "redis://redistogo:cdcddcfedd5ac469f64f4c44f7cc77bf@xxx.redistogo.com:9999/" Redis.current = Redis.new(url: redis_url) cookies = Redis::Value.new('cookies')
では、 Mechanize でログインした時点での Cookies を Redis To Go に保存します。
cookies.value = cookies_to_yaml_string(agent)
保存した Cookies を Mechanize のインスタンス agent に再度セットするには
set_cookies(agent, cookies.value)
とすれば OK です。
ログイン後アクセス
当初の目的の URL から JSON を取得します。
json = JSON.parse(agent.get('http://qiita.com/api/notifications').body)
その直後に、保持している Cookies で Redis 側に保存(更新)します。
cookies.value = cookies_to_yaml_string(agent)
このように、 Cookies がセットされていればログイン手順不要となるサイトは多いと思いますので、この手法はわりと使えると思います。
エラーハンドリング
通常のエラーメソッド (Ruby - エラーをrescueして、rescueしない場合と全く同じエラーメッセージを出力する - Qiita)に加えて、 Exception: Mechanize::ResponseCodeError では response_code メソッドが使えますので、エラーコードにより振り分けを行う事が可能です。
参考
参考
- 楽々スクレイピング! Ruby Mechanizeの使い方 -- ぺけみさお
- 楽々スクレイピング! Ruby Mechanizeの使い方(2) -- ぺけみさお
- mechanize-2.7.0 Documentation
- File: README — Documentation for mechanize (2.7.3)
- Mechanize - Ruby Mechanize wiki (ja)(内容が古い:バージョン1.0.0、最終更新が2010年10月06日)
- Mechanize でCookieの追加と削除 - それマグで!
- Mechanize で Cookie を手動でセットする - こしごぇ(B)