Webサイトをスクレイピングしてイベント情報を取得、iCalendar形式で出力してGoogleカレンダーにインポートできるようにした、という話。
リポジトリ
背景
3/13(土)に開催されたIDORISE!! FESTIVAL 2021で初めて観たCYNHNがあまりにかっこよく、楽曲にドハマリしたので現場に行きたいと思いスケジュールをチェック。しかしWebサイトに直接書かれているタイプだったのでなんとかしてGoogleカレンダーに登録したいという気持ちになった。そこでGoogleカレンダーにインポートできるicalendar形式に着目した。
『イナフイナス』がかっこよすぎる。綾瀬志希さん(5人がテーブルに座っているシーンでは真ん中)のダークさがすごくてよい。
『水生』もいいぞ。
なお、CYNHNは"スウィーニー"と読む。ロシア語で青の意(ロシア語では実際にはСиний
と綴る)。余談だがロシア語ではS
の発音はC
で表し、N
の発音はH
で表す。学生の頃に時間割に余裕ができてロシア語入門で1単位取ったがもはやキリル文字を読むのが限界。
現場に行きたいと思い
と書いたものの、4/10(土)に開催されるワンマンの一般チケットは発売と同時にアクセスしたが繋がらず敗北。プレイガイド各位はサーバーを強くしてくれという気持ちが強まった。
icalendar形式 is 何
macOSのカレンダーやGoogleカレンダーといったスケジュール管理アプリに対応したファイル形式。ical
と略される。拡張子は.ical
や.ics
。仕様はRFC 5545で定義されている。
構成は以下のような感じ。
BEGIN:VCALENDAR VERSION:2.0 PRODID:-//会社名//プロダクト名//国名 BEGIN:VEVENT # イベント情報を定義する END:VEVENT END:VCALENDAR
今回のコードではPRODID
は-//nao_y//CYNHN Unofficial Calendar//JP
としている。nao_y
は会社名だった……?
イベント情報には
- SUMMARY: イベント名称
- DESCRIPTION: 概要
- CATEGORY: カテゴリ
- DTSTART: 開始時刻
- DTEND: 終了時刻
などがある。
例
BEGIN:VCALENDAR VERSION:2.0 PRODID:-//nao_y//CYNHN Unofficial Calendar//JP BEGIN: VEVENT SUMMARY:CYNHN ONE MAN LIVE「Blue Spring」 東京・harevutai DTSTART:TZID=Asia/Tokyo;VALUE=DATE-TIME:20210410T143000 DTEND:TZID=Asia/Tokyo;VALUE=DATE-TIME:20210410T170000 CATEGORY:LIVE DESCRIPTION:DESCRIPTION:https://cynhn.com//contents/405411\n\nCYNHN ONE MAN LIVE 「Bl ue Spring」\n2021/4/10(土)\n会場:harevutai(東京都豊島区東 池袋1-19-1 Hareza池袋 1F)\n【1部】開場14:30/開演15:00\n【2 部】開場18:00/開演18:30 . . . END:VEVENT END:VCALENDAR
Pythonパッケージ
Pythonにおいてはicalendarがある。日本語の資料としてはtokibitoさんによる以下の記事がとてもわかりやすい。
スクレイピング対象
CYNHN公式サイトのスケジュール及びイベント詳細ページ。
スケジュールは以下のようにページ内に描画されている。
Headlessでカレンダーが取得できない問題
ぱっと見でJSで描画されていることがわかるのでSeleniumを使うことにした。いちいちブラウザが立ち上がるのも面倒なのでHeadless Chromeを使用。しかしいくら試してもカレンダーのDOMが取得できない。そこでHeadlessを諦めて普通にChromeを使うこととした。
カレンダーの日付がイベントに紐付いていない問題
見た目上は各マスにイベントが収まっているが、DOMを見るとマス内のイベントと日付が紐付いていないことがわかる。そこでイベントのリンクから詳細ページのURLを取得、詳細ページから開催日を取得することとした。
綾瀬志希さんの個展のように連続で開催されるイベントについては初日のみicalendarに登録した。
ラジオ番組の詳細ページ
毎週水曜にCYNHNメンバーが出演するラジオ番組があるが、この詳細ページは初回放送時のものに固定されており放送日が取得できない。そこでラジオ番組については詳細ページを参照せず、その月の木曜日を算出することとした。
calendarモジュールのmonthdays2calendar(year, month)
を使うことで指定した年月の週のリストが取得できる。
cal = calendar.Calendar(today.year) weeks = cal.monthdays2calendar(today.year, today.month) radio_days = [] for week in weeks: for day in week: if day[1] == 3: #木曜日は3で表現される . . .
今回のソースコードではadd_radio_schedule
という関数でやっている。
https://github.com/NaoY-2501/ical_with_idol/blob/master/ical_with_idol.py#L150L182
Pythonでicalendar形式
スクレイピングにおけるつらみをつらつらと書いたが、ようやく本題。
カレンダーに予定を追加する
基本的にはCalendar
オブジェクトのインスタンスを作り、そこにEvent
オブジェクトのインスタンスを追加していくといった感じになる。
from datetime import datetime from icalendar import Calendar, Event import pytz ical = Calendar() # calendarという変数名にしたくなるがcalendarオブジェクトと被るので注意 ical.add('version', '2.0') ical.add('prodid', '-//nao_y//Sample Calendar//JP') event = Event() event.add('summary', 'アイドルで理解するiCalendarを書き上げる') event.add('description', '投稿するところまでやる') event.add('category', 'ブログ執筆') event.add('dtstart', datetime(2021, 3, 30, 19, 0, 0, tzinfo=pytz.timezone('Asia/Tokyo'))) event.add('dtend', datetime(2021, 3, 30, 20, 0, 0, tzinfo=pytz.timezone('Asia/Tokyo'))) ical.add_component(event)
これでひとまずカレンダーに予定を追加することができた。
Calendar.add()
とEvent.add()
の第一引数はiCalendar形式のVCALENDAR, VEVENTに定義する項目と一致するが、小文字であることに注意。
カレンダーを出力する
出力にはCalendar.to_ical()
を使う。
with open('mycalendar.ics', 'wb') as fout: fout.write(ical.to_ical()
出力されるファイルはUTF-8だが、書き込むときにはバイナリとして扱う。
できあがったicsファイルはGitHubに置いておけばRAWの方のURLでGoogleカレンダーにインポートできるし、ローカルではmacOSのカレンダーアプリに登録できる。
今回作成したカレンダーは https://raw.githubusercontent.com/NaoY-2501/ical_with_idol/master/CYNHN_Unofficial_Calendar.ics
から参照できる。もしよかったらGoogleカレンダーに登録してみてください。(記事執筆時点では)3/1~4/10までのCYNHNのスケジュールが見れます。
ところでGitHubに上げたicalファイルを更新したらGoogleカレンダーの方にも反映されるんだろうか。まだ試してないのでそのあたりがわかっていない。カレンダーを更新すればいけそうな気もするが。
今後の課題
数ヶ月先までのスケジュールの取得
Seleniumを使っているので翌月に送るボタンを押せばいいように思うが、スクレイピングが一筋縄でいかない構造のWebページなので苦戦しそう。
自動化
月次作業めいて手動で更新するのもいいが、面倒なのでEC2あたりにスクリプトを置いてicsファイルのpushまで自動化したい。 しかし実際にブラウザを開いているのでサーバー上でできるのか、という懸念がある。