きたももんががきたん。

Ruby の WWW::Mechanize の説明はじめました

2008-03-28 RubyのWWW::Mechanizeを解説してみる

RubyのWWW::Mechanizeを解説してみる

対応バージョン:Mechanize 0.7.7 (2008-07-25)

まにゅあるめにゅー

本家マニュアル(英語)
http://mechanize.rubyforge.org/mechanize/

クリックで各ページに飛びます

Webブラウザとしての挙動一般
WWW::MechanizeWebブラウザ本体の作成と設定
WWW::Mechanize::HistoryブラウザがアクセスしたURLとファイル内容を保持
WWW::Mechanize::CookieJarクッキー管理のすべてを扱う
WWW::Mechanize::Cookieクッキーを自力で作る
HTML上のフォーム
WWW::Mechanize::FormHTMLの<form>タグと内部部品管理とsubmit
WWW::Mechanize::Form::Field各種1行入力欄と複数行入力ボックス
WWW::Mechanize::Form::Buttonフォームの中のsubmitでないボタン
WWW::Mechanize::Form::ImageButton画像つきボタンまたはサーバーサイドクリックマップ
WWW::Mechanize::Form::CheckBoxチェックボックスのグループ管理
WWW::Mechanize::Form::RadioButtonラジオボタンのグループ管理
WWW::Mechanize::Form::SelectList行選択リストまたはドロップダウンメニュー
WWW::Mechanize::Form::MultiSelectList複数選択可能な選択リスト
WWW::Mechanize::Form::Optionチェックボックスやラジオボタンの部品1個そのもの
WWW::Mechanize::Form::FileUploadアップロード欄にファイルを指定します
HTMLのリンク・各種要素
WWW::Mechanize::Page取得したHTMLファイルを解析した基本オブジェクト
WWW::Mechanize::Page::LinkHTMLの<a>タグの保持と利用、またはイメージマップ
WWW::Mechanize::Page::MetaHTMLの<meta>タグのrefresh用データのみ保持
WWW::Mechanize::Page::FrameHTMLの<frame>タグを保持
WWW::Mechanize::Page::BaseHTMLの<base>タグを保持
WWW::Mechanize::Headersファイル取得時のHTTPヘッダを保管
HTML以外
WWW::Mechanize::FileHTMLではないファイルを取得したオブジェクト
WWW::Mechanize::FileSaverファイルを取得次第ディスクに保存します
便利機能
WWW::Mechanize::ListMechanize全般で多用される配列っぽい便利な何か
WWW::Mechanize::PluggableParserContent-TypeごとにMechanizeの処理を指定可能
エラー用例外
WWW::Mechanize::ContentTypeErrorサーバからのContentTypeが不正です
WWW::Mechanize::ResponseCodeErrorサーバアクセスで404や500が返ったとき使う
WWW::Mechanize::UnsupportedSchemeErrorhttp://系以外のリンクを踏んだときに

かいせつめにゅー

クリックで各項目に移動します

WWW::Mechanize の仕組みてきとう解説

  1. Web アクセス用のクライアントオブジェクト作成ー
  2. URL に Net::HTTP でアクセスしてファイルをげっとー
  3. Hpricot に渡して HTML 成分を解析ー
  4. リンクやフォームといったものをオブジェクトにして専用の配列で保持ー
  5. 入力欄オブジェクトに値を「入れた」りリンクオブジェクトを「踏んだ」ことにしてサーバに送るー
  6. 2. に戻るー

こんな感じのことができる rubygems ライブラリが WWW::Mechanize です。 標準ライブラリの Net::HTTP や open-uri とは違い、

  • URI単位のアクセス履歴の管理(単純な If-Modified-Since による未更新操作)
  • 完全に自動なクッキーの遣り取り(ファイルへの保存・読み込みもサポート)
  • Basic 認証と Digest 認証を自動でやってくれる(401 Unauthorized 時に設定持って再アクセス)
  • Content-Encoding: gzip をデフォルトで使用し自動で展開する
  • 302 Found 系の Location: リダイレクトや <meta> タグによるリフレッシュの追随(の可否設定)
  • HTML 上のフォームを解析して入力欄を埋めたりチェックしたりして submit する
  • HTML 上のリンクを抽出して URL やテキストを指定してアクセスする
  • リンクでもフォームでもないHTML要素はHpricotのオブジェクトからユーザが引き出して使用

というようなことが普通にできます。
多機能の代償として起動が若干重いので、「特定のファイルを open-uri で毎日1つ取ってきて専用の正規表現でテキストを抜き出して一覧にする」というような既存のスクリプトを置き換えようとするにはややオーバーかもしれません。

他の言語にも同名のライブラリがありますが、直接的な移植の関係にはありません(動作は全く違います)。…というか、ああいうの Ruby でも使えたらいいなと思って Ruby で書いてみた、という位置付けなんだと思われます。

Hpricot と Watir

HTML の基本的な構造を理解していると、フォームやリンクの推定が楽になります。
内部で使用されている Hpricot という HTML をパースするライブラリの知識があると、フォームやリンク以外のHTMLの要素を利用することができます。…逆に言うと、フォームやリンク以外の HTML は Mechanize の担当ではありません。

  • Webアクセスに必要な<form>タグや<a>タグ …… Mechanize本体の変数で便利に利用
  • Webアクセスには不要なその他のHTML …… 内部にHpricotオブジェクトで保持しユーザに丸曲げ

というように、かなり割り切った分担が為されています。しかし、それゆえ Hpricot を全く知らないと「Google で検索させて結果を受け取るスクリプトはすぐ書けたが結果のタイトル一覧をうまく抽出する方法がよーわからん」という悲しい事態に陥ります(ただし、HTML 自体から正規表現で抽出するという行為はいつでも有効です)。

Mechanize の Page オブジェクトには root メソッドがあり、これで Hpricot のオブジェクトにアクセスできます。

require 'rubygems'
require 'mechanize'
require 'kconv'
# ここからMechanize
agent = WWW::Mechanize.new
agent.user_agent_alias = 'Windows IE 7'
search_top = agent.get(URI.parse('http://www.google.co.jp/'))
search_top.forms[0].fields.name('q').value = 'Ruby'
result_page = search_top.forms[0].submit
# rootの返り値に対してHpricotのメソッドを使用
results = result_page.root.search('a.l').map{|e| e.inner_text}
# 正規表現で書くとこんな感じ?
# results = result_page.body.scan(/<a .+? class=l .+?>(.+?)<\/a>/).map{|e| e[0].gsub(/<.+?>/){}}
puts results.join("\n").toeuc

結果:

オブジェクト指向スクリプト言語 Ruby
Rubyリファレンスマニュアル - Rubyリファレンスマニュアル
Ruby - Wikipedia
逆引きRuby - 逆引きRuby
Rubyソースコード完全解説
日本 Ruby 会議 2008 - FrontPage
TechCrunch Japanese アーカイブ  Twitter、Ruby on Railsを放棄か
Part3 一目でわかるRuby on Rails:ITpro
Rubyアソシエーション
Ruby (Japanese)

Mechanize は HTML をパースした Hpricot オブジェクトを捨てずに内部で保持してるので、Mechanize 自体に専用操作メソッドが無くてもけっこうなんとかなります。たとえば、操作可能なフォームの一覧を返す forms や <a> タグを抽出した結果の links のように、「画像一覧を返すメソッド」というものは Mechaznie には存在しませんが、Hpricot オブジェクトで <img> タグを全検索させれば同じようなことはできるでしょう。

「ちょっと応用してみる」程度ではできないこととしては

  • JavaScript や ActiveX/Flash などを解釈してアクセスする

これ、全然できません。テキストファイルやバイナリファイルとして自前で解析して計算してアクセスしたりすればできなくもないような気もしますが、それは既にライブラリがどうとか関係ないレベルのような気がします。

なお、Watir という rubygem 提供ライブラリを用いると IE(や特殊アドオン入りFirefox)を操作することができるそうなので、複雑な JavaScript を解釈して欲しい場合はご検討ください。

Watir は Mechanize に輪をかけて動作重いのと、個々人の IE のセキュリティや操作の設定の違いを乗り越えにくいのでスクリプトを公開しにくいのが困った点ですが…。



いんすとーるー

rubygems で提供されているので、 rubygems をインストールした上で gem install mechanize でインストールしてください。

mechanize のアーカイブファイルそのものは、以下の URL から入手できます。
http://rubyforge.org/frs/?group_id=1453

Hpricot など色々なものに依存してるので、gem install の使えない環境の人は適当に順番にインストールしましょう。
手作業だと依存関係で色々要求されて探すのめんどいなアンボイナという場合はgem install とするとハングアップするので手作業で入れるテストを参照してください。たとえば、hoe のアーカイブは http://rubyforge.org/projects/seattlerb に、rubyforge というファイルのアーカイブは http://rubyforge.org/projects/codeforpeople にあります。



Web アクセス用のクライアントオブジェクト作成ー
require 'rubygems'
require 'mechanize'
# Web アクセス用のクライアントオブジェクト作成(だけ)
agent = WWW::Mechanize::new
# 寂しいのでオブジェクトを p で表示
p agent

結果:

#<WWW::Mechanize:0x40930bbc @keep_alive=true, @digest=nil, @verify_callback=nil, @conditional_requests=true, 
@pluggable_parser=#<WWW::Mechanize::PluggableParser:0x40930b44 @default=WWW::Mechanize::File,  ... >

WWW::Mechanize のインスタンス(オブジェクト)はいわゆる「Webブラウザ」として振舞います。

  • get メソッドに URL を渡されると、サーバにアクセスして結果を取得
  • サーバと Cookie を自動で遣り取りする
  • フォームに入力されたデータを GET や POST でサーバに送る
  • リンクをクリックされたとみなされたらその URL にアクセスする
  • get した URL の履歴を管理する

ということができます。

agent という変数名にするのが一般的です(mech という変数名も根強く人気です)。
(Webブラウザの吐く文字列という意味ではなく)本当の意味でのユーザーエージェントの agent だと思われます。

以降の説明、またはマニュアル訳してみたテキストでは、agent という変数を説明なしに使用します。
「あー、agent = WWW::mechanize.new した結果のアレかー」と思ってください。



ふぁいるげっとー & HTML成分を解析ー

Mechanize の get メソッドに URL の文字列、または URI オブジェクトを渡すと、サーバにアクセスしてファイルを取得して解析して返します。

p メソッドで解析結果のオブジェクトを表示するとインスタンス変数その他が大量で悲しいことになるので、title メソッドで <title> タグの中身を表示させてみます。

require 'rubygems'
require 'mechanize'
# Web アクセス用のクライアントオブジェクト作成
agent = WWW::Mechanize::new
# 変数 page に、www.ruby-lang.org の /en/ を取得しHpricotで解析し登録された結果が入る
page = agent.get('http://www.ruby-lang.org/en/')
# 解析結果のクラスの title メソッドは HTML のタイトルを返す 
puts page.title
結果:
Ruby Programming Language

返される「げっとしたファイル」はその内容によってクラスが違い、

HTMLだった
WWW::Mechanize::Page クラスのオブジェクト
HTML以外の(Hpricotでパースできない)ファイルだった
WWW::Mechanize::File クラスのオブジェクト

ということになっています。

require 'rubygems'
require 'mechanize'
agent = WWW::Mechanize::new

# HTML なので WWW::Mechanize::Pageクラスのオブジェクトが返ってる
page = agent.get('http://www.ruby-lang.org/ja/')
# Page クラスの uri メソッドはその「ページ」の URI を返す
puts "#{page.uri} => #{page.class}"

# 画像なので WWW::Mechanize::Fileクラスのオブジェクトが返ってる
# agent は URL を渡されるたびに「普通」にアクセスを繰り返す
page = agent.get('http://www.ruby-lang.org/images/logo.gif')
# File クラスの uri メソッドは以下同文…というか逆で、Page が File を継承している
puts "#{page.uri} => #{page.class}"
結果:
http://www.ruby-lang.org/ja/ => WWW::Mechanize::Page
http://www.ruby-lang.org/images/logo.gif => WWW::Mechanize::File

しかし、実際は区別が面倒なので、サーバから返ってきたデータは一律「ページ」と呼ぶことが多いです(主にこのサイトの文章で)。
HTMLであった場合は Hpricot ライブラリに渡され、HTML の要素が解析され、フォームやリンクがあれば専用のインスタンス変数に格納されます。Hpricot のオブジェクト自体も保持されており、page.root などで利用できます。

require 'rubygems'
require 'mechanize'
agent = WWW::Mechanize::new

page = agent.get('http://www.ruby-lang.org/ja/')
p page.root.class
結果:
Hpricot::Doc

なお、上記のように、agent.get 自体の返す Page/File オブジェクトを変数に保存して利用するのではなく、agent.get した後に agent.page という形で Page/File オブジェクトを利用する方法もあります。この場合は、agent が「直前」にアクセスしたページの結果のオブジェクトが返されます。

以降の説明、またはマニュアル訳してみたテキストでは、agent.page というメソッドを説明なしに使用することがあります。
「あー、agent = WWW::mechanize.new して何か HTML を get した結果のアレかー」と思ってください。



Memo:

過去のバージョンにおける、引数にURL文字列を指定するときのバグについて

WWW::Mechanize 0.7.6 およびそれより前のバージョンには、「http://ja.wikipedia.org/wiki/%E3%83%8D%E3%82%B3といった%つきURL文字列をgetする(あるいはclickさせる)とうまく動作しない」という不具合があります。0.7.7 かそれ以降へバージョンアップするか、URIオブジェクトにしたものを渡す(か、clickさせるのではなくそのURLをURIオブジェクトにしてgetする)ようにして回避してください。

:omeM



リンクやフォームを専用の配列で保持ー

「HTML で最初に書いてあったフォームは配列にすると 0要素目ってことだから agent.page.forms[0] でよろしく」

という感じになります。リンクやフォームといった HTML の要素は、わかりやすい(はずの)変数名でWWW::Mechanize::List クラスのオブジェクトとして格納されています。

この WWW::Mechanize::List は Array を継承していて、

# そのページの HTML に含まれる <form> を集めたもの
page.forms = [
  <form name='form1'>〜</form>というフォームなオブジェクト,
  <form name='form2'>〜</form>というフォームなオブジェクト,
  <form name='mail'>〜</form>というフォームなオブジェクト]

とか

# そのページの HTML の 上から1番目の <form>〜</form> 内に含まれる入力欄を集めたもの
page.forms[0].fields = [
  <input type='text' name='address'>という入力欄なオブジェクト,
  <input type='text' name='name'>という入力欄なオブジェクト]

とかいう構造になってます。

  • 配列なので n 番目の要素とみなして Ruby 標準的に forms[0] とアクセス
  • 属性 name で区別できるので forms.name('form1') とアクセス

といった方法で中のオブジェクトを取り出します。

require 'rubygems'
require 'mechanize'
agent = WWW::Mechanize::new
page = agent.get('http://www.ruby-lang.org/ja/')
# Ruby公式ページにある最初(で最後)のフォームを取り出す
searchform = page.forms[0]
# フォームのACTION属性(問い合わせのURL)の内容を action メソッドで得る
p searchform.action
結果:
"/ja/search/"

上記の "/ja/search/" は、実際の HTML の 215 行目あたりで

    <form id="search-form" action="/ja/search/">
          <table class="fieldset">
            <tr>
              <td>
                <input class="field" type="text" name="q" />
              </td>
    <td>
                <input class="button" type="submit" value="Search" />
              </td>
            </tr>
          </table>
        </form>

と記述されている <form> を解析した結果になっています。



Memo:

Mechanize のログの表示法

WWW::Mechanize を利用してスクリプトを作ってると、「Mechanize の中の人はどういう動作をしてるんだろう」「具体的に何をサーバに送ってるんだろう」と気になることがあります。そういう場合は、agent.log に Logger オブジェクトを指定してください。Mechanize の通信ログや動作のログが Logger に渡されます。

require 'rubygems'
require 'mechanize'
require 'logger' # 注目
agent = WWW::Mechanize::new
agent.log = Logger.new($stdout) # 注目
page = agent.get('http://www.ruby-lang.org/')

端折った結果

I, [2008-06-20T06:19:06.836389 #2466]  INFO -- : Net::HTTP::Get: /
D, [2008-06-20T06:19:06.972887 #2466] DEBUG -- : request-header: user-agent => WWW-Mechanize/0.7.6 (http://rubyforge.org/projects/mechanize/)
D, [2008-06-20T06:19:07.054984 #2466] DEBUG -- : Read 35 bytes
D, [2008-06-20T06:19:07.058916 #2466] DEBUG -- : response-header: content-type => text/html
D, [2008-06-20T06:19:07.064641 #2466] DEBUG -- : response-header: location => http://www.ruby-lang.org/en/
I, [2008-06-20T06:19:07.066806 #2466]  INFO -- : status: 302
I, [2008-06-20T06:19:07.068004 #2466]  INFO -- : follow redirect to: http://www.ruby-lang.org/en/
I, [2008-06-20T06:19:07.070072 #2466]  INFO -- : Net::HTTP::Get: /en/
D, [2008-06-20T06:19:07.076448 #2466] DEBUG -- : request-header: user-agent => WWW-Mechanize/0.7.6 (http://rubyforge.org/projects/mechanize/)
D, [2008-06-20T06:19:07.077635 #2466] DEBUG -- : request-header: referer => http://www.ruby-lang.org/
D, [2008-06-20T06:19:07.158024 #2466] DEBUG -- : Read 725 bytes
D, [2008-06-20T06:19:07.533932 #2466] DEBUG -- : Read 12928 bytes
D, [2008-06-20T06:19:07.535568 #2466] DEBUG -- : response-header: connection => Keep-Alive
D, [2008-06-20T06:19:07.536702 #2466] DEBUG -- : response-header: content-type => text/html;charset=utf-8
I, [2008-06-20T06:19:07.543429 #2466]  INFO -- : status: 200

普通に get が行われただけに見えて、実は http://www.ruby-lang.org/ から http://www.ruby-lang.org/en/ にリダイレクトされているということがわかります。POST したときに実際に使われるデータなども表示されるので、活用してください。

なお、agent.log = Logger.new($stdout) の部分を agent.log = Logger.new('./mechlog.txt') とすると、カレントディレクトリの mechlog.txt というファイルにログが出力されます。INFO なログだけあれば十分な場合は agent.log.level = Logger::INFO を追加してください。

というような Logger の使用法については るびま 8号の Logger 解説などを参照のこと。

:omeM



入力欄に値を「入れた」りリンクを「踏んだ」ことにしてサーバに送るー

フォームに入力してみる

  1. 「ページの持つフォーム一覧のList」から目的の「フォームオブジェクト」をnameで探すか配列の要素指定で抽出
  2. その「フォームの持つ部品種類ごとのList」から目的の「"部品"オブジェクト」をnameで探すか配列の要素指定で抽出
  3. その"部品"オブジェクト固有のメソッドを利用して値を実際に入力したりチェックしたりする

という流れになります。なんか面倒そうですが、メソッドチェーンで記述するとわりとそのまんまです。

例1:「<form name='form1'> の中の <input type='text' name='address'> という入力欄の内容にJapanと入力」

agent = WWW::Mechanize::new
page = agent.get('http://www.example.com/')
# form1 という名前のフォームオブジェクトを page の forms というリストから抽出
form = page.forms.name('form1')
# address という名前の入力欄の"部品"オブジェクトを form のfieldsというリストから抽出
field = form.fields.name('address')
# 入力欄の value に Japan という文字列を入力するには入力欄オブジェクトのvalueメソッドを使う
field.value = 'Japan'
# いちいちめんどくさいのでメソッドチェーンで一気に書く
# page中でnameがform1なフォームの中の入力欄fieldの中のnameがaddressなやつのvalueをJapanに
page.forms.name('form1').fields.name('address').value = 'Japan'

例2:「HTML 上で 2 番目の <form> の中にあるチェックボックスの 3 つ目をチェックする」

agent = WWW::Mechanize::new
page = agent.get('http://www.example.com/')
page.forms[1].checkboxes[2].check # 配列なので添え字は 0から始まる

fields や checkboxes など、フォームの部品自体がどんなインスタンス変数に格納されているかは WWW::Mechanize::Form を参照してください。

これより短く書ける便利なメソッドがいくつも用意されていますが、「単に配列とみなして find や each で力技で抽出」ということももちろん可能です。
古いバージョンの WWW::Mechanize をもとにした解説では Array#find を使っているものがあるようです。まあどっちでもいいんですがせっかくなので name メソッドを使いましょう。

# 以下の2つは(該当するformや入力欄が存在する限り)同じもの
page.forms.name('form1').fields.name('address')

page.forms.find{|form| form.name == 'form1'}.find{|field| field.name == 'address'}


リンクをクリックしてみる

前述のフォームと構造はほぼ同じです。

  1. 「ページの持つリンク一覧のList」から目的の「リンクオブジェクト」を属性で探すか配列の要素指定で抽出
  2. そのリンクオブジェクト固有のメソッドを利用して「クリック」したことにさせる

詳しくは WWW::Mechanize::Page の links メソッドを参照してください。

click メソッドは、サーバにアクセスした結果のページのオブジェクトを(agent.get と同じように)返します。

require 'rubygems'
require 'mechanize'
require 'kconv'
## YOMIURI ONLINE の科学トピックスのトップ要約記事のリンクをたどって記事本文を取得する
agent = WWW::Mechanize::new
# ボクインターネットエクスプローラーダヨアンシンダネ
agent.user_agent_alias = 'Windows IE 7'
# YOMIURI ONLINE の科学トピックスのトップページ
uri = URI.parse('http://www.yomiuri.co.jp/science/')
page = agent.get(uri)
# トップ記事にある「[全文へ]」と書かれたリンクを抽出
# この手の文字コード関連は様々な理由で動作しないことがあるので注意
link = page.links.text('[全文へ]'.tosjis)
# リンクオブジェクトのclickメソッドはリンク先URLをagent.getするに等しい
whole_article = link.click
# 変数名考えるのめんどいのでメソッドチェーンで1行で記述
# agent.page.links.text('[全文へ]'.toeuc).click
# 記事タイトルを表示(文字コードはShift_JIS)
puts whole_article.title

結果:

原発の地震対策、IAEAが柏崎市で会議 : 科学 : YOMIURI ONLINE(読売新聞)


サーバにアクセスする

Web サーバにアクセスする代表的な方法は以下のとおりです。agent = WWW::Mechanize.new したあとだとお考えください。
Form の submit メソッドは、そのフォームの現時点でのデータをサーバに送ります。

すべて、サーバにアクセスした結果のページのオブジェクトを返します。

URL を直接指定してファイルを(GET で)取得する
agent.get('http://www.example.com/')
URL と GET のパラメータを直接指定してファイルを取得する
agent.get('http://www.example.com/search.cgi', {'q'=>'Ruby','num'=>'50'})
URL と POST のデータを直接指定してファイルを取得する
agent.post('http://www.example.com/post.cgi', {'text'=>'hogehoge'})
リンクの入ったオブジェクトの click メソッドを直接使う
agent.page.links.href('page1.html').click
フォームのオブジェクトの submit メソッドを直接使う
agent.page.forms[0].submit
リンクの入ったオブジェクトを agent にクリックさせる
agent.click(agent.page.links.href('page1.html'))
フォームのオブジェクトを agent に渡して submit させる
agent.submit(agent.page.forms[0]):


マニュアルで引っかかった疑問のコーナー


別に agent や page っていうローカル変数に入れなくてもいいよね?

agent の代わりに ua や mech というのも時々見ます。

page = agent.get('http://www.example.com/')
puts page.body

agent.get('http://www.example.com/')
puts agent.page.body

が同じであるという話は、まあ、便利に感じるほうをその時々で使い分ければよろしいのではないかと思われます。
agent.max_history = 0 として履歴数をゼロにしている場合は後者は動作しないので注意してください。

page.forms[0] の 0って何よ

配列の添え字の0です。WWW::Mechanize::Pageクラスのオブジェクトである page は、HTML に記述されている<form> タグ を配列(を継承したオブジェクト)として forms というインスタンス変数に保持しています。

たとえば HTML が

<html><title>てすと</title><body>
<form name='addr_data' action='./register.cgi'>
<input type='text' name='address'>
</form>

<form name='upform' action='./uploader.cgi'>
<input type='file' name='upload'>
</form>

<form name='gencheck' action='./gensearch.cgi'>
<input type='radio' name='gender' value='male'><input type='radio' name='gender' value='female'></form>
</body></html>

だった場合、
page.forms[0] は <form name='addr_data' action='./register.cgi'> のフォームを、
page.forms[1] は <form name='upform' action='./uploader.cgi'> のフォームを、
page.form[2] は <form name='gencheck' action='./gensearch.cgi'> のフォームを指しています。

もちろん、配列以外の理由で選択することもできます。これらの <form> には name 属性がついてるので page.forms.name('addr_data') などとしてください。これは page.forms[0] と同様に <form name='addr_data' action='./register.cgi'> のフォームを指しています。

q とか見慣れないメソッドがサンプルにあることがある

WWW::Mechanize では method_missing が妙に活用されてます。

WWW::Mechanize::Form においては、

page.forms[0].fields.name('q').value = 'Ruby'

と value をいじる代わりに

page.forms[0].q = 'Ruby'

と短く書くことができます。

fields 以外の checkboxes などでは使えず、value の読み込みや設定以外では使えません。

page.forms[0].fields の中に name='q' となっているオブジェクトが複数あった場合は、page.forms[0].q はそのうち最初のオブジェクトの value が返されます。

なお、ページのログインフォームなどの <input name = 'name'> を利用しようとして page.forms[0].name と書いた場合、method_missing で判断される前に本来の name メソッドとみなされて Mechanize はエラーを起こします。イマイチ頼りないです。

配列(みたいなの)が返ってるはずなのに click とか書かれてて俺涙目

Array を継承している WWW::Mechanize::List オブジェクトに適当なメソッドを与えると、「List の先頭にあるオブジェクトにそのメソッドがある」ものとして実行されます(つまり send(method) )

以下の2行のペアは同じことを表しています。

page.links.click
page.links[0].click
page.forms[1].fields.value = 'Ruby'
page.forms[1].fields[0].value = 'Ruby'
page.forms[2].fields.name('search').value = 'Ruby'
page.forms[2].fields.name('search')[0].value = 'Ruby'

hoge(param = nil) って何? 引数に = が必要?

hoge(param = nil) のように引数が「 = ナントカ」と = つきで表記されてるメソッドは、その引数を省略することができます(メソッド定義式の表記をそのまま流用しています)。
この場合は hoge() や hoge と書くことが可能で、省略された場合、メソッド内部において param は nil として扱われます。

hoge(url, param = nil) という表記の場合は、 url は省略することができませんが param は省略することができます。

もちろん、引数に渡すローカル変数の名前が param という名前である必要はありません。

agent.post('http://example.com/', 'q' => 'hogehoge') で => が浮いてる

Rubyでは、メソッドの引数ではハッシュの { } を省略することができます。
いわゆるキーワード引数のように振舞うことを期待しているようです。

以下の2行のペアは同じことを表しています。

agent.post('http://example.com/', "q" => 'hogehoge', 'num' => '50')
agent.post('http://example.com/', {"q" => "hogehoge", 'num' => '50'})

[](field_name) や []=(field_name, value) って何者?

[](field_name) は hoge[field_name] のことで、
[]=(field_name, value) は hoge[field_name] = value のことです。

ハッシュと同じように名前でアクセスできることを表しています。

もしかして履歴って無限?

無限です。しかも @body 変数にファイル内容を丸まんま保持したまま延々メモリ内に積み重なっていきます。メモリの空きが無くなるか、その WWW::Mechanize オブジェクトが終了するまで続きます。

agent.max_history = 1 とすると、動作に必要な最低限の履歴が確保できます。履歴に関する機能を自力で使わないのならこれで充分だと思われます。

wiki とかの URL に % の入ったページを get したら結果が変

WWW::Mechanize 0.7.6 のバグです。0.7.7 で解消されています。
アップグレードできない事情がある場合はRubyのMechanizeではパーセントつきURL文字列を処理できない を参照して、不審な動作のメソッド自体を上書きするなどしてください。

404 のときとかどうすんの?

200 と 301 と 302 以外のコードが返ってきた場合は、WWW::Mechanize::ResponseCodeError が発生するので、適当に rescue したりログ残したりしてください。

agent = WWW::Mechanize.new
uri = URI.parse('http://www.example.com/404error.html')
begin
  agent.get(uri)
rescue TimeoutError # Mechanize内部で処理されてて出てこないようにも見える
  agent.log.info("access timeout: #{uri}") if agent.log
  warn '接続自体がタイムアウトしました'
rescue WWW::Mechanize::ResponseCodeError => ex
  agent.log.info("#{ex.message}: #{uri}") if agent.log
  case ex.response_code
  when '404' then
    warn "404: #{uri} はサーバー上に存在しません"
  else
    warn ex.message
  end
end

301 Moved Permanently と 302 Found の場合は、Location ヘッダに従って自動的にリダイレクト先 URL を取得します。
履歴には「Location: を返してきた URL」で「リダイレクト先」のページ内容が登録されます。

forms や links があるなら images とかないの?

無いですが、Page クラスの root メソッドで Hpricot::Doc オブジェクトが利用できるので、画像の URL を抜き出すことくらいはできます。

require 'rubygems'
require 'mechanize'

agent = WWW::Mechanize.new
agent.user_agent_alias = 'Windows IE 7'
# Yahoo!映画のインタビュー一覧ページ(写真つき)
uri = URI.parse('http://movies.yahoo.co.jp/interview/')
agent.get(uri)
# searchメソッドでimgタグを検索して、結果の配列をsrc属性の中身で置き換える
srcs = agent.page.root.search('img').map{|e| e['src']}
# インタビュー写真特有のURL(/pict/interview/ から始まる)を抜き出して表示
puts srcs.find_all{|e| /\A\/pict\/interview\// =~ e}
# こっそりダウンロードコーナー(実行する場合は=beginと=endを取ってね)
# カレントディレクトリにJPEGファイルが10個ほど生成されるので注意
=begin
srcs.find_all{|e| /\A\/pict\/interview\// =~ e}.each do |url|
  # 画像アクセス時のリファラ(getの第2引数)をトップページに固定
  # 引数処理で混乱するのでPageオブジェクトを指定するのが無難
  # こうしないと1番目の画像URLをリファラにして2番目の画像を取りに行ってしまって不自然
  # まあある意味パラノイアさん用処理ではある
  # getの引数が相対URLや絶対パスだった場合、agent.pageやリファラのURLがベースURLとして使われる
  agent.get(URI.parse(url), agent.visited_page(uri))
  # 無引数のsaveメソッドはカレントディレクトリに「URL末尾のファイル名」で保存する
  agent.page.save
end
=end

メソッド追加しようと思ったら File.read とかが動かない

WWW::Mechanize クラスの中から見た場合、File クラスは WWW::Mechanize::File クラスになります。

::File.read(path) などとしてください。組み込みの「普通の」File クラスが利用できます。

あと、File.read は Windows 環境でバイナリファイルをバイナリとして扱うときにがっかりな動作になる(バイナリモードが設定されていない)ので公開や頒布には気をつけましょう。

インスタンス変数っていうかアクセサだよねこれ

あんま細かいこと気にすると禿げます

逆引き

かいせつ
  • 直前にagentがgetやclick等で得たページはagent.pageでも返る(変数名考えずに済んで楽)
  • 基本は「ページの中のフォームやリンク、の中の特定の名前をもつもの、の中の特定部品、の中で特定のデータに合致するもの、を、クリックor設定」というメソッドチェーンな流れ
  • 「Listで返る」場合はagent.page.forms.name('myform').checkboxes.name('hoge').clickという適当メソッドチェーンが使用可能
  • ページのPageオブジェクトやLink、Formなどは自分の取得に使われたagentを知ってて再利用している
  • アクセス用変数はフォームとリンクしか無いので他の画像やテーブルや段落の解析抽出はHpricotや正規表現で
  • JavaScriptは読まないので自力で必死に解析すること
  • 以下のスクリプトを実行済みとする
require 'rubygems'
require 'mechanize'
# uriは文字列でも動作はするがパーセントエンコードが入るとうまく動かない
uri = URI.parse('http://www.example.com/')
agent = WWW::Mechanize::new
page = agent.get(uri)
URLにアクセスする
uri にアクセスしてページオブジェクトとして返す
agent.get(uri)
uri のファイル自体の中身を表示
puts agent.get(uri).body
uri にアクセスした際の HTTP ヘッダが気になる
p agent.get(uri).header
uri に ?key1=hoge&key2=ねこ を GET で送る
agent.get(uri, {'key1' => 'hoge', 'key2' => 'ねこ'})
マルチバイト文字はいわゆる URL エンコードされて送られる
ページ内のHTMLのリンクをどうにかする
page内HTMLにある <a href="./hoge.html"> を探してアクセスして返す
page.links.href('./hoge.html').click
page内HTMLにある <a href="...">続きを読む</a> にアクセスして返す
agent.page.links.text('続きを読む').click
日本語指定はHTMLの文字エンコードと一致させること
page内HTMLにあるJPEGファイルへのリンクオブジェクトだけ抽出しListに
agent.page.links.href(/\.jpg\Z/)
page内HTMLにある <a class="extlink"> を探してURIの配列に
page.root.search('a#extlink'){|e| URI.parse(e.href)}
ページ内のHTMLのフォームをどうにかする
<form name='f1'> ... </form>を選択する
page.forms.name('f1') (か、 page.form('f1')
HTML 上の n 個目の <form> ... </form> を選択する
page.forms[n-1]
HTML 上で1個目のフォームの 'search' という名前のテキストボックスに 'Ruby' と入れる
page.forms[0].fields.name('search').value = 'Ruby'(か、page.forms[0].search = 'Ruby'
titleという欄に「たいとる」、authorという欄に「ちょしゃ」と連続入力
page.forms[0].set_fields({'title' => 'たいとる', 'author' => 'ちょしゃ'})
<input type="checkbox" name="c1" ...>というチェックボックスをチェック状態に
page.form[0].checkboxes.name('c1').check
<input type="checkbox" value="data1" ...>というチェックボックスをチェックしない状態に
page.form[0].checkboxes.value('data1').uncheck
ディスクにあるhoge.jpgを<input type="file" name="upfile">なフォームでアップロード
page.forms[0].file_uploads.name('upfile').file_name = './hoge.jpg'
<select name="selection" multiple>な選択リストでvalue=がoneとthreeのを選択
page.forms[0].fields.name('selection') = ['one', 'three']
他の選択肢のチェックは自動で全部外れる
<input type="radio" value="male"> なラジオボタンをチェック
page.forms[0].radiobuttons.value('male').check
HTML 上の1個目のフォームにおいて、現在設定されている全データをサーバに送信して結果を返す
page.form[0].submit
ページ内のHTMLの要素をどうにかする
<h2>を全部抜き出し、タグに囲まれた内部の文(タグとか無視)を配列で返す
page.root.search('h2').map{|e| e.inner_text}
<p class=""description">を最初のひとつだけ探し、囲まれた内部の文を文字列で返す
page.root.at('p.description').inner_text
<table>の中の<tr>の中の<td>を全部抜き出しHTMLを極力保持したまま配列で返す
page.root.search('table/tr/td').map{|e| e.inner_html}
アクセスの設定を変える
User-Agent を設定する
agent.user_agent_alias='Windows IE 7'
または agent.user_agent='文字列'
cookie を使用する
自動処理中
agent.cookie_jar.save_as(ファイル名) で全 cookie を YAML で保存する
agent.cookie_jar.load(ファイル名) で YAML 形式の cookie の読み込みが可能
プロキシを設定する
agent.set_proxy(アドレス, ポート, ユーザー名, パスワード)
特定のリファラー URL 文字列を設定する
agent.get(uri, URI.parse('http://www.example.com/referer_url'))
nil や無指定だと現在のページが自動で使われる
SSL 通信・https を使う
OpenSSL::SSL::VERIFY_PEER を使うなら @ca_file の指定が必要
ファイルを取り扱う
アクセスしたファイルをディスクに保存する
page.save または page.save_as(ファイル名)

satake7satake7 2008/07/19 05:39 いんすとーるーのところで「gem install mecanize」になってます。間違えるひとはいないと思いますが念のため(→ mechanize)

kitamomongakitamomonga 2008/07/19 13:04 >間違えるひとはいないと思いますが
mechanizeを書き間違えたひとが通りますよ

修正しました。ご指摘感謝です。なにかありましたらまたよろしくです。

ゲスト