Skip to content

Instantly share code, notes, and snippets.

@ha4gu ha4gu/anipota.rb
Last active Apr 3, 2019

Embed
What would you like to do?
アキバ総研 (https://akiba-souken.com/) 様の新作アニメリストページから情報を取得し、録画サーバ2台での録画予約リストを作成するための個人的なスクリプト。
### ここから依存パッケージ定義パート
# NokogiriとChronicをgemでインストールしておくように。
require 'open-uri'
require 'nokogiri'
require 'date'
require 'time'
require "active_support/all"
require 'chronic'
### ここから関数定義パート
# アニメのタイトルを整形するための関数 format_title() を定義
def format_title(string)
string.gsub!(/\s/, '_')
string.gsub!(/ /, '_')
string.gsub!(/\//, '')
string.gsub!(/\!/, '')
string.gsub!(/!/, '')
string.gsub!(/\?/, '')
string.gsub!(/?/, '')
string.gsub!(/\&/, '')
string.gsub!(/~(.+)~/, '')
string.gsub!(/\((.+)\)/, '')
string.gsub!(/-(.+)-/, '')
string.sub!(/__+/, '')
string.sub!(/_\Z/, '')
string
end
def format_datetime(string)
# 年の取得
year, rest = string.split('', 2)
year = year.to_i
# 月の取得
month, rest = rest.split('', 2)
month = month.to_i
# 日の取得
day, rest = rest.split('', 2)
if rest.nil? # 日付未定による分割失敗のケース
return Chronic.parse("#{year}-#{month}")
else
day = day.to_i
end
# 時刻の取得
hour, minute = rest.gsub(/\((.+)\)/, '').split(':').map(&:to_i)
if minute.nil? # 時刻未定による分割失敗のケース
return Chronic.parse("#{year}-#{month}-#{day}")
else
return Chronic.parse("#{year}-#{month}-#{day} #{hour}:#{minute}")
end
end
### ここから仕込みパート
# 時刻はすべて日本時間で処理を行う
Time.zone = 'Asia/Tokyo'
Chronic.time_class = Time.zone
wd = ["", "", "", "", "", "", ""]
# 処理の対象とするURL
url = "https://akiba-souken.com/anime/spring/"
season = "2019-2Q春アニメ"
charset = nil
# 不要なタイトルのリスト
no_needs = [
"名作くん",
"ヴァンガード",
"ガンダム誕生秘話",
"KING OF PRISM",
"けだまのゴンじろー",
"少年アシベ",
"ダイヤのA",
"ちいさなプリンセス",
"デュエル・マスターズ",
"なむあみだ仏",
"ねこねこ日本史",
"猫のニャッホ",
"BAKUMATSUクライシス",
"爆丸バトルプラネット",
"パウ・パトロール",
"Bラッパーズ",
"MIX",
"ムーミン谷のなかまたち",
"妖怪ウォッチ",
]
# ページを読み込んでNokogiriでの処理にかける
html = open(url) do |page|
charset = page.charset
page.read
end
doc = Nokogiri::HTML.parse(html, nil, charset)
doc.search('br').each { |n| n.replace("\n") } # brタグを改行に変換
### ここから情報取得パート
# 取得後のデータを格納する配列を用意
watchable_animes = []
unwatchable_animes = []
unneeded_animes = []
# 視聴不可と判断された放送局を格納する配列を用意
channels_cannot_watch = []
# ページ内のすべてのアニメを取得する
all_animes = doc.xpath('//div[@id="contents"]/div[contains(@class, "main")]/div[@class="itemBox"]')
all_animes.each.with_index do |anime, i| # アニメごとに処理
# 録画対象となる放送を格納する配列
available_schedules = []
# タイトルの取得
title = anime.xpath('div[@class="mTitle"]/h2').text
if title.nil?
puts "【エラー】タイトル取得失敗(上から#{i+1}番目)"
next # 次のアニメを評価
end
# 不要アニメかどうかのチェック
no_need_flag = false
no_needs.each do |keyword|
if title.include?(keyword)
no_need_flag = true
break
end
end
if no_need_flag
unneeded_animes << {title: title}
next # 次のアニメを評価
end
# 放送スケジュールの取得
tr = anime.xpath('div[@class="itemData"]/div[@class="schedule"]/table[contains(., "放送スケジュール")]').xpath('tr')
if tr.size != 0 # tr == 0 ならTV放送がない(ネット配信のみ)だと判断。
tr.shift # tr[0]はテーブルヘッダ部分。不要なので捨てる。
tr.xpath('td').each do |td| # 放送スケジュールの各ワクごとに処理
if td.xpath('span').count == 0 # ワクが空欄である場合
next # 次のワクを評価
elsif td.xpath('span').count != 2 # 0でも2でもないのは想定外ケース
puts "【エラー】タイトル: #{title}, td内のspan数が#{td.xpath('span').count}"
next # 次のワクを評価
else # 2である場合。これが、放送日時が記載されている場合の標準パターン。
# 放送局を特定する処理
station = td.xpath('span')[0].text # => "TOKYO MX" など
if station.empty? # 放送局が取得できない場合
puts "【エラー】タイトル: #{title}, 放送局の取得に失敗"
next # 次のワクを評価
end
# 特定した放送局から、地上波かBSかを特定する処理
type = nil
# アニメ放送があると思われる、我が家で視聴可能なテレビ局のリスト。
# ※NHK BS, NHK BSプレミアム, ディーライフが配列に含まれていないため要注意。
tokyo_gr = ["NHK総合", "NHK Eテレ", "日本テレビ", "TBS", "フジテレビ", "テレビ朝日", "テレビ東京", "TOKYO MX"] # 地上波の放送局
tokyo_bs = ["BS日テレ", "BS朝日", "BS-TBS", "BSテレ東", "BSフジ", "BS11", "BS12 トゥエルビ"] # BSの放送局
if tokyo_gr.include?(station)
type = 'GR' # 放送局は地上波
elsif tokyo_bs.include?(station)
type = 'BS' # 放送局はBS
else # 視聴できないチャンネルでの放送であると思われる。
channels_cannot_watch << station # 視聴不可放送局リストに格納
next # 次のワクを評価
end
# 放送開始日時の特定
temp = td.xpath('span')[1].text.gsub(/~(.*)\Z/, '') # => "2019年4月2日(火)21:54" など
if temp.empty? # 放送開始日時が取得できない場合
puts "【エラー】タイトル: #{title}, 放送局: #{station}, 放送開始日時の取得に失敗"
next # 次のワクを評価
else
start = format_datetime(temp)
end
# 録画すべき放送として配列に格納
available_schedules << {type: type, station: station, start: start}
end # span数での分岐終了
end # 放送スケジュールの各ワクごとに処理
end # TV放送がある場合の処理
# 取得した結果を配列に格納
if available_schedules.empty? # 有効なTV放送がなかった場合
unwatchable_animes << {title: title}
else # 有効なTV放送があった場合
watchable_animes << {title: title, schedules: available_schedules}
end
end # アニメごとに処理
### ここから情報吟味パート
# watchable_animesは配列、各項目には :title, :schedulesが格納されている。
# :titleは単なるstring。
# :schedulesは配列、1件以上の項目が含まれている。
# 各項目は :type (String、'GR'または'BS') 、:station (String)、 :start (TimeWithZone) を持つ。
main_list = []
spare_list = []
no_spare_list = []
watchable_animes.each do |anime|
# :schedulesから:typeの各件数を取得し、それぞれがもし2件以上ある場合にはエラー扱いとする
count_gr = 0
count_bs = 0
gr = {}
bs = {}
anime[:schedules].each do |schedule|
if schedule[:type] == 'GR'
count_gr += 1
gr = schedule
elsif schedule[:type] == 'BS'
count_bs += 1
bs = schedule
end
end
if count_gr >= 2 || count_bs >= 2
puts "【エラー】タイトル: #{anime[:title]}, 地上波放送数: #{count_gr}, BS放送数: #{count_bs}, 放送数過多"
next # 次のアニメを評価
end
# 放送が1つしかない場合にはそれをメインとする
if anime[:schedules].size == 1
main_list << {title: format_title(anime[:title]), type: anime[:schedules][0][:type], station: anime[:schedules][0][:station], start: anime[:schedules][0][:start]}
no_spare_list << {title: format_title(anime[:title])}
else # 地上波とBSとそれぞれで放送がある場合、判断が必要
if bs[:start] - gr[:start] < 6*60*60
# bsの方が先に放送されているか、もしくは同じ晩に放送されていそうな場合にはbs側をメインとし、gr側をスペアとする
main_list << {title: format_title(anime[:title]), type: bs[:type], station: bs[:station], start: bs[:start]}
spare_list << {title: format_title(anime[:title]), type: gr[:type], station: gr[:station], start: gr[:start]}
else
# それ以外のケースではgr側をメインとし、bs側をスペアとする
main_list << {title: format_title(anime[:title]), type: gr[:type], station: gr[:station], start: gr[:start]}
spare_list << {title: format_title(anime[:title]), type: bs[:type], station: bs[:station], start: bs[:start]}
end
end
end
# 配列をそれぞれソートしておく。
# no_spare_listは単純にタイトルによるソート
no_spare_list.sort! { |a, b| a[:title] <=> b[:title] }
# main_listとspare_listは、
# 1) 日毎(ただし06:00〜30:00の区切り)
# 2) 地上波かBSか
# 3) 放送開始時刻
# の順でソートする。
main_list.sort! { |a, b|
((a[:start] - 6.hour).to_date <=> (b[:start] - 6.hour).to_date).nonzero? ||
(b[:type] <=> a[:type]).nonzero? ||
a[:start] <=> b[:start]
}
spare_list.sort! { |a, b|
((a[:start] - 6.hour).to_date <=> (b[:start] - 6.hour).to_date).nonzero? ||
(b[:type] <=> a[:type]).nonzero? ||
a[:start] <=> b[:start]
}
### ここから結果出力パート
# メインの方の出力
puts "■■■ メイン ■■■"
previous_date = Date.new(2000, 1, 1)
main_list.each do |e|
current_date = (e[:start] - 6.hour).to_date
if previous_date != current_date
head_date = current_date.strftime("%Y/%m/%d(#{wd[current_date.wday]})")
puts "\n#{head_date}"
previous_date = current_date
end
night_hour = ""
if e[:start].hour < 6
night_hour = (e[:start].hour + 24).to_s
elsif e[:start].hour < 10
night_hour = "0" + (e[:start].hour).to_s
else
night_hour = (e[:start].hour).to_s
end
puts "anime/#{season}/#{e[:title]}"
puts "#{e[:type]} #{night_hour}:#{e[:start].strftime("%M")} #{e[:station]}"
puts
end
puts
# スペアの方の出力
puts "■■■ スペア ■■■"
puts
puts "■スペアなし"
no_spare_list.each do |e|
puts e[:title]
end
puts
previous_date = Date.new(2000, 1, 1)
spare_list.each do |e|
current_date = (e[:start] - 6.hour).to_date
if previous_date != current_date
head_date = current_date.strftime("%Y/%m/%d(#{wd[current_date.wday]})")
puts "\n#{head_date}"
previous_date = current_date
end
night_hour = ""
if e[:start].hour < 6
night_hour = (e[:start].hour + 24).to_s
elsif e[:start].hour < 10
night_hour = "0" + (e[:start].hour).to_s
else
night_hour = (e[:start].hour).to_s
end
puts "anime/#{season}/#{e[:title]}/spare"
puts "#{e[:type]} #{night_hour}:#{e[:start].strftime("%M")} #{e[:station]}"
puts
end
puts
### ここからダメだった分の出力パート
puts "■■■ 有効な放送なし ■■■"
unwatchable_animes.each do |anime|
# schedulesについて、
puts anime[:title]
end
puts
# 最後に、channels_cannot_watchの中身をsortしてuniqして出力し、取りこぼしがないかどうかを確認しておきたい。
puts "■■■ 諦めたチャンネル ■■■"
puts channels_cannot_watch.sort.uniq
puts
puts "■■■ 元々見る気がないヤツ ■■■"
unneeded_animes.each do |anime|
puts anime[:title]
end
puts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.