2010-01-26

Mechanizeで無茶をする

mechanize-0.9.3 Documentation

自分にとって Mechanize による自動化はたいがい無理を通す行為である。分かりやすく言えば API なんかない、あるいはあっても足りないみたいな状態で、それでもどうにか自動化したいから Mechanize を使う。

Mechanize が持っている標準的な機能だけで済んでいる場合はまだかなりマシで、実際のところ無理というか「無茶」なレベルに突入してしまうことが、なぜかそれなりにあったりする。具体的には HTML が壊れているのでパースに失敗して、あるはずの要素がなくなっていたりする場合などである。

今回はそんな無茶の一部をご紹介。

パーサを Hpricot に変える

ずばり基本でしょう。

Mechanize は 0.9 以降デフォルトパーサを Hpricot から Nokogiri に切り替えているが、そもそも Nokogiri は HTML 用にできていない。XML 用の道具に、 Hpricot によく似たインターフェイスを付けたものである。HTML は自由度の高い書式で、XML 用のノコギリでは歯こぼれを起こすことがよくある。

そこでこの設定(0.9.0 〜 0.9.2)。

require 'hpricot'
WWW::Mechanize.html_parser = Hpricot

cf.

0.9.3 (以降?)はサブクラスの利用に注意

html_parser がインスタンスのアクセッサとして定義されたのでインスタンスごとに parser をセットできるようになったのはいいんだけど、Mechanizeクラスオブジェクトのインスタンス変数を self.class で参照して自身に書き戻しているので、

サブクラスに反映されない

状態になっている。(少なくとも Ruby 1.8.7 では parser が nil になって動かなかった。)定義部分は以下のような感じ。

class Mechanize
  ...
  @html_parser = Nokogiri::HTML
  class << self; attr_accessor :html_parser, :log end
  def initialize
    ...
    @html_parser = self.class.html_parser
  end
  ...
end

動かすとこんな感じ。

$ cat sub_mechanize.rb
class SubMechanize < WWW::Mechanize
end
$ irb
irb(main):001:0> require 'mechanize'
=> true
irb(main):002:0> a = WWW::Mechanize.new
=> (snip)irb(main):003:0> a.html_parser
=> Nokogiri::HTML
irb(main):004:0> require './sub_mechanize'
=> true
irb(main):005:0> b = SubMechanize.new
=> (snip)
irb(main):006:0> b.html_parser
=> nil

恐らく Mechanize のインスタンスについては html_parser のセット方法に互換性をとりつつインスタンスごとに設定できるようにしたかったためにこうなったんだろうけど、いつもデバッグしやすいように独自のサブクラスを噛ましていた1ので、まったく parse できない現象にハマってしまった。仕方ないのでサブクラスの中で独自に定義することにした。

class SubMechanize < WWW::Mechanize
  def initialize
    super
    @html_parser = Hpricot
  end
end

こんなんでいいのかな。

cf. RubyのMechanizeの0.9.3が6月8日に出てたっぽい - きたももんががきたん。

Field を作る

これはまだ初級。

Form#add_field!( name, value )

無駄に JavaScript に分離したフォームの場合、必要な field が HTML 上に存在しないことがよくある。その値を無理矢理 form 上に反映するために使う。

FileUpload を作る

これに気づいたときにはけっこう嬉しかった。add_field! では Field は作れても FileUpload は作れないから。

どうやるかというと、Form オブジェクトに対して instance_eval を使う。

Form#enctype = 'multipart/form-data'
Form#instance_eval {
  @file_uploads << WWW::Mechanize::Form::FileUpload.new( name[, filename] )
}

もともと file_upload が存在しない form として解釈した場合は enctype が違うことがある(というか指定してないかも)ので手で変更してあげると吉。

Form を作る

instance_eval() を思い出してしまえば簡単。もう form そのものを解釈できませんでしたという凶悪な HTML 向け。

Page#instance_eval {
  @forms << WWW::Mechanize::Form.new( node[, mech, page] )
}

node には Hpricot::Elem オブジェクトを入れてあげれば ok.

このとき、Hpricot::Elem になれば元の文字列はなんでもよいので、

Form.new( Hpricot( String#scan( /<form.*?>.*?<\/form>/m ).to_s ).at( 'form' ) )

みたいなこともできる。HTML が壊れているので正規表現でいったん form だけ引っこ抜いて、それを Hpricot オブジェクトに戻してやって Form を作る。途中の整形も思いのまま。

Page を作る

Page を作るのはちょっと手が掛かる。以前作ったものを gist に置いてあるので参考になれば嬉しい。

  1. http://gist.github.com/76140 

About

例によって個人のなんちゃらです