iCalendarデータの仕様とPythonにおける利用方法

## 概要
この記事ではiCalendarデータの基本的な仕様およびPythonでiCalendarデータを扱える[icalendarパッケージ](https://pypi.org/project/icalendar/)についてまとめます。

## iCalendarとは
iCalendarとはインターネットを通してPCやスマートフォンなど異なるデバイス間でカレンダー情報をやりとりするためのデータ形式です。

現在、カレンダー情報を扱うためのもっとも標準的な形式であり、iCalendar形式で作成されたファイルの拡張子は通常`.ics`、`.ical`、`.icalendar`のいずれかになります。

なおiCalendarではTODO情報やジャーナル情報も取り扱うことが可能ですが、それらについての説明は割愛します。

## iCalendarの歴史
iCalendarは、1998年にIETF（Internet Engineering Task Forceの略。インターネット技術の標準化推進団体）によって[RFC2445](https://datatracker.ietf.org/doc/html/rfc2445)が作成され、以来、標準仕様とされてきました。

その後、インターネット技術の発展に合わせて、2009年には大きな改訂を加えた[RFC5545](https://datatracker.ietf.org/doc/html/rfc5545)が策定され、RFC5545の拡張的役割で2016年に[RFC7986](https://datatracker.ietf.org/doc/html/rfc7986)が定義されました。

より詳しい歴史を知りたい場合はこちらの記事（英語）が参考になります。
https://devguide.calconnect.org/iCalendar-Topics/History/ 

## iCalendarデータ形式の構造
###  iCalendarデータの例
 Googleカレンダーをエクスポートすると入手できるicsファイルのデータを例として、iCalendarデータの形式について説明します。

まず画像のような設定で予定を作成しました。

![スクリーンショット 2021-09-17 15.42.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677774/9234d265-025e-77b9-c616-a82a1802df94.png)


```
BEGIN:VCALENDAR 
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:iCalendarテスト
X-WR-TIMEZONE:UTC
BEGIN:VEVENT
DTSTART:20210918T000000Z
DTEND:20210918T010000Z
DTSTAMP:20210917T064347Z
ORGANIZER:mailto:test-organizar@example.com
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=:test@example.com
 il.com;X-NUM-GUESTS=0:mailto:test@example.com
 il.com
CREATED:20210917T064324Z
DESCRIPTION:Googleカレンダーのicsファイルを見る
LAST-MODIFIED:20210917T064324Z
LOCATION:自宅
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:テスト用の予定
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
```

### データ構造と各セクション
上記のデータを見るとわかる通り、iCalendarは全体を`BEGIN:VCALENDAR`と`END:VCALENDAR`で囲み、その中に各プロパティを記述します。

`VCALENDAR`セクション内には`BEGIN:VEVENT`と`END:VEVENT`に囲まれたセクションがあり、ここにはカレンダーの各予定（イベント）ごとの情報が定義されます。

`VCALENDAR`は一度だけしか登場しませんが、`VEVENT`はカレンダーに登録されたイベントの数だけ設定されます。

次にiCalendarの具体的なプロパティについて説明します。

### VCALENDARのプロパティ

| プロパティ名 | 役割 | 
| -------- | -------- | 
| PRODID | カレンダーの識別子  | 
| VERSION | iCalendarデータを生成したアプリケーションの識別情報。サービス名や利用言語情報を組み合わせるケースが多い |
| CALSCALE | カレンダーの暦法。一般的にGREGORIANを指定する |
| METHOD | PUBLISH・REQUEST・CANCELなどの値が入る |

### VEVENTのプロパティ
| プロパティ名 | 内容 | 
| -------- | -------- | 
| DTSTART | イベント開始日時  | 
| DTEND | イベント終了日時 |
| DTSTAMP | iCalendarオブジェクトのインスタンスが作成された日時 |
| ORGANIZER | イベント主催者情報。詳しくは[こちら](#ORGANIZER・ATTENDEEの詳細) |
| ATTENDEE |  イベント参加者情報。詳しくは[こちら](#ORGANIZER・ATTENDEEの詳細) |
| CREATED | イベントが作成された日時 |
| DESCRIPTION | イベントの説明文 |
| LOCATION | イベント開催地 |
| SEQUENCE | イベント情報を変更する度に更新されていくシーケンス番号 |
| STATUS | イベントの状態。一般的にイベント作成時はCONFIRMED、キャンセル時はCANCELLEDが指定される|
| SUMMARY | イベントタイトル |
| TRANSP | イベントを設定した時間の状態。OPAQUEまたはTRANSPARENTという値が入る。TRANSPARENTは空き時間を表すため、会議などの予定の場合OPAQUEを設定する |

### ORGANIZER・ATTENDEEの詳細
ORGANIZER（主催者情報）とATTENDEE（参加者情報）ではカレンダー上での表示名や出欠の返事を求めるかなど細かな設定ができます。

```text:設定例
ORGANIZER;CN=テスト太郎:mailto:test_organizer@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=テスト花子:mailto:test@example.com
```

例えば上記のような設定の場合、イベントの主催者は`test_organizer@example.com`でカレンダー上ではテスト太郎と表示されます。
参加者は`test@example.com`で表示名はテスト花子、イベントには必須参加であり、参加状況についてアクションを求めるということになります。

| プロパティ名 | 内容 | 
| -------- | -------- | 
| CN | カレンダーにおける表示名  | 
| ROLE | 役割。CHAIR（議長）・REQ-PARTICIPANT（必須参加）・OPT-PARTICIPANT（任意参加）などが設定される |
| PARTSTAT | 参加状況について。NEEDS-ACTION・ACCEPTED・DECLINED・TENTATIVE・DELEGATEDなどが設定される |

## PythonでiCalendarデータを扱うには
Pythonには[RFC5545](https://datatracker.ietf.org/doc/html/rfc5545)互換のiCalendarファイル用パーサジェネレータであるicalendarパッケージがあります。
https://pypi.org/project/icalendar

公式サイトにインストール方法や詳細な使い方が載っているためぜひご覧になってください。
https://icalendar.readthedocs.io/en/latest/

ここではicalendarパッケージの基本的な使い方を紹介します。

## icalendarパッケージの使い方
### ジェネレータとして
例として「2021年10月1日12時から一時間〇〇会議あり」という予定をicalendarパッケージを利用して作成してみます。

```py
# coding: utf-8

from datetime import datetime
from icalendar import Calendar, Event, vCalAddress, vText

cal = Calendar()
cal.add('prodid', '-//Test//test-product//ja//')
cal.add('version', '2.0')
cal.add('calscale', 'GREGORIAN')
cal.add('method', 'REQUEST')

# イベント作成
event = Event()
event.add('summary', u'〇〇会議')
event.add('dtstart', datetime(2021, 10, 1, 12))
event.add('dtend', datetime(2021, 10, 1, 13))
event.add('description', u'〇〇についての会議です')

# 開催者設定
organizer = vCalAddress('MAILTO:test_organizer@example.com')
organizer.params['cn'] = vText(u'テスト 太郎')
organizer.params['role'] = vText('CHAIR')
event['organizer'] = organizer

# 参加者設定
attendee = vCalAddress('MAILTO:test@example.com')
attendee.params['cn'] = vText('テスト 花子')
attendee.params['ROLE'] = vText('REQ-PARTICIPANT')
event.add('attendee', attendee)

cal.add_component(event)

# icsファイル作成
f = open('test.ics', 'wb')
f.write(cal.to_ical())
f.close()
```
各プロパティを定義するためには[add()](https://icalendar.readthedocs.io/en/latest/api.html?highlight=walk#icalendar.cal.Component.add)メソッドを利用します。
このコードを実行すると以下のようなicsファイルが作成されます。

```
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//test-product//ja//
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
SUMMARY:〇〇会議
DTSTART;VALUE=DATE-TIME:20211001T120000
DTEND;VALUE=DATE-TIME:20211001T130000
ATTENDEE;CN="テスト 花子";ROLE=REQ-PARTICIPANT:MAILTO:test@example.com
DESCRIPTION:〇〇についての会議です
ORGANIZER;CN="テスト 太郎";ROLE=CHAIR:MAILTO:test_organizer@example.com
END:VEVENT
END:VCALENDAR
```

### パーサとして
先ほど作成したicsファイルをicalendarパッケージでパースしてみます。
まず、[from_ical()](https://icalendar.readthedocs.io/en/latest/api.html?highlight=walk#icalendar.prop.TypesFactory.from_ical)メソッドを使い、iCalendarデータをプリミティブなPythonの型にデコードします。

```py
# coding: utf-8

from datetime import datetime
from icalendar import Calendar

file = open('test.ics')
calendar = Calendar.from_ical(file.read())
print(calendar)
> VCALENDAR({u'CALSCALE': vText('GREGORIAN'), u'VERSION': vText('2.0'), u'METHOD': vText('REQUEST'), u'PRODID': vText('-//Test//test-product//ja//')}, VEVENT({u'ATTENDEE': vCalAddress('MAILTO:test@example.com'), u'DESCRIPTION': vText('〇〇についての会議です'), u'DTEND': <icalendar.prop.vDDDTypes object at 0x10b52a2d0>, u'DTSTART': <icalendar.prop.vDDDTypes object at 0x10b694390>, u'ORGANIZER': vCalAddress('MAILTO:test_organizer@example.com'), u'SUMMARY': vText('〇〇会議')}))
```
[walk()](https://icalendar.readthedocs.io/en/latest/api.html?highlight=walk#icalendar.cal.Component.walk)メソッドを使うと、各プロパティの値を取り出せます。

```py
print(calendar.walk('VCALENDAR')[0]['VERSION'])
> 2.0
print(calendar.walk('VEVENT')[0]['ORGANIZER'])
> MAILTO:test_organizer@example.com
```

## 終わりに
広く普及しているiCalendarですが、なかなかデータの中身をじっくり見る機会はなく、まとめてみると構造はシンプルながらも意外と多様な情報を持っていることに気付かされました。

冒頭で書いた通り、イベント予定の管理以外にTODO情報やジャーナル情報も扱えます。今回は紹介していませんが、それらのためのプロパティも存在するので興味があれば調べてみると面白いかもしれません。

## 参考サイト
iCalendar関連
https://datatracker.ietf.org/doc/html/rfc2445
https://datatracker.ietf.org/doc/html/rfc5545
https://datatracker.ietf.org/doc/html/rfc7986
https://icalendar.org/
https://devguide.calconnect.org/iCalendar-Topics

icalendarパッケージ関連
https://icalendar.readthedocs.io/en/latest/
https://github.com/collective/icalendar