初めてのWebスクレイパー構築ガイド 第2回:Mechanizeでリンク・フォーム操作を自動化する
このチュートリアルでは、Mechanizeを使ってリンクのクリック、フォームへの入力、ファイルのアップロードを行う方法を学びます。さらに、Mechanizeのページオブジェクトを自在に扱う方法や、Google検索を自動化してその結果を保存する方法についても解説します。
目次
- 単一ページとページネーションの違い
- Mechanizeとは
- エージェント(Agent)
- ページ(Page)
- Nokogiriメソッドの活用
- リンク操作
- クリック操作
- フォーム操作
単一ページとページネーション
これまでのパートでは、Nokogiriを使って単一ページからデータを抽出する方法を見てきました。これは良い基礎となりましたが、次の一歩として、複数ページからコンテンツを抽出する方法を学ぶ必要があります。
というのも、私たちが解決しようとしている問題は、140以上のエピソードからコンテンツを取得することです。これは1つのWebページに収まる量をはるかに超えています。つまり、ページネーション(ページ送り)に対応し、「ウサギの穴」のように続くコンテンツをたどっていく方法を考える必要があるのです。
ここでNokogiriの出番は終わり、Mechanizeという便利なGemが登場します。
Mechanizeとは
Mechanizeは、多くの便利な機能を備えた強力なツールです。その本質は、コンテンツを抽出したいWebサイトとのやり取りを自動化できることにあります。Capybaraを使ったテストで見たことがある機能に少し似ていると感じるかもしれません。
誤解しないでほしいのですが、Nokogiriで単一ページを扱うだけでも十分に楽しいものです。しかし、より複雑なデータ抽出タスクには、もう少しパワーが必要になります。Mechanizeを使えば、必要なだけページをクロールし、その要素とやり取りすることで、人間の行動を模倣・自動化できます。かなり強力ですよね。
このGemを使えば、リンクをたどったり、フォームフィールドに入力してデータを送信したりできるだけでなく、Cookieの扱いまで可能です。つまり、ユーザーがプライベートセッションにログインする動作を再現し、自分だけがアクセスできるサイトからコンテンツを取得することもできるのです。
認証情報でログインフォームに入力し、Mechanizeにその後の手順を指示すればよいのです。リンクのクリックもフォームの送信もできるため、このツールでできないことはほとんどありません。MechanizeはNokogiriと密接な関係があり、Nokogiriに依存しています。そして、この素晴らしいGemの作者の一人が、またしてもAaron Pattersonなのです。
Mechanizeエージェントのインスタンス化
実際にMechanizeを使い始める前に、まずMechanizeエージェントをインスタンス化する必要があります。
some_scraper.rb
require 'mechanize' agent = Mechanize.new
このagentは、Nokogiriで行ったのと同じように、ページを取得するために使用します。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url)
ここでは、MechanizeエージェントがポッドキャストのページとそのCookieを取得しました。
ページコンテンツの抽出
これで抽出準備が整ったページを手に入れました。実際に抽出する前に、inspectメソッドを使って内部構造を覗いてみることをおすすめします。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) puts page.inspect
出力はかなりのボリュームになります。Mechanize::Pageオブジェクトが何で構成されているのか、自分の目で確かめてみてください。ページのすべての属性がここに表示されます。
個人的には、抽出したいデータを切り分ける際に、このオブジェクトは非常に便利だと感じています。
出力例
#<Mechanize::Page
{url #https://betweenscreens.fm/>}
{meta_refresh}
{title "Between | Screens "}
{iframes
#<Mechanize::Page::Frame
nil
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/290328784&color=ff0000&auto...>
...}
{frames}
{links
#<Mechanize::Page::Link "Logo cube" "/">
#<Mechanize::Page::Link "about" "pages/about/">
#<Mechanize::Page::Link "design" "design/">
#<Mechanize::Page::Link "code" "code/">
#<Mechanize::Page::Link "Randy J. Hunt" "episodes/144/">
#<Mechanize::Page::Link "Jason Long" "episodes/143/">
#<Mechanize::Page::Link "David Heinemeier Hansson" "episodes/142/">
#<Mechanize::Page::Link "Zach Holman" "episodes/141/">
#<Mechanize::Page::Link "Joel Glovier" "episodes/140/">
#<Mechanize::Page::Link "Older Stuff »" "page/2/">
...}
{forms}>
HTMLページそのものを見たい場合は、bodyまたはcontentメソッドを付け加えるだけです。
some_scraper.rb
... print page.body ...
このポッドキャストサイトはページ上の要素が少ないので、参考としてgithub.comから返されるMechanize::Pageも紹介しておきます。こちらはより多様なコンテンツを含んでおり、こうした構造に慣れておくことは重要だと思います。
github.comの出力例
#<Mechanize::Page
{url #https://github.com/>}
{meta_refresh}
{title "How people build software · GitHub"}
{iframes}
{frames}
{links
#<Mechanize::Page::Link "Skip to content" "#start-of-content">
#<Mechanize::Page::Link "Sign up" "/join?source=header-home">
#<Mechanize::Page::Link "Sign in" "/login">
#<Mechanize::Page::Link "Pricing" "/pricing">
...}
{forms
#<Mechanize::Form
{name nil}
{method "GET"}
{action "/search"}
{fields
[hidden type: hidden name: utf8 value: ✓]
[text type: text name: q value: ]}
...>}
ポッドキャストの話に戻ると、エンコーディング、HTTPレスポンスコード、URI、レスポンスヘッダーなども確認できます。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) puts 'Encodings' puts page.encodings puts 'Response Headers' puts page.response puts 'HTTP response code' puts page.code puts 'URI' puts page.uri
出力例
Encodings
EUC-JP
utf-8
utf-8
Response Headers
{"server"=>"GitHub.com", "date"=>"Sat, 29 Oct 2016 17:56:00 GMT", "content-type"=>"text/html; charset=utf-8", ...}
HTTP response code
200
URI
https://betweenscreens.fm/
さらに深く掘り下げたい場合には他にも多くの情報がありますが、ここではこの程度にとどめておきます。
Nokogiriメソッドの活用
atsearch
Mechanizeは内部的にNokogiriを使用してページからデータをスクレイピングします。したがって、最初の記事で学んだNokogiriの知識は、Mechanizeのページオブジェクトにもそのまま適用できます。つまり、一般的にはページ間の移動にはMechanizeを、実際のスクレイピングにはNokogiriのメソッドを使うことになります。
例えば、単一のオブジェクトを検索したい場合はatを、特定のページ上でセレクターに一致するすべてのオブジェクトを取得したい場合はsearchを使います。言い換えれば、これらのメソッドはNokogiriのドキュメントオブジェクトでもMechanizeのページオブジェクトでも動作するということです。
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
podcast_url = "https://betweenscreens.fm/"
page = agent.get(podcast_url)
first_title = page.at('h2.post-title')
all_titles = page.search('h2.post-title')
all_titles.each do |title|
puts title
end
puts " * "*33
puts first_title
出力例
<h2 class="post-title"><a href="episodes/144/">Randy J. Hunt</a></h2> <h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2> <h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> <h2 class="post-title"><a href="episodes/141/">Zach Holman</a></h2> <h2 class="post-title"><a href="episodes/140/">Joel Glovier</a></h2> <h2 class="post-title"><a href="episodes/139/">João Ferreira</a></h2> <h2 class="post-title"><a href="episodes/138/">Corwin Harrell</a></h2> * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * <h2 class="post-title"><a href="episodes/144/">Randy J. Hunt</a></h2>
リンク操作
linkslink_withlinks_with
サイト全体を思い通りにナビゲートすることもできます。Mechanizeの最も重要な機能は、おそらくリンクを自由に扱える点でしょう。そうでなければ、Nokogiri単体で十分だったはずです。ページにリンクを問い合わせると何が返ってくるのか、見てみましょう。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) puts page.links
大量のリンクが出力されましたね。ひとつずつ分解してみましょう。Mechanizeに他の場所を見るよう指示していないため、最初のページだけからのリンク配列が返されています。Mechanizeはページを上から順に走査し、リンクのリストを返します。
ちなみに、これは私のポッドキャストサイトのリデザイン後の最終結果でもあります。デモ用としてはこのバージョンの方が分かりやすいと思います。最終的な成果物がどんな見た目になるのか、そしてなぜ古いSinatraサイトをスクレイピングする必要があったのか、その理由もなんとなく感じてもらえるでしょう。
いつものように、そこからテキストだけを抽出することもできます。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) page.links.each do |link| puts link.text end
出力例
Logo cube fork! about design code Randy J. Hunt Jason Long David Heinemeier Hansson Zach Holman Joel Glovier João Ferreira Corwin Harrell Older Stuff » Exercise Company benefits Tmux FileTask Decision making Favorite feature Working out Scott Savarie Titles Erik Spiekermann Newbie mistakes Playbook Delegation Heat maps Europe Sizing type Focus Virtual assistants Writing Hacking Joel Glovier Corwin Harrell Mario C. Delgado Tom Dale Obie Fernandez Chad Pytel Zach Holman Max Luster Kyle Fiedler Roberto Machado
リンクをまとめて取得できるのは非常に便利ですが、場合によっては絞り込みが必要です。幸い、必要なものを精密に選び出すためのツールがいくつか用意されています。
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
podcast_url = "https://betweenscreens.fm/"
page = agent.get(podcast_url)
focus_link = agent.page.links.find { |link| link.text == 'Focus' }
puts focus_link
特定の条件(例えばテキストなど)に一致するリンクを探したい場合は、links_withやlink_withといったより使いやすいAPIが利用できます。また、同じFocusというリンクが複数ある場合は、角括弧[]を使ってページ上の特定の番号のものを指定できます。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) focus_link = agent.page.links_with(:text => 'Focus')[2] puts focus_link
リンクのテキストではなくリンク自体を探したい場合は、特定のhrefを指定するだけです。Mechanizeは柔軟に対応してくれます。textの代わりにhrefを渡せばよいのです。
some_scraper.rb
page = agent.page.link_with(href: '/episodes/95/') page = agent.page.links_with(href: '/episodes/95/')
目的のテキストを持つ最初のリンクだけを見つけたい場合は、次のような構文も使えます。とても便利で、少し読みやすくなります。
some_scraper.rb
focus_links = agent.page.link_with(:text => 'Focus')
では、そのFocusリンクの先に何が隠れているのか、実際にたどってみましょう。クリックしてみます!
クリック操作
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
podcast_url = "https://betweenscreens.fm/"
page = agent.get(podcast_url)
focus_links = agent.page.links.find { |link| link.text == 'Focus' }.click.links
puts focus_links
これで先ほどと同じように長いリンクのリストが取得できます。.click.linksを組み合わせるのがいかに簡単か分かりますね。Mechanizeがリンクをクリックし、新しいページへ移動してくれます。さらにリンクのリストを要求しているので、新しいページ上でMechanizeが見つけられるすべてのリンクが返されます。
例えば、同じインタビュー対象者へのテキストリンクが2つあり、1つはタグページへ、もう1つは最近のエピソードへリンクしているとしましょう。それぞれのページからリンクを取得したい場合です。
some_scraper.rb
require 'mechanize' agent = Mechanize.new podcast_url = "https://betweenscreens.fm/" page = agent.get(podcast_url) links = agent.page.links_with(text: "Some interviewee") links.each do |link| puts link.click.links end
これで両方のページのリンクリストが得られます。インタビュー対象者の各リンクを反復処理すると、Mechanizeがクリックされたリンクをたどり、新しいページで見つけたリンクを収集してくれます。以下に、組み合わせの例をいくつか紹介します。始める際の参考にしてください。
some_scraper.rb
agent.page.links.find { |l| l.text == 'Focus' }
agent.page.links.find { |l| l.text == 'Focus' }.click
agent.page.link_with(text: 'Focus')
agent.page.links_with(text: 'Focus')[0]
agent.page.links_with(text: 'Focus')[1].click
agent.page.links_with(text: 'Focus')[2].click.links
agent.page.link_with(href: '/some-href')
agent.page.link_with(href: '/some-href').click
agent.page.links_with(href: '/some-href')
agent.page.links_with(href: '/some-href').click
フォーム操作
submitfield_withcheckbox_withradiobuttons_withfile_uploads
次にフォームを見てみましょう!
some_scraper.rb
require 'mechanize' agent = Mechanize.new google_url = "https://google.com/" page = agent.get(google_url) forms = page.forms puts forms.inspect
出力例
[#<Mechanize::Form
{name "f"}
{method "GET"}
{action "/search"}
{fields
[hidden type: hidden name: ie value: ISO-8859-1]
[hidden type: hidden name: hl value: es]
[hidden type: hidden name: source value: hp]
[hidden type: hidden name: biw value: ]
[hidden type: hidden name: bih value: ]
[text type: name: q value: ]
[hidden type: hidden name: gbv value: 1]}
{radiobuttons}
{checkboxes}
{file_uploads}
{buttons
[submit type: submit name: btnG value: Buscar con Google]
[submit type: submit name: btnI value: Voy a tener suerte]}>
]
formsメソッドを使うと、フォームが1つしかなくても配列が返されます。フォームの名前が"f"であることが分かったので、単数形のformメソッドを使ってそのフォームに絞り込むことができます。
...
{name "f"}
...
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
google_url = "https://google.com/"
page = agent.get(google_url)
search_form = page.form('f')
puts search_form.inspect
form('f')を使うことで、作業対象となる特定のフォームを選び出せました。その結果、配列ではなく単一のフォームオブジェクトが返されます。
テキスト入力フィールドの名前(q)も識別できます。
... [text type: name: q value: ] ...
この名前を指定して、Rubyの属性のように値を設定できます。必要なのは新しい値を代入することだけです。上記の出力例から、デフォルトでは空であることが分かるでしょう。
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
google_url = "https://google.com/"
page = agent.get(google_url)
search_form = page.form('f')
search_form.q = 'New Google Search'
puts search_form.inspect
出力例
#<Mechanize::Form
{name "f"}
{method "GET"}
{action "/search"}
{fields
...
[text type: name: q value: New Google Search]
...}>
ご覧のとおり、テキストフィールドの値がNew Google Searchに変わりました。あとはフォームをsubmitして、Googleが返すページから結果を収集するだけです。これほど簡単なことはありません。今度は別のキーワードで検索してみましょう!
some_scraper.rb
require 'mechanize'
agent = Mechanize.new
google_url = "https://google.com/"
page = agent.get(google_url)
search_form = page.form('f')
search_form.q = 'GitHub TouchFart'
page = agent.submit(search_form)
pp page.search('h3.r').map(&:text)
ここではCSSセレクターh3.rで検索結果の見出しを特定し、そのtextをマッピングして、結果を整形して出力しました。難しくなかったですよね?簡単な例ではありますが、これを使えば無限の可能性が広がると考えてください。
出力例
["GitHub - hungtruong/TouchFart: A fart app for the new Macbook ...", "TouchFart/TouchFart at master · hungtruong/TouchFart · GitHub", "Commits · hungtruong/TouchFart · GitHub", "Projects · hungtruong/TouchFart · GitHub", "Pull Requests · hungtruong/TouchFart · GitHub", "Issues · hungtruong/TouchFart · GitHub", "TouchFart/license.txt at master · hungtruong/TouchFart · GitHub", "Add autoplay attribute to <audio> tag and touchfart (er ... - GitHub", "Find file - File Finder · GitHub", "Fart app for the new Macbook Pro's Touch... #3860 on topic touchfart ..."]
Mechanizeにはさまざまな入力フィールドが用意されており、ファイルのアップロードさえも可能です。
field_withcheckbox_withradiobuttons_withfile_uploads
ラジオボタンやチェックボックスも名前で識別し、予想どおりcheckメソッドでチェックできます。
some_scraper.rb
form.radiobuttons_with(:name => 'gender')[3].check form.checkbox_with(:name => 'coder').check
optionタグは、ドロップダウンリストから項目を1つ選択するために使われます。これも同様に名前で対象を特定し、選択したいオプションの番号を指定します。
some_scraper.rb
form.field_with(:name => 'countries').options[22].select
ファイルアップロードは、フォームにテキストを入力するのと似た方法で、Rubyの属性のように設定します。アップロードフィールドを特定し、転送したいファイルのパス(ファイル名)を指定するだけです。言葉にすると複雑に聞こえますが、実際は簡単です。見てみましょう。
some_scraper.rb
form.file_uploads.first.file_name = "some-path/some-image.jpg"
まとめ
どうでしょうか、魔法など何もないことが分かりましたね!これであなたも自分で楽しく試せる準備が整いました。NokogiriとMechanizeについてはまだ学ぶべきことがいくらかありますが、不必要な部分に時間をかけすぎるよりも、実際に触って遊んでみて、初心者向け記事の範囲を超える問題に直面したときにドキュメントを調べるのがおすすめです。
このGemがいかにシンプルで美しく、どれほどのパワーを提供してくれるか感じてもらえたなら嬉しいです。ご存じのとおり、大きな力には責任が伴います。法的な枠組みの中で、APIが利用できない場合に使うようにしましょう。日常的に使う機会は多くないかもしれませんが、本格的なスクレイピングが必要になったとき、これらのツールは本当に役立つはずです。
-
「Webページがブラウザの速度を低下させています」エラーの解決方法【Firefox】
Firefoxで「Webページがブラウザの速度を低下させています。停止しますか?それとも待機しますか?」というメッセージに悩まされていませんか?これは、ブラウジング中に遭遇する最も一般的なFirefoxのエラーメッセージの一つです。多くのユーザーから、ウェブページを開こうとした際にこの通知が突然表示されるという報告が寄せられています。「停止する」を選択するとエラーメッセージは一旦消えますが、15〜30秒後には再び表示されるという不思議な現象も起こります。 こうした問題に直面すると、デバイスがウイルスやマルウェアに感染していないかと疑うのが自然ですが、今回はそのケースではありません。Mozil
-
ブラウザはどの程度プライベートであるべき?知っておきたいセキュリティとプライバシーの重要性
ブログ概要 – ウェブブラウザを選ぶ際、そのセキュリティやプライバシー機能に注目していますか?実はウェブブラウザも私たちのデータを収集・利用することがあります。この記事では、ブラウザがどの程度プライベートであるべきかを詳しく解説します。 ブラウザはインターネットを閲覧するために欠かせないツールです。多くの人は「怪しいウェブページを見なければ安全だ」と考えています。しかし、ブラウザからの情報漏洩や、収集されたデータがターゲット広告に利用されるというニュースは珍しくありません。では、ウェブブラウザは本当に私たちにとって安全なのでしょうか?本記事で詳しく見ていきましょう。 インターネットの普及当