-
-
Save Map987/f838d867cb6d46355d3731d613ae78af to your computer and use it in GitHub Desktop.
アキバ総研 (https://akiba-souken.com/) 様の新作アニメリストページから情報を取得し、録画サーバ2台での録画予約リストを作成するための個人的なスクリプト。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### ここから依存パッケージ定義パート | |
# 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