Googleカレンダーの予定を一覧表にしたい場面はよくあります。作業時間の集計、訪問記録の整理、月次報告などです。ただ、Googleカレンダーの標準の書き出し機能で出せるのはCSVではありません。iCalendar形式(拡張子 .ics)のファイルです。

そのため、作業は次の3段階で進めます。

  1. GoogleカレンダーからICSファイルを書き出す
  2. ICSファイルを、日時・件名・場所が並んだ一覧(CSV)に変換する
  3. 変換した表で、終日予定・時刻・繰り返し予定が正しく出ているか確認する

この記事では、手元のパソコンだけで変換する方法を中心に説明します。変換後に見落としやすい点も順に取り上げます。

手順1:GoogleカレンダーからICSファイルを書き出す

書き出しは、パソコンのブラウザで開いたGoogleカレンダーから行います。

すべてのカレンダーをまとめて書き出す

  1. 右上の歯車アイコンから「設定」を開く
  2. 左側のメニューで「インポート/エクスポート」を選ぶ
  3. 「エクスポート」を押す

ZIPファイルがダウンロードされます。展開すると、カレンダーごとに .ics ファイルが入っています。

特定のカレンダーだけを書き出す

設定画面の左側にある「マイカレンダーの設定」から、対象のカレンダーを選びます。その中の「カレンダーをエクスポート」を押すと、こちらもZIP形式でダウンロードされます。

画面の項目名や配置は変わることがあります。見つからないときは、Googleカレンダーのヘルプで「エクスポート」の最新の手順を確認してください。他の人から共有されたカレンダーは、権限によっては書き出しの対象に入らないことがあります。ZIPを展開したら、必要なカレンダーのファイルがそろっているかを最初に確認しましょう。

書き出すときに期間を指定する項目はないため、過去の予定もまとめて入っているものと考えてください。必要な期間は、変換後の表で絞り込みます。

ICSファイルはダブルクリックで開かない

Windowsでは、.ics がOutlookやカレンダーアプリに関連付けられていることがあります。その場合、ダブルクリックすると予定の取り込みが始まってしまうおそれがあります。中身を見るときは、先にメモ帳を起動し、「ファイル」→「開く」でファイルの種類を「すべてのファイル」に切り替えてから開いてください。

手順2:ICSの中身と、一覧表の列との対応を確認する

ICSファイルの中身はテキストです。1件の予定は BEGIN:VEVENT から END:VEVENT までのまとまりとして書かれています。たとえば次のような形です(値は説明のための例です)。

BEGIN:VEVENT
DTSTART:20261005T010000Z
DTEND:20261005T020000Z
SUMMARY:定例ミーティング
LOCATION:会議室A
DESCRIPTION:議題\n・進捗確認
UID:abc123@google.com
END:VEVENT

一覧表にするときは、おもに次の項目を列に割り当てます。

ICSの項目 意味 表の列 変換するときの注意
DTSTART 開始日時 開始日・開始時刻 末尾に Z があればUTC(協定世界時)。日本時間にするには9時間足す
DTEND 終了日時 終了日・終了時刻 終日予定では「最終日の翌日」が書かれている
SUMMARY 件名 件名 \, \; \n のように前に \ が付いた文字を元に戻す
LOCATION 場所 場所 空欄の予定も多い
DESCRIPTION 説明 説明 改行やHTMLタグを含むことがある
RRULE 繰り返しのルール 繰り返し この行がある予定は、1件で複数回分を表している
RECURRENCE-ID 繰り返し予定のうち、個別に変更した回 繰り返し 元の繰り返し予定とは別のまとまりとして書かれる
UID 予定の識別子 UID 繰り返しの元の予定と、個別に変更した回は同じUIDになる

ICSには、ほかにも単純な行単位の処理では崩れる書き方があります。

  • 行の折り返し:長い行は途中で改行され、続きの行は半角スペースで始まります。ICSをそのまま1行ずつExcelへ貼ると、長い説明文が途中で切れたように見えるのはこのためです。
  • 入れ子の通知設定:予定の中に BEGIN:VALARM(通知の設定)が入っていることがあり、その中にも DESCRIPTION が出てきます。予定の説明と取り違えないよう、変換時はこの部分を読み飛ばす必要があります。
  • タイムゾーンの書き方:末尾が Z のUTC表記と、DTSTART;TZID=Asia/Tokyo:20261005T100000 のようにタイムゾーン名が付いた表記が、同じファイルに混ざっていることがあります。

手順3:変換方法を選ぶ

方法 向いている場面 注意点
Pythonで変換する(次の手順4) ファイルを外に出さず手元で変換したい/件数が多い Pythonのインストールが必要。繰り返し予定は展開されず1行のまま
GoogleスプレッドシートとApps Script(後述) 繰り返し予定を1回ずつの行に分けたい ICSを使わず、カレンダーから直接読み出す方法。最初の実行時に権限の許可が必要
Web上の変換サービス 少数の予定を一度だけ変換したい 件名・説明・参加者などが外部に送られる可能性がある。ブラウザ内で処理されるかを、サービスの説明と実際の通信内容で確認してから使う

カレンダーには、取引先名や面談相手、会議のURLなどが含まれていることが少なくありません。業務の予定であれば、手元で変換する方法を優先するのが無難です。

手順4:PythonでICSをCSVに変換する

Python 3がインストールされていることが前提です。PowerShellかコマンドプロンプトで python --version を実行し、バージョンが表示されるか確認してください。

次の内容を、UTF-8のテキストとして ics_to_csv.py という名前で保存します。

import csv
import re
import sys
from datetime import datetime, timedelta, timezone

JST = timezone(timedelta(hours=9))
FIELDS = ['開始日', '開始時刻', '終了日', '終了時刻', '終日', '件名', '場所',
          '説明', '繰り返し', 'タイムゾーン', 'UID']


def unfold(text):
    """空白で始まる折り返し行を、前の行につなげる"""
    lines = []
    for line in text.splitlines():
        if line[:1] in (' ', '\t') and lines:
            lines[-1] += line[1:]
        else:
            lines.append(line)
    return lines


def unescape(value):
    """エスケープされた改行・カンマ・セミコロン・円記号を元に戻す"""
    return re.sub(r'\\([\\;,nN])',
                  lambda m: '\n' if m.group(1) in 'nN' else m.group(1), value)


def parse_dt(params, value, is_end=False):
    """(日付, 時刻, 終日, タイムゾーン) を返す"""
    if 'VALUE=DATE' in params:
        d = datetime.strptime(value, '%Y%m%d')
        if is_end:
            d -= timedelta(days=1)  # 終日予定の終了日は翌日で書かれている
        return d.strftime('%Y/%m/%d'), '', 'はい', ''
    if value.endswith('Z'):
        dt = datetime.strptime(value, '%Y%m%dT%H%M%SZ')
        dt = dt.replace(tzinfo=timezone.utc).astimezone(JST)
        return dt.strftime('%Y/%m/%d'), dt.strftime('%H:%M'), '', 'UTC→日本時間'
    tzid = next((p[5:] for p in params if p.startswith('TZID=')), '指定なし')
    dt = datetime.strptime(value, '%Y%m%dT%H%M%S')
    return dt.strftime('%Y/%m/%d'), dt.strftime('%H:%M'), '', tzid


def main(src, dst):
    with open(src, encoding='utf-8-sig') as f:
        lines = unfold(f.read())

    rows, ev, nested = [], None, 0
    for line in lines:
        if line == 'BEGIN:VEVENT':
            ev, nested = {}, 0
        elif line == 'END:VEVENT':
            if ev is not None:
                rows.append(ev)
            ev = None
        elif ev is None:
            continue
        elif line.startswith('BEGIN:'):
            nested += 1  # 通知設定(VALARM)などの入れ子
        elif line.startswith('END:'):
            nested -= 1
        elif nested == 0 and ':' in line:
            head, value = line.split(':', 1)
            name, *params = head.split(';')
            if name == 'DTSTART':
                ev['開始日'], ev['開始時刻'], ev['終日'], ev['タイムゾーン'] = parse_dt(params, value)
            elif name == 'DTEND':
                ev['終了日'], ev['終了時刻'], _, _ = parse_dt(params, value, is_end=True)
            elif name == 'SUMMARY':
                ev['件名'] = unescape(value)
            elif name == 'LOCATION':
                ev['場所'] = unescape(value)
            elif name == 'DESCRIPTION':
                ev['説明'] = unescape(value)
            elif name == 'RRULE':
                ev['繰り返し'] = value
            elif name == 'RECURRENCE-ID':
                ev['繰り返し'] = '変更された回(元の日時 ' + value + ')'
            elif name == 'UID':
                ev['UID'] = value

    rows.sort(key=lambda r: (r.get('開始日', ''), r.get('開始時刻', '')))
    with open(dst, 'w', encoding='utf-8-sig', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows(rows)
    print(f'{len(rows)} 件を {dst} に書き出しました')


if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])

ICSファイルと同じフォルダで、次のように実行します。ファイル名は、展開したICSファイルの名前に合わせて書き換えてください。

python ics_to_csv.py calendar.ics calendar.csv

このスクリプトは、次の処理をしています。

  • 折り返された行を1行に戻す
  • 予定の中にある通知設定(VALARM)の部分を読み飛ばす
  • 末尾が Z のUTC時刻を日本時間(UTC+9)に変換する
  • TZID 付きの時刻は書かれている時刻のまま出し、「タイムゾーン」列にタイムゾーン名を残す
  • 終日予定は開始時刻を空欄にし、終了日を「翌日」から「最終日」に直す
  • 繰り返しのルールは「繰り返し」列にそのまま残す(回ごとの行には分けない)
  • 開始日時の順に並べ、Excelで文字化けしにくいBOM付きUTF-8で保存する

でき上がったCSVをExcelで開き、作業用に .xlsx で保存し直してから使うと安全です。必要な期間だけ残すときは、「開始日」列にフィルターをかけて絞り込みます。

終了日時が DTEND ではなく所要時間(DURATION)で書かれている予定は、このスクリプトでは終了日・終了時刻が空欄になります。空欄の行があったら、元のICSでその予定の書き方を確認してください。

繰り返し予定は「1行で全回分」になっている点に注意する

ICSでは、毎週の定例会議のような繰り返し予定が、RRULE を持つ1件のまとまりとして書かれています。上のスクリプトで変換しても、繰り返し予定は1行しか出てきません。さらに次のような扱いの違いがあります。

  • 個別に時間を変更した回:RECURRENCE-ID を持つ別のまとまりになり、表にも別の行として出ます
  • 個別に削除した回:元の予定の EXDATE に記録されるだけで、表には行として出ません

この表をそのまま「今月の会議の回数」や「訪問件数」の集計に使うと、件数が実際より少なくなります。予定の種類を一覧で見るだけなら、「繰り返し」列で繰り返し予定だと分かれば十分です。1回ずつの行が必要なときは、次のどちらかで展開します。

  • Pythonで展開する:繰り返し予定を展開できるライブラリ(icalendar や recurring-ical-events など)を使う。インストール方法と使い方は、各ライブラリの公式ドキュメントで確認してください。
  • Apps Scriptでカレンダーから直接読み出す:期間を指定すると、繰り返し予定も1回ずつの予定として取得できます。手順は次のとおりです。

Apps Scriptで期間内の予定を1回ずつ書き出す

  1. 新しいGoogleスプレッドシートを作る
  2. 「拡張機能」→「Apps Script」を開く
  3. 次のコードを貼り付け、期間を書き換えて保存する
  4. 関数 exportEvents を実行する。最初の実行では権限の確認画面が出るので、内容を確認してから許可する
  5. 「予定一覧」シートができたら、「ファイル」→「ダウンロード」からCSVまたはExcel形式で保存する
function exportEvents() {
  const cal = CalendarApp.getDefaultCalendar();  // メインのカレンダー
  const start = new Date(2026, 9, 1);  // 2026/10/01 0:00(月は0から数える)
  const end = new Date(2027, 0, 1);    // 2027/01/01 0:00 の手前まで
  const rows = [['開始', '終了', '終日', '件名', '場所', '説明']];
  cal.getEvents(start, end).forEach(e => {
    rows.push([e.getStartTime(), e.getEndTime(), e.isAllDayEvent() ? 'はい' : '',
               e.getTitle(), e.getLocation(), e.getDescription()]);
  });
  const sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet('予定一覧');
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}

このコードで注意する点は次のとおりです。

  • メイン以外のカレンダーを読むときは、getDefaultCalendar() を getCalendarById('カレンダーID') に置き換えます。カレンダーIDは、そのカレンダーの設定画面の「カレンダーの統合」で確認できます。
  • 同じ名前のシートがあると2回目の実行でエラーになります。前回の「予定一覧」シートを削除するか、名前を変えてから実行してください。
  • 期間の始めや終わりをまたぐ予定も取得されることがあります。
  • 終日予定の終了は「翌日の0:00」として出ます。
  • 時刻はスプレッドシートのタイムゾーンで表示されます。「ファイル」→「設定」のタイムゾーンを、カレンダーのタイムゾーンと合わせておきましょう。
  • 予定が非常に多いと、処理が途中で止まる場合があります。そのときは期間を短く区切って、何回かに分けて実行してください。

変換後に確認するチェックリスト

どの方法で変換しても、表を使い始める前に次の点を確認しておくと、あとで数字が合わない原因を減らせます。

  • 件数:ICSの予定数と、CSVの行数が一致している。ICSの予定数は、PowerShellで (Select-String -Path .\calendar.ics -Pattern '^BEGIN:VEVENT').Count を実行すると数えられる
  • 時刻:カレンダー画面で時刻が分かっている予定を3件ほど選び、一覧の時刻と照合した。9時間ずれていれば、UTCから日本時間への変換が漏れている
  • 終日予定:開始時刻が空欄になっている。複数日にわたる終日予定で、終了日が最終日になっている(翌日になっていない)
  • タイムゾーン列:「指定なし」や Asia/Tokyo 以外の行を確認した。海外のタイムゾーンで作った予定は、その地域の時刻のまま出ているため、日本時間に直すかどうかを決める
  • 繰り返し予定:「繰り返し」列に値がある行は、全回分が展開されていないことを理解したうえで使っている。集計に使う場合は展開済みの表を使う
  • 変更・削除した回:繰り返し予定のうち、時間を変えた回が別の行になっていること、削除した回が行として出ないことを確認した
  • 説明欄:改行で行の高さが大きくなっていないか、HTMLタグや会議URLが入っていないかを見て、不要なら列ごと削除した
  • 期間:過去の予定まで入っているため、必要な期間だけにフィルターで絞り込んだ
  • 文字化け:Excelで件名や場所が正しく表示されている。文字化けしていれば、CSVの文字コード(BOM付きUTF-8かどうか)を確認する
  • 共有前の確認:他の人に渡す前に、説明・場所・参加者など、相手に不要な個人情報や社内情報が残っていない

まとめ

Googleカレンダーの予定を表にするときは、まず「インポート/エクスポート」からICSファイルを書き出し、それを手元でCSVに変換するのが基本の流れです。変換で特に注意したいのは、UTC表記の時刻のずれ、終日予定の終了日、繰り返し予定が1行にまとまっていることの3点です。件数や回数を集計する目的なら、繰り返し予定を展開してから数えましょう。最後に、時刻が分かっている予定と照合して、変換結果を確認してから使い始めてください。

おすすめの記事