Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

初めてのWebスクレイパー構築 第2回:Mechanizeでリンク操作・フォーム送信・ファイルアップロードを自動化する

このチュートリアルでは、Mechanizeを使ってリンクのクリック、フォームの入力・送信、ファイルのアップロードを行う方法を学びます。また、Mechanizeのページオブジェクトをスライス(抽出)する方法や、Google検索を自動化して結果を保存する方法も解説します。

扱うトピック

  • 単一ページ vs ページネーション(複数ページ)
  • Mechanize
  • Agent(エージェント)
  • Page(ページオブジェクト)
  • Nokogiriのメソッド
  • リンクの操作
  • クリック(遷移)
  • フォームの操作

単一ページ vs ページネーション

これまでNokogiriを使って単一ページのスクレイピングを行う基礎を学んできました。これは、複数ページからコンテンツを抽出するための良い足掛かりとなります。

今回解決したい問題は、140エピソード以上のコンテンツを取得することです。これらは単一ページに収まりきらないため、ページネーション(ページ送り)を扱い、コンテンツの「奥深く」まで追跡する方法を知る必要があります。

ここでNokogiriの役割は終わり、Mechanizeという強力なGemが登場します。

Mechanizeとは

Mechanizeは、Webサイトとのインタラクションを自動化するための多機能なツールです。Capybaraを使ったテストに似た機能を提供しますが、データ抽出に特化しています。

Nokogiriだけで単一ページを操作するのも素晴らしいですが、より複雑なデータ抽出にはさらなる馬力が必要です。Mechanizeを使えば、必要なだけページをクロールし、リンクのクリック、フォームの送信、Cookieの処理など、人間のブラウジング操作を模倣・自動化できます。非常に強力です!

このGemを使えば、ログインフォームに認証情報を入力し、プライベートなセッション内のコンテンツを取得することも可能です。Aaron Patterson氏もこのGemの作者の一人です。

Mechanize Agentのインスタンス化

Mechanizeを使い始めるには、まずAgentをインスタンス化します。

some_scraper.rb

require 'mechanize'

agent = Mechanize.new

このagentを使ってページを取得します。Nokogiriで行ったのと似ていますが、MechanizeはCookieなども自動で扱ってくれます。

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...>
  ...
 {links
  #<Mechanize::Page::Link "Logo cube" "/">
  #<Mechanize::Page::Link "fork!" "https://github.com/vis-kid/betweenscreens">
  #<Mechanize::Page::Link "about" "pages/about/">
  ...
  #<Mechanize::Page::Link "Older Stuff »" "page/2/">
  ...
 {forms}>

HTMLそのものを見たい場合はbodycontentメソッドを使います。

some_scraper.rb

...

print page.body

...

また、GitHubのようなより複雑なページのMechanize::Pageオブジェクトを見ると、リンク、フォーム、iframeなど多様な要素が含まれていることがわかります。これらを把握しておくと、実際のスクレイピングで役立ちます。

エンコーディング、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のメソッドをMechanizeで使う

  • at
  • search

Mechanizeは内部でNokogiriを使用しており、Nokogiriで学んだメソッドをMechanizeのページオブジェクトにも適用できます。一般的に、ページ遷移などのナビゲーションはMechanizeで、データ抽出はNokogiriのメソッドで行います。

  • at:最初にマッチした単一の要素を取得
  • search:マッチするすべての要素を配列で取得

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/144/">Randy J. Hunt</a></h2>

リンクの操作

  • links
  • link_with
  • links_with

Mechanizeの真骨頂はリンク操作にあります。ページ上のすべてのリンクを配列として取得できます。

some_scraper.rb

require 'mechanize'

agent = Mechanize.new

podcast_url = "https://betweenscreens.fm/"

page = agent.get(podcast_url)

puts "#{page.links}"

出力はページ上部から順に、すべてのリンク(Mechanize::Page::Linkオブジェクト)のリストになります。テキストのみを抽出することも簡単です。

some_scraper.rb

page.links.each do |link|
  puts link.text
end

出力例

Logo cube
fork!
about
design
code
Randy J. Hunt
Jason Long
...
Older Stuff »
Exercise
Company benefits
...

特定のリンクを絞り込むには、findlink_withlinks_withを使います。テキストやhrefでフィルタリング可能です。

some_scraper.rb

# テキストで最初のリンクを取得
focus_link = agent.page.link_with(text: 'Focus')

# テキストでマッチするすべてのリンクから3番目を取得(インデックス2)
focus_link = agent.page.links_with(text: 'Focus')[2]

# hrefで指定
page = agent.page.link_with(href: '/episodes/95/')
page = agent.page.links_with(href: '/episodes/95/')

クリック(ページ遷移)

リンクをクリックして次のページへ遷移するにはclickメソッドを使います。遷移先のページでさらにリンクを取得するなど、チェーンでつなげられます。

some_scraper.rb

require 'mechanize'

agent = Mechanize.new

podcast_url = "https://betweenscreens.fm/"

page = agent.get(podcast_url)

# 'Focus'リンクをクリックし、遷移先のページのリンク一覧を取得
focus_links = agent.page.links.find { |link| link.text == 'Focus' }.click.links

puts focus_links

同一人物への複数のリンク(タグページとエピソードページなど)からそれぞれ遷移してリンクを収集する例:

some_scraper.rb

links = agent.page.links_with(text: "Some interviewee")

links.each do |link|
  puts link.click.links
end

よく使う組み合わせのチートシート:

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

フォームの操作

  • submit
  • field_with
  • checkbox_with
  • radiobuttons_with
  • file_uploads

フォームの送信もMechanizeの得意分野です。Google検索を例に見てみましょう。

some_scraper.rb

require 'mechanize'

agent = Mechanize.new

google_url = "https://google.com/"

page = agent.get(google_url)

forms = page.forms

puts forms.inspect

formsメソッドでフォームの配列が返ります。フォーム名(ここでは"f")がわかればform('f')で特定のフォームを取得できます。

some_scraper.rb

search_form = page.form('f')
puts search_form.inspect

検索入力フィールドの名前がqであることがわかります。このフィールドに値をセットするのはRubyの属性代入と同じ感覚で行えます。

some_scraper.rb

search_form.q = 'New Google Search'
puts search_form.inspect

値がNew Google Searchに変わったことが確認できます。あとはsubmitして結果ページを取得するだけです。

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)

出力例

["GitHub - hungtruong/TouchFart: A fart app for the new Macbook ...",
 "TouchFart/TouchFart at master · hungtruong/TouchFart · GitHub",
 ...]

他にもチェックボックス、ラジオボタン、セレクトボックス、ファイルアップロードなど、さまざまなフォーム要素を操作できます。

チェックボックス・ラジオボタンの操作

form.radiobuttons_with(name: 'gender')[3].check
form.checkbox_with(name: 'coder').check

セレクトボックス(ドロップダウン)の操作

form.field_with(name: 'countries').options[22].select

ファイルアップロード

form.file_uploads.first.file_name = "some-path/some-image.jpg"

まとめ

どうでしょう、魔法のような難しさはありませんね。これで基本的な武器は揃いました。NokogiriとMechanizeにはまだ奥深い機能がありますが、まずは実際に手を動かし、困った時にドキュメントを参照するのが上達の近道です。

このGemがいかにシンプルで強力かお分かりいただけたと思います。しかし「大いなる力には大いなる責任が伴う」の通り、法的な枠組み内で、APIが提供されていない場合の最終手段として使ってください。頻繁に使うツールではないかもしれませんが、本格的なスクレイピングが必要になったとき、これほど心強い味方はいません。

次回は、実際のプロジェクトとして、筆者のポッドキャストサイト(古いSinatra製)からデータをスクレイピングし、新しいMiddlemanサイト(Markdownファイルベース)へ移行する実例を紹介します。日付、エピソード番号、ゲスト名、見出し、サブ見出しなどを抽出します。お楽しみに!

  1. 「Webページがブラウザの速度を低下させています」エラーの解決方法【Firefox】

    Firefoxで「Webページがブラウザの速度を低下させています。停止しますか?それとも待機しますか?」というメッセージに悩まされていませんか?これは、ブラウジング中に遭遇する最も一般的なFirefoxのエラーメッセージの一つです。多くのユーザーから、ウェブページを開こうとした際にこの通知が突然表示されるという報告が寄せられています。「停止する」を選択するとエラーメッセージは一旦消えますが、15〜30秒後には再び表示されるという不思議な現象も起こります。 こうした問題に直面すると、デバイスがウイルスやマルウェアに感染していないかと疑うのが自然ですが、今回はそのケースではありません。Mozil

  2. ブラウザはどの程度プライベートであるべき?知っておきたいセキュリティとプライバシーの重要性

    ブログ概要 – ウェブブラウザを選ぶ際、そのセキュリティやプライバシー機能に注目していますか?実はウェブブラウザも私たちのデータを収集・利用することがあります。この記事では、ブラウザがどの程度プライベートであるべきかを詳しく解説します。 ブラウザはインターネットを閲覧するために欠かせないツールです。多くの人は「怪しいウェブページを見なければ安全だ」と考えています。しかし、ブラウザからの情報漏洩や、収集されたデータがターゲット広告に利用されるというニュースは珍しくありません。では、ウェブブラウザは本当に私たちにとって安全なのでしょうか?本記事で詳しく見ていきましょう。 インターネットの普及当