◆ Ruby の Mechanize の使い始めさん向き解説あります ◆
- [Ruby][Mechanize] Ruby Mechanize 2.1.pre.1+
- [Ruby][Mechanize] RubyのMechanize2.0によるcookieで始まるちょっと泣けるログインできないい話
- [Ruby] Ruby の YARD(Yardoc) の @ タグ
- [Ruby][Mechanize] Ruby Mechanize 2.0 リリース
- [Ruby無関係] UbuntuのPulseAudioでコマンドから音を鳴らす
- [Ruby][Mechanize] RubyのMechanizeでのHTTPステータスとmetaリフレッシュ
- [Ruby] Rubyでテキストなプログレスバーを表示するProgressBar gem
- 【自作】RubyのMechanizeのGETにプログレスバーをつけるMechanize-ProgressBar
- [Ruby] RubyInstallerのMinGW32なRubyでのgem installでmakeが必要なとき
- [Ruby] Ruby1.9のNet::HTTPで取得した文字列はforce_encodingせざるを得ない
2008-04-10
修正済】RubyのMechanizeではパーセント記述されたURL文字列を処理できない++++(8/21)
8/21更新
7/25更新
6/8 更新
4/18 更新
(8/21) パーセントエンコードの不具合は Mechanize 0.7.7 では直っています。アップグレードをおすすめします。
(7/25) to_absolute_uri には「<a href="?param=hoge">というリンクを踏めない」という不具合も以下省略
(6/8) Mechanize 0.7.6 用の上書きスクリプトを作れたような気がします
(4/18)コメントを受けて absolute_uri を上書きするスクリプトの「てきとうなバグ回避っぷり」が改善されました
以下更新済みの本文
問題バージョン:Mechanize 0.7.5、Mechanize 0.7.6 (2008-05-11)
ええと、ご存知の方もけっこういるかと思われますが、例の to_absolute_uri の話です。
結論から言うと 0.7.6 までの不具合です。意図的に注意深く避けてスクリプトを書くか、自前で問題のメソッドを再定義する必要があります。
agent.get などで外部から URL を指定する場合、URI オブジェクトと URL な文字列の2通りあるわけですが、2バイト文字などの%を含む URL(URLエンコードされたURL)の場合は引数を URI オブジェクトで指定しないとうまく動作しません。
# 文字列で指定するとうまく動作しない例 require "rubygems" require "mechanize" require 'kconv' require 'logger' logger = Logger.new($stderr) logger.level = Logger::INFO url = 'http://d.hatena.ne.jp/keyword/%C6%FC%CB%DC%B8%EC' agent = WWW::Mechanize.new{|a| a.log = logger a.user_agent_alias='Windows Mozilla'} agent.get(URI.parse(url)) puts agent.page.title.toeuc agent.get(url) puts agent.page.title.toeuc
結果
I, [2008-04-10T16:31:36.390456 #15028] INFO -- : Net::HTTP::Get: /keyword/%C6%FC%CB%DC%B8%EC I, [2008-04-10T16:31:36.769150 #15028] INFO -- : status: 200 日本語とは - はてなダイアリー I, [2008-04-10T16:31:37.139741 #15028] INFO -- : Net::HTTP::Get: /keyword/%C6 ←間違えてる I, [2008-04-10T16:31:37.203359 #15028] INFO -- : status: 302 I, [2008-04-10T16:31:37.204632 #15028] INFO -- : follow redirect to: http://d.hatena.ne.jp/ I, [2008-04-10T16:31:37.206728 #15028] INFO -- : Net::HTTP::Get: / I, [2008-04-10T16:31:37.404399 #15028] INFO -- : status: 200 はてなダイアリー - 快適、安心、シンプルなはてなのブログ
301 のリダイレクト先にパーセントエンコードされた URL が返ってくることのある Wikipedia などではこれはかなり深刻で、
[2008-04-10T16:37:16.258073] INFO -- : status: 301 [2008-04-10T16:37:16.258641] INFO -- : follow redirect to: http://ja.wikipedia.org/wiki/%C3%83 [2008-04-10T16:37:16.259963] INFO -- : Net::HTTP::Get: /wiki/%C3 ←間違えてる [2008-04-10T16:37:16.392255] INFO -- : status: 301 [2008-04-10T16:37:16.392824] INFO -- : follow redirect to: http://ja.wikipedia.org/wiki/%C3%83 [2008-04-10T16:37:16.394154] INFO -- : Net::HTTP::Get: /wiki/%C3 ←間違えてる [2008-04-10T16:37:16.527068] INFO -- : status: 301 [2008-04-10T16:37:16.527640] INFO -- : follow redirect to: http://ja.wikipedia.org/wiki/%C3%83 [2008-04-10T16:37:16.529057] INFO -- : Net::HTTP::Get: /wiki/%C3 ←間違えてるってば ...
と、GET すべき URL を間違え続けるため、無限にサーバへのアクセスを繰り返すことになってしまいます。
内部で使用されている to_absolute_uri というメソッドの処理に誤りがあるため、0.7.5ではこうなります。
Link オブジェクトを click するときは to_absolute_uri 使ってたんでしたっけ?
require "rubygems"; require "mechanize"; require 'kconv' require 'logger' logger = Logger.new($stderr); logger.level = Logger::INFO agent = WWW::Mechanize.new agent.log = logger agent.user_agent_alias='Windows Mozilla' agent.get(URI.parse("http://d.hatena.ne.jp/keyword/%C6%FC%CB%DC%B8%EC")) link = agent.page.links.text(/#{'はてな検索で検索'.toeuc}/e) p link.uri.to_s link.click
結果
I, [2008-04-10T16:42:39.307822 #15031] INFO -- : Net::HTTP::Get: /keyword/%C6%FC%CB%DC%B8%EC I, [2008-04-10T16:42:39.723080 #15031] INFO -- : status: 200 "http://search.hatena.ne.jp/search?word=%c6%fc%cb%dc%b8%ec" ← 抽出したURL I, [2008-04-10T16:42:40.282121 #15031] INFO -- : Net::HTTP::Get: /search?word=%c6 ←間違えてる I, [2008-04-10T16:42:40.906072 #15031] INFO -- : status: 200
HTML に書かれた URL へのアクセスでも間違えるんじゃそもそも駄目じゃん…
で、問題の個所の mechanize.rb のソースが、たとえば 0.7.5 だと
# at mechanize.rb def to_absolute_uri(url, cur_page=current_page()) unless url.is_a? URI url = url.to_s.strip.gsub(/[^#{0.chr}-#{125.chr}]/) { |match| sprintf('%%%X', match.unpack($KCODE == 'UTF8' ? 'U' : 'c')[0]) } url = URI.parse( Mechanize.html_unescape( url.split(/%[0-9A-Fa-f]{2}|#/).zip( url.scan(/%[0-9A-Fa-f]{2}|#/) ).map { |x,y| "#{URI.escape(x)}#{y}" }.join('') ) ) end url.path = '/' if url.path.length == 0 # construct an absolute uri if url.relative? raise 'no history. please specify an absolute URL' unless cur_page.uri base = cur_page.respond_to?(:bases) ? cur_page.bases.last : nil url = ((base && base.uri && base.uri.absolute?) ? base.uri : cur_page.uri) + url url = cur_page.uri + url # Strip initial "/.." bits from the path url.path.sub!(/^(\/\.\.)+(?=\/)/, '') end return url end def html_unescape(s) return s unless s s.gsub(/&(\w+|#[0-9]+);/) { |match| number = case match when /&(\w+);/ Hpricot::NamedCharacters[$1] when /&#([0-9]+);/ $1.to_i end number ? ([number].pack('U') rescue match) : match } end
うおー何やってるかわからん。
Ruby 付属の URI モジュールの挙動の差異を吸収する意味でもあるんですかね。
仕方がないので、to_absolute_uri 自体をスクリプトの最初で上書きすることにします。
コメント欄で指摘受けましたので、その部分だけピンポイントで書き換えてみます。
require "rubygems" require "mechanize" # WWW::Mechanize#to_absolute_uri を上書き # split と scan の正規表現を /%[0-9A-Fa-f]{2}|#/ から変更 module WWW class Mechanize def to_absolute_uri(url, cur_page=current_page()) unless url.is_a? URI url = url.to_s.strip.gsub(/[^#{0.chr}-#{125.chr}]/) { |match| sprintf('%%%X', match.unpack($KCODE == 'UTF8' ? 'U' : 'c')[0]) } url = URI.parse( Mechanize.html_unescape( url.split(/(?:%[0-9A-Fa-f]{2})+|#/).zip( url.scan(/(?:%[0-9A-Fa-f]{2})+|#/) ).map { |x,y| "#{URI.escape(x)}#{y}" }.join('') ) ) end url.path = '/' if url.path.length == 0 # construct an absolute uri if url.relative? raise 'no history. please specify an absolute URL' unless cur_page.uri base = cur_page.respond_to?(:bases) ? cur_page.bases.last : nil url = ((base && base.uri && base.uri.absolute?) ? base.uri : cur_page.uri) + url url = cur_page.uri + url # Strip initial "/.." bits from the path url.path.sub!(/^(\/\.\.)+(?=\/)/, '') end return url end end end # ここから普通 require 'kconv' require 'logger' logger = Logger.new($stderr); logger.level = Logger::INFO agent = WWW::Mechanize.new agent.log = logger agent.user_agent_alias='Windows Mozilla' agent.get(URI.parse("http://d.hatena.ne.jp/keyword/%C6%FC%CB%DC%B8%EC")) link = agent.page.links.text(/#{'はてな検索で検索'.toeuc}/e) p link.uri.to_s link.click
結果
I, [2008-04-18T18:01:00.707866 #913] INFO -- : Net::HTTP::Get: /keyword/%C6%FC%CB%DC%B8%EC I, [2008-04-18T18:01:01.101886 #913] INFO -- : status: 200 "http://search.hatena.ne.jp/search?word=%c6%fc%cb%dc%b8%ec" I, [2008-04-18T18:01:01.619172 #913] INFO -- : Net::HTTP::Get: /search?word=%c6%fc%cb%dc%b8%ec I, [2008-04-18T18:01:02.393165 #913] INFO -- : status: 200
わー動いた。
コメント寄せて頂いた tance_tk さんありがとうございます。
Mechanize 0.7.6
問題の部分は0.7.5と同じなのでたぶん動く、たぶん…
で、GmailでMechanize使えなかったよ記録 にもあるんですが、実はもうひとつ困った動作があって
uri = URI.parse('?param=foo') agent.page.uri = URI.parse('http://www.example.com/dir/') puts to_absolute_uri(uri).request_uri
という環境だと
http://www.example.com/?param=foo
になってしまうんです。パスのない相対 URI オブジェクトを渡すと、現在のページの URI の path 部分の dir/ が消えてしまうんですね。
という話なんですが、検索しても困ってる人がいない感じで、ちょっと不安です。
raise 'Mechanize 0.7.6 only' unless WWW::Mechanize::VERSION == '0.7.6' module WWW class Mechanize def to_absolute_uri(url, cur_page=current_page()) ## 1) original: to_absolute_uri('http://ex/%xx%yy%zz') returns URI(http://ex/%xx) ## 1) fixed: to_absolute_uri('http://ex/%xx%yy%zz') returns URI(http://ex/%xx%yy%zz) ## 2) original: to_absolute_uri('?param=foo',Page(http://ex/a/)) returns URI(http://ex/?param=foo) ## 2) fixed: to_absolute_uri('?param=foo',Page(http://ex/a/)) returns URI(http://ex/a/?param=foo) unless url.is_a? URI url = url.to_s.strip.gsub(/[^#{0.chr}-#{126.chr}]/) { |match| sprintf('%%%X', match.unpack($KCODE == 'UTF8' ? 'U' : 'c')[0]) } url = URI.parse( Mechanize.html_unescape( # url.split(/%[0-9A-Fa-f]{2}|#/).zip( # (1) url.split(/(?:%[0-9A-Fa-f]{2})+|#/).zip( # url.scan(/%[0-9A-Fa-f]{2}|#/) # (1) url.scan(/(?:%[0-9A-Fa-f]{2})+|#/) ).map { |x,y| "#{URI.escape(x)}#{y}" }.join('') ) ) end url = @scheme_handlers[url.relative? ? 'relative' : url.scheme.downcase].call(url, cur_page) # url.path = '/' if url.path.length == 0 # (2) # construct an absolute uri if url.relative? raise 'no history. please specify an absolute URL' unless cur_page.uri base = cur_page.respond_to?(:bases) ? cur_page.bases.last : nil url = ((base && base.uri && base.uri.absolute?) ? base.uri : cur_page.uri) + url url = cur_page.uri + url # Strip initial "/.." bits from the path url.path.sub!(/^(\/\.\.)+(?=\/)/, '') end url.normalize! # (2) return url end end end