福井高専の行事予定からWebカレンダーを作成してみた

こんにちは。すずともです。
この4月で、某高専(まぁ調べればすぐに分かるけどw)の5年生になりました🎉

新学年になる前にいつもしていることがあります。それは、
**自分のカレンダーに一年間の行事予定を追加する**
ことです。

意外と行事予定ってあんまり担任から言われないので、当日になって「あれ、今日短縮授業(いつもより10分短い授業)だっけ？」「今日、大掃除なん？」って思うことが多々あります。
なので、こうしていつも行事予定をカレンダーに登録しているわけです。

毎年、作業しながら「学校でカレンダー共有してくれや」と思うわけです。

まぁ、そんなことを思いながら今年も作業しようと思った時、これHPからプログラミングでとってきた方が早くね。

そんなわけで、初めての「Webスクレイピング」をやってみることにしました。

# 使用言語・ランタイム
言語はTypescriptで、Deno信者なのでDeno使います😀

Denoについてちと宣伝。
[Deno](https://deno.land/)とはJavascript／Typescriptを実行できるランタイムです。Nodejs開発者によるNodejsの問題点などを克服するために作られました。
![deno-logo](https://github.com/denoland/deno/blob/main/docs/images/deno3.png?raw=true)
かわいいDenoちゃん。

開発もホットで、日々バージョンアップを遂げています。ここ最近のイベントは、Denoが会社化したことですかねぇ…。まぁ、Denoはいいぞぉ。

# 使用モジュール
Deno布教も終わったところで、次は使用するモジュール。

Denoの便利なところはモジュールを全てWebから直接仕入れるというところです。(もうちょっとうまい言い方あると思うけど…)
Nodejsみたいに、npmでモジュールをインストールして…という手間が必要ありません。というかそのようにしてモジュールを使えません。(`require`を使えない)
実行時に追加でモジュールをインストールすることなく使えるのもDenoのいいところです。

ということで、今回使用するモジュールは
「[Deno DOM](https://deno.land/x/deno_dom@v0.1.7-alpha)」
というモジュール。DenoでHTMLパースを行うことのできるモジュールです。
まだ0.1.7α版ということで伸びしろはまだまだありますが、基本的なDOM解析であれば十分対応できます。というか、[ドキュメンテーション](https://deno.land/x/deno_dom@v0.1.7-alpha#documentation)の欄には

>Refer to MDN (Mozilla Developer Network) for documentation. If there are inconsistencies (that aren't a result of legacy APIs) file an issue.

意訳すると、「MDNのページ(Javascriptのリファレンスページ)みて使って。違ったら言ってね」という感じで、普通のDOM操作と同じように使えるそうです。

また、Typescript対応ということで、型チェック、補完などの機能も存分に使えます。

# スクレイピング
今回スクレイピング対象の高専は、[福井高専](https://www.fukui-nct.ac.jp/)。
行事予定はこのページにあります。
[行事予定 – 福井高専](https://www.fukui-nct.ac.jp/life/event/)

まずは、fetchでHTMLを取得。

```typescript
const res = await fetch(url);
const html = await res.text();
```

2行で書けて簡単！トップレベルでawaitが使えるのもDenoの魅力。

さっきのモジュールを使って、bodyを取得。

```typescript
const dom = await getDOM("https://www.fukui-nct.ac.jp/life/event/");
const body = dom.body;
```

そして、DOM解析して年月日、行事を取得します。
解析の流れとしては下記のような感じ。

1. `entry-content`というIDのついた要素を取得する。
1. `entry-content`内のh2タグを取得しテキストを取ってくる。
1. テキストから数字のみを正規表現で抽出し、それを年度とする。
1. `entry-content`内のtableを`getElementsByTagName`関数で取得。tableは月ごとに分かれているので、インデックスを元に計算すれば月が分かる。
1. 各table内にあるtrを同じく`getElementsByTagName`で取得。trは行事ごとに分かれている。
1. 各tr内にあるtdをまたまた`getElementsByTagName`で取得。ここで取得した配列の0番目は何日かを表すセル。1番目は行事の内容を表すセルとなる。
1. 0番目の要素のテキストから正規表現で数字を抽出。これがこの行事のある日付になる。
1. 1番目の要素のテキストがそのまま行事内容になる。

というように、table(月)内のtr内のtd(日、行事)という感じで取得できます。

コードは以下のようになりました。(最終的な形なので、まだ説明していない処理なども書いてあります。)
結構短いです。

```typescript
const titleH2 = body.querySelector("#entry-content h2");
if (!titleH2) return;
const yearMatch = titleH2.textContent.match(/\d+/);
if (!yearMatch || yearMatch.length < 1) return;
const year = parseInt(yearMatch[0]);
console.log(year + "年度");

body.getElementById("entry-content")
  ?.getElementsByTagName("table").forEach(
    (monthTable, i) => {
      const month = (i + 4) % 12;
      console.log(month + "月");
       monthTable.getElementsByTagName("tr").forEach((dayTr) => {
        const dayTds = dayTr.getElementsByTagName("td");
        if (dayTds.length == 2) {
          const [dayTd, eventTd] = dayTds;
          const dayMatch = dayTd.textContent.match(/\d+/);
          if (!dayMatch || dayMatch.length < 1) return;
          const day = parseInt(dayMatch[0]);
          const summary = eventTd.textContent;
          console.log(day + "日", summary);
        }
      });
    },
  );
```

# スクレイピング完了！
これで無事スクレイピング出来ました！
実行結果はこんな感じです。(2021年4月4日時点)

```
2021年度
4月
1日 春季休業（～5日）
5日 入寮式、新入寮生オリエンテーション、開寮
6日 第57回入学式、HR（1～3年）、教科書販売（2・3年）、保護者懇談会
7日 一斉健康診断（本科・専攻科）、専攻科オリエンテーション、HR（4・5年）、教科書販売（4・5年、専攻科）
8日 前期授業開始
14日 短縮授業、放課後：新入生歓迎会、クラブ紹介
17日 新入生オリエンテーション研修（本科1年）
21日 放課後：校長表彰・校長訓示、学生総会
24日 開校記念日
29日 球技大会
5月
15日 舞鶴高専交歓試合
19日 TOEIC IPテスト（4年）
22日 専攻科推薦選抜、寮祭（～23日）
6月
4日 休業、福井県高等学校春季総合体育大会予選（～6日）
10日 本科前期中間学力確認週間（～21日）
26日 専攻科学力選抜
7月
3日 北陸地区高等専門学校体育大会（富山高専）（～4日）
8日 在学生保護者対象授業参観（～9日）
17日 学生寮保護者会
20日 木曜日の授業
21日 金曜日の授業
30日 専攻科ガイダンス
8月
5日 本科・専攻科前期期末試験（～12日）
12日 試験終了後：HR、大掃除
13日 夏季休業（～9月17日）
14日 閉寮
9月
20日 開寮
21日 試験返却・学力強化期間（～24日）
27日 休業
28日 休講、体育祭（延期の場合は休業）
29日 体育祭予備日
30日 後期授業開始、大掃除、HR
10月
1日 （キャンパスツアー準備）
2日 キャンパスツアー
3日 午前：キャンパスツアー
6日 放課後：専攻科インターンシップ報告会
9日 専攻科・大学・大学院合同説明会
13日 （弁論大会準備）
14日 休講、午前：弁論大会　午後：高専祭準備・クリーン大作戦
15日 高専祭（～17日）
18日 休講（高専祭片付け）、午後：本科4年校外実習発表会
21日 北陸技術交流テクノフェア（～22日）
24日 東海・北陸地区高等専門学校ロボットコンテスト（福井高専）
28日 在学生保護者対象授業参観（～29日）
30日 保護者懇談会
11月
8日 研修旅行（3年）（～12日）
11日 文化体験日（1・2・4・5年、専攻科1・2年）
14日 技術英語検定（2年）
26日 本科後期中間学力確認週間（～12月7日）
28日 全国高等専門学校ロボットコンテスト（両国国技館）
0月
2日 PROGテスト（～3日）
3日 専攻科授業（水曜の授業）
4日 全国高専デザインコンペティション（呉高専）（～5日）
6日 専攻科授業（～7日）
15日 Jointフォーラム
22日 授業終了後：HR、大掃除
25日 閉寮
27日 冬季休業（～1月7日）
1月
10日 開寮
11日 授業再開
12日 月曜日の授業
19日 校長表彰・校長講話
26日 放課後：特別研究Ⅱ発表会（専攻科2年）
27日 短縮授業、放課後：特別研究Ⅰ発表会（専攻科1年）
2月
7日 専攻科2年後期期末試験（～8日）、専攻科1年休業（～9日）
9日 専攻科2年試験返却（～10日）
10日 本科・専攻科1年後期期末試験（～17日）
14日 専攻科2年休業（～17日）
17日 試験終了後：大掃除、HR
18日 授業予備日（休業）
21日 休業（～22日）
24日 試験返却（～3月1日）、特別学習（～3月2日）、専攻科2年休業（～3月1日）
3月
2日 本科5年卒業研究発表
3日 休業（～17日）、本科5年卒業研究発表、5年HR、達成度評価シート記入
4日 キャリア教育セミナー（予定）
5日 閉寮
18日 卒業式・修了式
22日 学年末休業（～31日）
```

ちゃんと取得できてますね！
さて、今回の目的はここで終わりではありません。きちんとカレンダーに出力しなければなりません。
ここで活躍するのが、iCalendar形式と呼ばれるファイル形式です。

# iCalendar形式って？
iCalendar形式とは、カレンダーの予定たちを保存するファイル形式です。Googleカレンダーで作成したカレンダーを他のカレンダーで使うために出力するときにも使われる形式で、ほとんどのカレンダーアプリが対応しています。拡張子は`ics`です。

# iCalendar形式に出力！
ってことで、スクレイピングしたデータをiCalendar形式に変換してファイルに保存します。

流れは、カレンダークラスを作成して、イベントを追加して、iCalendar形式に変換したテキストを保存！って感じ。
実際に以下のようには使えないけれども感じでいうと下記のようになります。

```typescript
const calendar = new VCalendar();
calendar.addEvent(new Event({dtStart:""/*開始日時*/,dtEnd:""/*終了日時*/,summary:""/*内容*/}));
// addEventを追加する予定の数だけ行う...

const icalText = calendar.toICSString();
Deno.writeTextFileSync("fukui.ics",icalText);
```

DenoでiCalender形式に出力できるクラスが見つからなかったので自作しました。
先ほど年月日、行事を抽出したので、このクラスを使ってイベントを登録していきます。
さいごに`toICSString()`を使ってiCalender形式のテキストを生成したら、`Deno.writeTextFileSync`で書き込むだけ。

# 実際に使ってみる
出来たファイルをGoogleカレンダーでインポートしてみると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255360/00802464-0ba7-041b-c65c-789030c7f9e5.png)

例えば4月はこんな感じ。実際のHPと比べてもきちんとインポートできてそうな感じですね。
↓実際のHPの行事予定↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255360/c3b02bc8-57f3-862b-ebec-b5752cff8f34.png)

# Github Pagesで公開
これが本当にやりたかったこと。
Githubに作成したicsファイルを置いて、Github Pagesで公開！
すると、GoogleカレンダーやOutlookカレンダー、iPhoneのカレンダーにもURLから追加することができます。それも、このicsファイルが更新されたら、追加したカレンダーも自動で更新してくれます！

ということで、URLはこちら！
https://kamekyame.github.io/kosen-calendar/fukui/fukui.ics

各サービスでの追加方法は以下のようになります。
なんと、iPhoneではこのリンクをタップするだけで追加出来ちゃいます！便利ですねぇ。

|サービス|対応状況|追加方法
|--|:--:|--|
|Outlook.com|○|「予定表を追加」→「Webから定期受信」
|Googleカレンダー|○|「他のカレンダー」横の＋マーク→「URLで追加」
|iPhoneカレンダー|○|iPhoneから下の追加したい高専のURLをタップ|

# 高専カレンダー

![kosen-calendar logo](https://kamekyame.github.io/kosen-calendar/img/banner.png)

お気づきの方がいるかもしれませんが、上記Githubのリポジトリ名は「kosen-calendar」です。
福井高専だけでなく、全国高専のカレンダーをここにまとめていきたいと思っています！

現在icsファイルでの行事予定提供ができているのは、

- 長岡高専
- 石川高専
- 福井高専
- 弓削商船高専
- 北九州高専
- 熊本高専(八代)

の6校になります！
たった6校って？実はこのなかで石川・弓削以外の4校、HTMLで行事予定を公開している高専なんです。他の高専はPDFでの提供、または、2021年度の行事予定が更新されていない学校になります。

このissueにも未追加の高専がありますが、8割ぐらい？(計算してないけど)PDFでの提供となっています。(まだissueが48個もあるよぉ…)
https://github.com/kamekyame/kosen-calendar/issues

# 協力のお願い
ぜひ、こんなつまらん記事を読んでるあなた！。スクレイピングしてみたくないですか？
俺が、私がスクレイピングしてやるぜって方、ぜひ協力お願いします🙇🙇
スクリプトがそろったら、1年ごとにGithub Actionsで更新自動化…なんてことも

Githubはこちらになります！PRお待ちしております！(特にPRテンプレートとか無いので自由に作ってください。icsファイルが出力できてれば文句言いません。たぶん)

[kosen-calendar - Github](https://github.com/kamekyame/kosen-calendar)

# 最後に
最後まで記事を読んで下さりありがとうございます。
僕の他の記事見てくれればわかりますが、bot系よく作ってます。ぜひ僕のTwitter botも可愛がってあげてください(^▽^)/
高専カレンダーの更新状況もツイートします。(雑多垢なので他にもしますが…)
[Twitter(@SuzuTomo2001)](https://twitter.com/SuzuTomo2001)


弓削商船高専は神。というのも学校が公式でGoogleカレンダー共有してた…
石川高専は有志の方が手打ちで作って下さりました。神。