WWW::Mechanize(Rubyの)で一部のフォームが取れない問題
どうも,well-formedじゃないHTMLの場合に,フォームの一部を取れないみたい.例えば,以下のようなフォームがあるときにbarが取れない(WWW::Mechanize#page.forms.first.field('bar').nil? == trueになる).
<p> <form> <input name="foo"> </p> <input name="bar">
検索してみたけど情報が無い.みんな困っていないのだろうか?
まだ対策できていないけど,テストだけ張っておく.これがpassすれば問題解決.
#!/usr/local/bin/ruby # $Id$ require 'rubygems' require 'mechanize' require 'logger' require 'webrick' require 'test/unit' require 'ruby-debug' class DumbHTTPD class Servlet < WEBrick::HTTPServlet::AbstractServlet @@tmpl = lambda do |val| <<_EOT_ <html> <body> <h1>Fill out below form:</h1> <p> <form action="/" method="POST"> <input type="text" name="foo" value="#{val['foo']}"> </p> <input type="text" name="bar" value="#{val['bar']}"> <input type="submit" value="save"> </form> </body> </html> _EOT_ end def do_GET(req, res) res['Content-Type'] = 'text/html; charset=utf-8' res.body = @@tmpl.call({ 'foo' => 'Hello world', 'bar' => 'hoge', }) return res end def do_POST(req, res) res['Content-Type'] = 'text/html; charset=utf-8' res.body = @@tmpl.call(req.query) return res end end attr_accessor :webrick, :runtime, :bindaddr, :port def initialize(bindaddr, port) self.bindaddr, self.port = bindaddr, port end def init_webrick self.webrick = WEBrick::HTTPServer.new( :BindAddress => self.bindaddr, :Port => self.port) self.webrick.mount('/', Servlet) end def start self.init_webrick self.runtime = Thread.new(self.webrick) {|w| w.start} return self end def stop self.runtime.kill.join self.webrick.shutdown return self end end class TC_Mech < Test::Unit::TestCase attr_accessor :agent, :servlet def setup self.agent = WWW::Mechanize.new {|a| a.log = Logger.new($STDERR) } self.agent.max_history = 1 self.agent.user_agent_alias = 'Windows IE 6' self.servlet = DumbHTTPD.new('localhost', 10182).start end def teardown self.servlet.stop end def test_scrape_not_wellformed_html page = self.agent.get('http://localhost:10182') form = page.forms.first {'foo' => 'Hello world', 'bar' => 'hoge', }.each do |k, v| field = form.field(k) assert(!field.nil?, "form field '#{k}' is not exists") assert_equal(v, field.value, "form field '#{k}'.value != '#{v}'") end end end
対処法の考察(メモ)
WWW::Mechanize::Page#formsは,最初の一回呼び出された時にHpricot.parseしてsearch('form')して,出てきたHpricot::Elements分だけformを作って(WWW::Mechanize::Pageのインスタンス変数に)キャッシュする.んだけども,Hpricotは上記のような壊れたHTMLを読む時,後続のinputを無視してしまう.
だけど,Hpricot的には多分悪くない動作.多分,WWW::Mechanizeで対策するべきで,Mechはplaggable_parserという機構がありHTMLパーサを動的に差し替える事が可能.これを使って「ゆるくHTMLフォームを解釈するWWW::Mechanize::Pageの子孫クラス」を適当に作って以下のようにすればよい.
class WWW::Mechanize::LamePage < WWW::Mechanize::Page def initialize(uri=nil, response=nil, body=nil, code=nil) super(uri, response, body, code) end def forms # ここでformsを再定義 end end agent = WWW::Mechanize.new agent.pluggable_parser.html = WWW::Mechanize::LamePage # 標準のパーサを差し替え # 後は普通に使う
と,対策手法はわかっているんだけど,Hpricotの使い方が難しくてなかなか進みません…….
追記 (2009/2/24)
WWW::Mechanize 0.9.0で追試したところ,本エントリで触れた問題は解決していた.よかった.
どうやら,HTMLパーサがHpricotからNokogiriに変わったことで,このような問題がなくなった模様.