Docomoスケジュール&メモをGoogle Calendarなどに移行する

# Docomo スケジュール&メモ
このアプリはどうやらGoogle Play StoreにもないのでSIMフリースマホにしたら利用し続ける敷居が上がります。APKファイルも全然見つかりませんでした。

## この記事の背景
VCS(vCalendar)ファイルを直接Google Calendarでも読み込むことが可能**らしい**ですが、4000件Overのアイテムを直接ブラウザからインポートするのはできませんでした。
(API経由なら行けるかもしれませんが)
そのため、CSVに変換してからアップロードしてみる。ということです。

また、他の人の記事にて、**Windows**マシンの**Outlook**を用意すれば、
[Docomoスケジュール&メモ] => [VCS] => [Outlook] => [ICS] => [Google Calendar]
のように移行することができるみたいですが...

LinuxユーザにとってそのためにWindowsを実行するのはかなり癪です。(?)

## 準備

### スケジュール&メモでの操作

設定のエクスポートなどからVCSファイルを吐き出してください。

### PCでの操作

+ Python実行環境

が必要です。
追加ライブラリは不要です。


### リポジトリのクローン

```
git clone https://github.com/H-goto16/vcs_to_csv
```

### 実行

適切なディレクトリに遷移して

```
python3 main.py input.vcs output.csv
```

ファイル名は適宜変えてください。

また、大きすぎるCSVを細かくするには同梱のプログラム
ファイル名をgoogle_calendar_import.csvにして同じ階層にする。
区切る数を**4**にするなら

```
pytohn3 split_csv.py 4
```

これでOKです。

### Google Calendarでの操作

カレンダーのインポートから先程のCSVをアップロードしたら完了です。
念の為、新しいカレンダーに追加すると失敗したときでも消せるのでオススメです。

## コード解説

```py
def normalize_quoted_printable(text: str) -> str:
    return re.sub(r'=\r?\n', '', text)
```

Quoted-printableエンコーディングで改行を表す =\n や =\r\n を削除します。


```python
def search_field(text: str, pattern: str) -> str:
    match = re.search(pattern, text, re.S)
    if match:
        value = match.group(1)
        value = re.split(r'\n[A-Z\-]+:', value)[0]
        return value.strip()
    return ''
```

正規表現からフィールドを検索する関数。

```py
def parse_vcs(vcs: str) -> List[Dict[str, str]]:
    events = vcs.split('BEGIN:VEVENT')[1:]
    parsed_events = []

    for event in events:
        summary_raw = search_field(event, r'SUMMARY[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        description_raw = search_field(event, r'DESCRIPTION[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        location_raw = search_field(event, r'LOCATION[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        dtstart_raw = search_field(event, r'DTSTART:(\d{8}T\d{6}Z)')
        dtend_raw = search_field(event, r'DTEND:(\d{8}T\d{6}Z)')

        summary = decode_text(summary_raw)
        description = decode_text(description_raw)
        location = decode_text(location_raw)

        start_date, start_time = parse_utc_to_jst(dtstart_raw)
        end_date, end_time = parse_utc_to_jst(dtend_raw)

        parsed_events.append({
            'subject': summary,
            'start_date': start_date,
            'start_time': start_time,
            'end_date': end_date,
            'end_time': end_time,
            'description': description,
            'location': location
        })

    return parsed_events
```

Google Calendarには

`'Subject', 'Start Date', 'Start Time', 'End Date', 'End Time',All Day Event', 'Description', 'Location', 'Private'`



のヘッダが必要です。
それに対応するのをVCSから抽出します。

`subject, start_date, start_time, end_date, end_time, description, location`

になるので、あまっているAll Day Event, PrivateはFalseを入力。

あとは時刻変換、ファイルのopen, writeなどです。

+ コード全体

```python
import re
import quopri
import csv
import sys
from datetime import datetime, timedelta
from typing import List, Dict

def normalize_quoted_printable(text: str) -> str:
    return re.sub(r'=\r?\n', '', text)

def parse_vcs_file(filepath: str) -> List[Dict[str, str]]:
    with open(filepath, 'r', encoding='utf-8') as f:
        vcs = f.read()
    return parse_vcs(vcs)

def parse_vcs(vcs: str) -> List[Dict[str, str]]:
    events = vcs.split('BEGIN:VEVENT')[1:]
    parsed_events = []

    for event in events:
        summary_raw = search_field(event, r'SUMMARY[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        description_raw = search_field(event, r'DESCRIPTION[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        location_raw = search_field(event, r'LOCATION[^:]*:(.+?)(?:\n[A-Z\-]+:|\nEND:VEVENT|\Z)')
        dtstart_raw = search_field(event, r'DTSTART:(\d{8}T\d{6}Z)')
        dtend_raw = search_field(event, r'DTEND:(\d{8}T\d{6}Z)')

        summary = decode_text(summary_raw)
        description = decode_text(description_raw)
        location = decode_text(location_raw)

        start_date, start_time = parse_utc_to_jst(dtstart_raw)
        end_date, end_time = parse_utc_to_jst(dtend_raw)

        parsed_events.append({
            'subject': summary,
            'start_date': start_date,
            'start_time': start_time,
            'end_date': end_date,
            'end_time': end_time,
            'description': description,
            'location': location
        })

    return parsed_events

def search_field(text: str, pattern: str) -> str:
    match = re.search(pattern, text, re.S)
    if match:
        value = match.group(1)
        value = re.split(r'\n[A-Z\-]+:', value)[0]
        return value.strip()
    return ''

def decode_text(input_str: str) -> str:
    if not input_str:
        return ''
    normalized = normalize_quoted_printable(input_str)
    try:
        return quopri.decodestring(normalized).decode('utf-8')
    except Exception:
        return normalized

def parse_utc_to_jst(utc_str: str) -> (str, str):
    if not utc_str:
        return '', ''
    dt_utc = datetime.strptime(utc_str, "%Y%m%dT%H%M%SZ")
    dt_jst = dt_utc + timedelta(hours=9)
    return dt_jst.strftime("%Y/%m/%d"), dt_jst.strftime("%H:%M")

def write_events_to_csv(events, filename):
    fieldnames = [
        'Subject', 'Start Date', 'Start Time', 'End Date', 'End Time',
        'All Day Event', 'Description', 'Location', 'Private'
    ]
    with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        for event in events:
            writer.writerow({
                'Subject': event['subject'],
                'Start Date': event['start_date'],
                'Start Time': event['start_time'],
                'End Date': event['end_date'],
                'End Time': event['end_time'],
                'All Day Event': 'False',
                'Description': event['description'],
                'Location': event['location'],
                'Private': 'False'
            })

if __name__ == "__main__":
    if len(sys.argv) >= 2:
        vcs_file = sys.argv[1]
    else:
        vcs_file = "calendar.vcs"
    if len(sys.argv) >= 3:
        csv_file = sys.argv[2]
    else:
        csv_file = "google_calendar_import.csv"
    events = parse_vcs_file(vcs_file)
    write_events_to_csv(events, csv_file)
```

## 感想
これでDocomoなどのサービスから逃げられます。
Googleなどのサービスにサクッと移行したほうがいいでしょう。

以上です。お疲れ様でした。

## 参考文献

https://www.docomo.ne.jp/service/schedule_memo/usage.html?icid=CRP_SER_schedule_memo_to_CRP_SER_schedule_memo_usage&d=2&p=2,3