# # = yahoo_jp_transit.rb # # Copyright (c) 2007 SUGAWARA Genki # # == Example # # $KCODE = 's' # require 'yahoo_jp_transit' # # searcher = YahooJpTransit::Searcher.new # # selected = searcher.select('大宮', '東京') # puts selected # # from = selected.from[0] # to = selected.to[0] # # result = searcher.query(from, to) # # exit if result.empty? # # begin # puts result # end while result = result.next_pagerequire # require 'cgi' require 'kconv' require 'open-uri' module YahooJpTransit class SearchError < StandardError; end class Searcher @@url = 'http://transit.yahoo.co.jp/search' DEPARTURE = 'DEP' ARRIVAL = 'ARR' LAST = 'LST' NONE = 'NON' attr_reader :kcode def initialize(kcode = $KCODE, proxy = nil) @kcode = case kcode when /\Ae/i Kconv::EUC when /\As/i Kconv::SJIS when /\Au/i Kconv::UTF8 end @proxy = proxy end def query(from, to, options = {}) options = prepare_options(:result, from, to, options) open("#{@@url}?#{query_string(options)}", { :proxy => @proxy }) do |f| ResultParser.new(f, self, from, to, options).parse end end def select(from, to, options = {}) options = prepare_options(:select, from, to, options) open("#{@@url}?#{query_string(options)}", { :proxy => @proxy }) do |f| SelectParser.new(f, self).parse end end private def prepare_options(htmb, from, to, options) options = options.dup defaults = { :val_htmb => htmb, :from => from, :p => to, :sort => 0, :num => 0, :valtimekb => DEPARTURE, :val_dsmask_charge => :CHARGE, :val_dsmask_air => :AIR, :val_search => "\303\265\272\367" }.merge(divide_datetime(Time.now)) if dt = options.delete(:datetime) options.update(divide_datetime(dt)) end if timekb = options.delete(:timekb) options[:valtimekb] = timekb end if options.delete(:charge) == false or options.delete(:val_dsmask_charge) == false defaults.delete(:val_dsmask_charge) end if options.delete(:air) == false or options.delete(:val_dsmask_air) == false defaults.delete(:val_dsmask_air) end return defaults.merge(options) end def query_string(params) params.map {|k, v| k = CGI.escape("#{k}") v = CGI.escape(Kconv.kconv("#{v}", Kconv::EUC, @kconv || Kconv::AUTO)) "#{k}=#{v}" }.join('&') end def divide_datetime(datetime) val_m = datetime.strftime('%M') { :val_yymm => datetime.strftime('%Y%m'), :val_dd => datetime.strftime('%d'), :val_hh => datetime.strftime('%H'), :val_m1 => val_m.slice(0, 1), :val_m2 => val_m.slice(1, 1) } end end # class Searcher class ResultParser def initialize(input_stream, searcher, from, to, search_options) @input_stream = input_stream @searcher = searcher @from = from @to = to @search_options = search_options end def parse rs = [] vars = nil @input_stream.each do |line| raise(SearchError, parse_error) if line['"error_container"'] break if line['"page_control2"'] # parse page control if line['"page_count"'] and not vars vars = { :searcher => @searcher, :from => @from, :to => @to, :search_options => @search_options } line = @input_stream.gets count, range = line.scan(%r|([^<]+)|e).map {|i| i.first } vars[:count] = count.to_i vars[:head_num] = range.slice(0, 1).to_i vars[:tail_num] = range.slice(-1, 1).to_i end next unless line['"result_order"'] rs << Result.new do |header_class, path_class| header = parse_result_header(header_class) paths = [] while parse_result_path(path_class, paths); end [header, paths] end end unless rs.empty? rs.instance_variable_set(:@vars, vars) def rs.count; @vars[:count]; end def rs.head_num; @vars[:head_num]; end def rs.tail_num; @vars[:tail_num]; end def rs.page; (head_num.to_f / 3).ceil; end def rs.pages; (count.to_f / 3).ceil; end def rs.next_page; if page < pages options = @vars[:search_options].merge(:num => (3 * page)) @vars[:searcher].query(@vars[:from], @vars[:to], options) else nil end end def rs.prev_page; if page > 1 options = @vars[:search_options].merge(:num => (3 * (page - 2))) @vars[:searcher].query(@vars[:from], @vars[:to], options) else nil end end else def rs.next_page; nil; end def rs.prev_page; nil; end end return rs end private def parse_result_header(header_class) kwargs = {} kwargs[:start] = search('"start', '"result_head_right_cont"') {|l| l.match(/>([^<]+)([^<]+)(.+)|e)[1].gsub(%r|]*>|e, '') end kwargs[:distance] = search('"result_distance', '([^<]+)(.+)|e)[1].gsub(%r|]*>|e, '') end kwargs[:connection] = search('"result_connection', '(.+)|e)[1].gsub(%r|]*>|e, '') end kwargs[:pass] = search('"result_pass', '(.+)|e)[1].gsub(%r||e, '') end header = header_class.new(kwargs) if @searcher.kcode header.kconv(@searcher.kcode) end return header end def parse_result_path(path_class, out) kwargs = {} kwargs[:station] = search(['[', "\241\316"], "") {|l| l.match(%r|(.+)|e)[1] } line = '' line = @input_stream.gets until line[""] line = @input_stream.gets until line =~ /\A/e || line =~ %r|\A|e if has_next = (line =~ /\A/e) kwargs[:time] = search('', "") {|l| l.match(%r|([^<]+)|e)[1] } kwargs[:course], kwargs[:fare] = search('', "") do |l| desc = l.match(%r|>([^<]+)|e)[1] fare = (m = l.match(%r|>([^<]+)|e)) ? m[1] : nil [desc, fare] end end path = path_class.new(kwargs) if @searcher.kcode path.kconv(@searcher.kcode) end out << path return has_next end def parse_error errs = [] @input_stream.each do |line| break if line[''] if line["\241\246"] errs << line.gsub("\241\246", '').gsub(%r|]+>|e, '').strip end end if (errmsg = errs.join(', ')).empty? errmsg = 'search error' elsif @searcher.kcode errmsg = Kconv.kconv(errmsg, @searcher.kcode, Kconv::EUC) end return errmsg end def search(strs, limit, default = false) if strs.kind_of?(String) strs = [strs] end until (line = @input_stream.gets)[limit] if strs.any? {|str| line[str] } return yield(line) end end if default == false raise "cannot find strings: #{strs.map{|i| i.inspect }.join(', ')}" else return default end end end # class ResultParser class SelectParser def initialize(input_stream, searcher) @input_stream = input_stream @searcher = searcher end def parse from = [] to = [] @input_stream.each do |line| raise(SearchError, parse_error) if line['"error_container"'] break if line["

2. \222T\215\365\223\372\216\236

"] if line["\275\320\310\257\261\330"] parse_options(from) end if line["\305\376\303\345\261\330"] parse_options(to) end end selected = Selected.new(from, to) if @searcher.kcode selected.kconv(@searcher.kcode) end return selected end private def parse_options(list) until (line = @input_stream.gets)[''] if (m = %r|^]*>([^<]+)|.match(line)) list << m[1] end end end def parse_error errs = [] @input_stream.each do |line| break if line[''] if line["\241\246"] errs << line.gsub("\241\246", '').gsub(%r|]+>|e, '').strip end end if (errmsg = errs.join(', ')).empty? errmsg = 'search error' elsif @searcher.kcode errmsg = Kconv.kconv(errmsg, @searcher.kcode, Kconv::EUC) end return errmsg end end # class SelectParser class Result class Header @@attrs = [ :start, :reach, :lapse, :distance, :passage, :connection, :pass ] attr_reader *@@attrs def initialize(kwargs) @@attrs.each {|kw| self.instance_variable_set("@#{kw}", kwargs[kw]) } end def to_s @@attrs.map {|kw| val = self.instance_variable_get("@#{kw}") "#{kw}=#{val}" }.join(', ') end def kconv(out_code) @@attrs.map do |kw| val = self.instance_variable_get("@#{kw}") or next val = Kconv.kconv(val, out_code, Kconv::EUC) self.instance_variable_set("@#{kw}", val) end end def to_hash vals = @@attrs.map {|kw| self.instance_variable_get("@#{kw}") } Hash[*(@@attrs.zip(vals).flatten)] end end class Path @@attrs = [ :station, :time, :course, :fare ] attr_reader *@@attrs def initialize(kwargs) @@attrs.each {|kw| self.instance_variable_set("@#{kw}", kwargs[kw]) } end def has_next? not @time.nil? end def kconv(out_code) @@attrs.map do |kw| val = self.instance_variable_get("@#{kw}") or next val = Kconv.kconv(val, out_code, Kconv::EUC) self.instance_variable_set("@#{kw}", val) end end def to_hash hash = {} @@attrs.map do |kw| val = self.instance_variable_get("@#{kw}") hash[kw] = val if val end return hash end end attr_reader :header, :paths def initialize @header, @paths = yield(Header, Path) end def to_yaml yaml = ['---'] yaml << 'header:' @header.to_hash.each {|k, v| yaml << " #{k}: #{v}" } yaml << 'paths:' @paths.each do |path| yaml << ' - ' + path.to_hash.map {|k, v| "#{k}: #{v}" }.join("#{$/} ") end yaml.join($/) end def to_s to_yaml end end # class Result class Selected attr_reader :from, :to def initialize(from, to) @from = from @to = to end def kconv(kcode) @from = @from.map {|i| Kconv.kconv(i, kcode, Kconv::EUC) } @to = @to.map {|i| Kconv.kconv(i, kcode, Kconv::EUC) } end def to_yaml yaml = ['---'] yaml << 'from:' @from.each {|i| yaml << " - #{i}" } yaml << 'to:' @to.each {|i| yaml << " - #{i}" } yaml.join($/) end def to_s to_yaml end end # class Selected end # module YahooJpTransit