◆ Ruby の WWW::Mechanize の説明はじめました ◆
- [Ruby][Mechanize] RubyのMechanizeの0.7.8が出てます@2008-08-26
- [Ruby][Mechanize] RubyのMechanizeの動作時間を短くできるかもしれない方法
- [Ruby][Mechanize] RubyのMechanizeの0.7.7が出たよ@2008-07-25
- [Ruby][Mechanize] RubyのMechanizeで日本語ファイルをパース前に変換したい
- [Ruby][Hpricot] Hpricotのinner_textで を空白として表示する
- [Ruby][Mechanize] RubyのMechanizeは追加のHTTPヘッダを送れない
- [Ruby][Mechanize] RubyのMechanizeでGmail送信できるようにした
- [Ruby][Mechanize] RubyのMechanizeのMozillaクッキー読み込み機能が重いので迂回する on Firefox2
- [Ruby][Mechanize] RubyのMechanizeのMozillaクッキー読み込み機能が重いので迂回する on Firefox3
- [Ruby][Mechanize] RubyのMechanizeのreferer隠せないよ問題
2008-03-28 RubyのWWW::Mechanizeを解説してみる
RubyのWWW::Mechanizeを解説してみる
対応バージョン:Mechanize 0.7.7 (2008-07-25)
まにゅあるめにゅー
- 本家マニュアル(英語)
- http://mechanize.rubyforge.org/mechanize/
クリックで各ページに飛びます
かいせつめにゅー
クリックで各項目に移動します
- WWW::Mechanize の仕組みてきとう解説
- マニュアルで引っかかった疑問のコーナー
- 別に agent や page っていうローカル変数に入れなくてもいいよね?
- page.forms[0] の 0って何よ
- q とか見慣れないメソッドがサンプルにあることがある
- 配列(みたいなの)が返ってるはずなのに click とか書かれてて俺涙目
- hoge(param = nil) って何? 引数に = が必要?
- agent.post('http://example.com/', 'q' => 'hogehoge') で => が浮いてる
- [](field_name) や []=(field_name, value) って何者?
- もしかして履歴って無限?
- wiki とかの URL に % の入ったページを get したら結果が変
- 404 のときとかどうすんの?
- forms や links があるなら images とかないの?
- メソッド追加しようと思ったら File.read とかが動かない
- インスタンス変数っていうかアクセスメソッドだよねこれ
- 逆引き
WWW::Mechanize の仕組みてきとう解説
- Web アクセス用のクライアントオブジェクト作成ー
- URL に Net::HTTP でアクセスしてファイルをげっとー
- Hpricot に渡して HTML 成分を解析ー
- リンクやフォームといったものをオブジェクトにして専用の配列で保持ー
- 入力欄オブジェクトに値を「入れた」りリンクオブジェクトを「踏んだ」ことにしてサーバに送るー
- 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 した結果のアレかー」と思ってください。
過去のバージョンにおける、引数に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する)ようにして回避してください。
リンクやフォームを専用の配列で保持ー
「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> を解析した結果になっています。
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 解説などを参照のこと。
入力欄に値を「入れた」りリンクを「踏んだ」ことにしてサーバに送るー
- 「ページの持つフォーム一覧のList」から目的の「フォームオブジェクト」をnameで探すか配列の要素指定で抽出
- その「フォームの持つ部品種類ごとのList」から目的の「"部品"オブジェクト」をnameで探すか配列の要素指定で抽出
- その"部品"オブジェクト固有のメソッドを利用して値を実際に入力したりチェックしたりする
という流れになります。なんか面倒そうですが、メソッドチェーンで記述するとわりとそのまんまです。
例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'}
前述のフォームと構造はほぼ同じです。
- 「ページの持つリンク一覧のList」から目的の「リンクオブジェクト」を属性で探すか配列の要素指定で抽出
- そのリンクオブジェクト固有のメソッドを利用して「クリック」したことにさせる
詳しくは 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 っていうローカル変数に入れなくてもいいよね?
- page.forms[0] の 0って何よ
- q とか見慣れないメソッドがサンプルにあることがある
- 配列(みたいなの)が返ってるはずなのに click とか書かれてて俺涙目
- hoge(param = nil) って何? 引数に = が必要?
- agent.post('http://example.com/', 'q' => 'hogehoge') で => が浮いてる
- [](field_name) や []=(field_name, value) って何者?
- もしかして履歴って無限?
- wiki とかの URL に % の入ったページを get したら結果が変
- 404 のときとかどうすんの?
- forms や links があるなら images とかないの?
- メソッド追加しようと思ったら File.read とかが動かない
- インスタンス変数っていうかアクセスメソッドだよねこれ
別に 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 環境でバイナリファイルをバイナリとして扱うときにがっかりな動作になる(バイナリモードが設定されていない)ので公開や頒布には気をつけましょう。
インスタンス変数っていうかアクセサだよねこれ
あんま細かいこと気にすると禿げます
逆引き
- かいせつ
- URLにアクセスする
- ページ内のHTMLのリンクをどうにかする
- ページ内のHTMLのフォームをどうにかする
- ページ内のHTMLの要素をどうにかする
- アクセスの設定を変える
- ファイルを取り扱う
かいせつ
- 直前に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(ファイル名)