超A&G+の新しい番組表(2015/04現在)をスクレイピングする
4月の番組改変に合わせて番組表が新しいやつになっていて悲鳴あげながら直しました。 前の番組表は1つの曜日が1つのtableにマッピングされていて楽だったのですが、 新しいやつは
- 全曜日で1テーブル
- 行(tr)は曜日ではなく時刻ごと。その中に全曜日の番組tdで入っている
- rowspanで行の結合があるため、それを考慮した2次元配列を作っておかないと曜日の判定がずれる
となかなか凶悪な仕様になっております。
自分が作っているラジオ録画ソフトではこんな感じで対応した。
こんかいの対応にかかったコーディング時間は2時間ほど。 こういうのは反射神経が重要なので、新しいやつが出たときにババっと書き捨てられる力をつけていきたいものです。
require 'net/http' require 'time' require 'chronic' require 'pp' require 'moji' module Ag class Program < Struct.new(:start_time, :minutes, :title) end class ProgramTime < Struct.new(:wday, :time) SAME_DAY_LINE_HOUR = 5 # convert human friendly time to computer friendly time def self.parse(wday, time_str) time = Time.parse(time_str) if time.hour < SAME_DAY_LINE_HOUR wday = (wday + 1) % 7 end self.new(wday, time) end def next_on_air time = chronic(wday_for_chronic_include_today(self[:wday])) if time > Time.now return time else chronic(wday_to_s(self[:wday])) end end def chronic(day_str) Chronic.parse( "#{day_str} #{self[:time].strftime("%H:%M")}", context: :future, ambiguous_time_range: :none, hours24: true, guess: :begin ) end def wday_for_chronic_include_today(wday) if Time.now.wday == wday return 'today' end wday_to_s(wday) end def wday_to_s(wday) %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)[wday] end end class Scraping def main programs = scraping_page programs = validate_programs(programs) programs end def validate_programs(programs) if programs.size < 20 puts "Error: Number of programs is too few!" exit end programs.delete_if do |program| program.title == '放送休止' end end def scraping_page html = Net::HTTP.get(URI.parse('http://www.agqr.jp/timetable/streaming.php')) dom = Nokogiri::HTML.parse(html) trs = dom.css('.timetb-ag tbody tr') # may be 30minutes belt two_dim_array = table_to_two_dim_array(trs) two_dim_array.inject([]) do |programs, belt| programs + parse_belt_dom(belt) end end def parse_belt_dom(belt) belt.each_with_index.inject([]) do |programs, (td, index)| next programs unless td wday = (index + 1) % 7 # monday start programs << parse_td_dom(td, wday) end end def table_to_two_dim_array(trs) aa = [] span = {} trs.each_with_index do |tr, row_n| a = [] col_n = 0 tr.css('td').each do |td| while span[[row_n, col_n]] a.push(nil) col_n += 1 end a.push(td) cspan = 1 if td['colspan'] =~ /(\d+)/ cspan = $1.to_i end rspan = 1 if td['rowspan'] =~ /(\d+)/ rspan = $1.to_i end (row_n...(row_n + rspan)).each do |r| (col_n...(col_n + cspan)).each do |c| span[[r, c]] = true end end col_n += 1 end aa.push(a) end aa end def determine_wday(index, padded) wday = index - 1 % 7 # monday start end def padded?(td) end def parse_td_dom(td, wday) start_time = parse_start_time(td, wday) minutes = parse_minutes(td) title = parse_title(td) Program.new(start_time, minutes, title) end def parse_minutes(td) rowspan = td.attribute('rowspan') if !rowspan || rowspan.value.blank? 30 else td.attribute('rowspan').value.to_i * 30 end end def parse_start_time(td, wday) ProgramTime.parse(wday, td.css('.time')[0].text) end def parse_title(td) [td.css('.title-p')[0].text, td.css('.rp')[0].text].select do |text| !text.gsub(/\s/, '').empty? end.map do |text| Moji.normalize_zen_han(text).strip end.join(' ') end end end
元コード
悲しみのdiff
Follow the A&G+ new timetable!!!!!!!!!!!!!! · yayugu/net-radio-archive@67990ee · GitHub