Ruby + Mosaic = Rusaic ブラウザを作ってみた(gzip対応)

結構前に作りはじめて、そこそこ安定してきたのでこっそり公開。対話的に使えるわけではなく、プログラム中から気軽にhttp/httpsアクセスしたいというのが目的。

今から見るとちょっとRubyの機能を使いこなしていない書き方や、意図の不明なコメントもちらほらあるけど(「とりあえず」ってなんだ!?)

一番単純な使い方

require "rusaic.rb"
browser = Rusaic.new
browser.request("http://www.google.com")
puts browser.body

出力

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.jp/">here</A>.
</BODY></HTML>

メソッドの説明は下の方にあります。

工夫したところ・便利なところ

  • Cookieを自動で保存し、送信してくれる
  • gzip圧縮のコンテンツを自動で展開してくれる
  • リダイレクトはあえて対応していない。

ソースコード

rusaic.rb

$KCODE = "utf-8"

require 'net/http'
require 'net/https'
require 'date'
require 'stringio'
require 'zlib'

Rusaic_basic_req_head={
  "User-Agent" => %q|Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3|,
  "Accept" => %q|text/html,application/xrds+xml,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8|,
  "Accept-Language" => %q|ja,en-us;q=0.7,en;q=0.3|,
  "Accept-Encoding" => %q|gzip,deflate|,
  "Accept-Charset" => %q|Shift_JIS,utf-8;q=0.7,*;q=0.7|,
  "Keep-Alive" => "115",
  "Connection" => "keep-alive"
}

class Rusaic
  def initialize(verify_depth=5, ex_cookie="")
    @depth = verify_depth
    @cookie = Array.new #cookieは配列の配列
    ex_cookie.split(/;/).each{|e|
      @cookie << [e,""]
    }
    @res
    @req_head = Hash.new("")
    @res_body=""
    @protocol
    @domain
    @path
    @encoding=""
    @charset = ""
    @href = Array.new
    @src = Array.new
    @link = Array.new
    @req_head = Rusaic_basic_req_head
  end
  attr_reader :charset, :domain, :path, :href, :src, :link, :cookie, :res, :encoding
  attr_accessor :req_head
  #ここから、プライベート
  private
  def parse_cookie(str)
    return nil if str == nil
#    str.gsub!(/;,/, ';')
    str.gsub!(/(Mon|Tue|Wed|Thu|Fri|Sat|Sun),/){|match| match[0,3] + 'c' } #日付表示の中の","をcに置換
    arr = str.split(/,/) #,で分割
    arr.each{|e|
      e.gsub!(/(Mon|Tue|Wed|Thu|Fri|Sat|Sun)c/){|match| match[0,3] + ',' }#cにした,を元に戻す
      a = e.split(/;/)
      @cookie << a
    }
    return arr
  end
  def set_cookie
    c = Array.new
    @cookie.each{|e|
      c << e[0].strip
    }
    @req_head['Cookie'] = c.join(";")
  end
  def parse_url(url)
    col = url.split(/:\/\//)
    @protocol = col[0]
    @domain = col[1][0..col[1].index("/")-1]
    @path = col[1][col[1].index("/")..col[1].length]
    return @protocol, @domain, @path
  end
  def parse_charset(str)
    return nil if str==nil||str==""||!(str=~/;/)
    return str.split(/;/)[1].split(/=/)[1].downcase
  end
  def parse_body#とりあえず
    @src.clear
    @href.clear
    @link.clear
    @charset = parse_charset(@res['Content-Type'])
    @encoding = @res['content-encoding']
    @res.body.each_line{|l|
      @src << l if l =~ /src/
      @href << l if l =~ /<a.+href/
      @link << l if l =~ /<link/
    }
    @charset
  end
	def hash2str(hash)
		str = ""
		hash.each{|k,v|
			str += k+"="+v.to_s+"&"
		}
		return str
	end
  def get_ssl(domain, path)
    https = Net::HTTP.new(domain, 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE
    https.verify_depth = @depth
    https.start{|w|
      @res = w.get(path, @req_head)
      }
      return @res
  end
  def post_ssl(domain, path, content)
    https = Net::HTTP.new(domain, 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE
    https.verify_depth = @depth
    https.start{|w|
      @res = w.post(path, content, @req_head)
      }
      return @res
  end
  def get_normal(domain, path)
    Net::HTTP.start(domain, 80){|http|
      @res = http.get(path, @req_head)
    }
    return @res
  end
  def post_normal(domain, path, content)
    Net::HTTP.start(domain, 80){|http|
      @res = http.post(path, content, @req_head)
    }
    return @res
  end
  def get
    if @protocol=="http"
      @res = get_normal(@domain, @path)
    elsif @protocol=="https"
      @res = get_ssl(@domain, @path)
    else
    end
      @res
  end
  def post(content)
    if @protocol=="http"
      @res = post_normal(@domain, @path, content)
    elsif @protocol=="https"
      @res = post_ssl(@domain, @path, content)
    else
    end
      @res
  end
  #以下、パブリック
  public
  def add_cookie(a)
    @cookie << a
  end
  def request(url="", content=nil)
    return if url==""
    set_cookie
    parse_url(url)
    puts url if $DEBUG
    begin
      if content==nil
       @res = get
      else
		content = hash2str(content) if content.class == Hash
       @req_head['Content-Length'] = content.length.to_s
       @res = post(content)
       @req_head.delete("Content-Length")
      end
    rescue StandardError => ex
      str = ex.backtrace.to_s
      raise StandardError, "Rusaic Error : "+ex.message+str
    rescue Timeout::Error => ex
      raise StandardError, "Rusaic Timeout : "+ex.message
    end
    parse_cookie(@res['Set-Cookie'])
    parse_body
    if @encoding == "gzip"
    StringIO.open(@res.body, 'rb'){|sio|
      @res_body =  Zlib::GzipReader.wrap(sio).read
      }
    else
      @res_body = @res.body
    end
	puts "GOT:"+url if $DEBUG 
    @res
  end

  def body
    @res_body
  end
  def dump
    puts "@res:"
    puts @res
    @res.each{|k,v| puts k+":"+v }
    puts "@req_head"
    @req_head.each{|k,v| puts k+":"+v }
  end
end

クラスの説明

例外

タイムアウトも含めてStandardErrorとして返します。

クラスメソッド

Rusaic.new([verify_depth=5, [ex_cookie=""]])

  • verify_depth・・・net/httpsのverify_depthに渡す値
  • ex_cookie・・・手動で設定したいcookie・・・"I_do_Javascript=yes" とか
メソッド
  • request(url [,content])
    • サーバーにリクエストします。
    • urlはhttpまたはhttpsから始まる文字列。URLの最後のスラッシュを忘れるとエラーになります。
    • contentは「key1=value1&key2=value2&...」の形の文字列。スペースは含めない。url.encodeしておく必要があります。
    • 返値:RubyのNet::HTTP(S)Responseオブジェクト
  • body
    • レスポンスボディを返します。
  • dump
    • 現在ブラウザ内部で保持している情報を一気に表示します。
  • リーダー
    • domain・・www.google.comやd.hatena.ne.jpといった値
    • path・・domainの後ろの文字列
    • cookie・・保持しているCookie
    • res・・RubyのNet::HTTP(S)Responseオブジェクト
    • charset・・コンテンツの文字コード
    • href・・・hrefを含む行の配列*1
    • src・・・srcを含む行の配列*2
    • link・・linkを含む行の配列*3
  • アクセサ
    • req_head・・ブラウザのリクエストヘッダ

*1:きちんと解析しているわけではない。hrefを含む行を配列に放り込んでいるだけ

*2:hrefと同様にこのメソッドの有用性は低い

*3:これも同様。コメントの「とりあえず」ってそういうことだった