見出し画像

GoogleカレンダーからExcelのカレンダーに転記してみた

こんにちは、「笑顔工学」の専門家、木村光範です。
笑顔工学って何??という方は、ぜひ自己紹介をご覧ください!

さて、私、だいたいの予定は「Googleカレンダー」で管理しています。
便利ですよね。
入れ忘れた予定は、完全に抜けてしまって飛ばしちゃうこともあるので注意が必要ですが・・・

そんな私ですが、役員を務めている会社で
「いつ居るのかデスクに貼っておいてほしい」
というリクエストがありまして、、
Googleカレンダーを印刷して対応する、とか、
関係無い詳細な用事や個人的な用事も混じるので、1つ別のカレンダーを作って共有設定したり印刷したりする、とか、考えたのですが、
その会社用に、わかりやすく編集したいというニーズもありまして、Googleカレンダーから、半自動で、まずはExcelにして、それを編集して印刷する、という手法をとろうと思いつきました。

そもそも、ITを活用して省力化しよう、などと言う立場の人間が、そんな「予定の転記」などという作業を手で行うのは、絶対よろしくない、と思いまして、どうやって自動的に転記しようか考えた次第です。

まず、Googleカレンダーのデータは、iCal形式でエクスポートできます。

PCでGoogleカレンダーを開き、左側のマイカレンダーのところの自分のカレンダーの右側をクリックして「設定と共有」をクリックします。

設定と共有のところの下の方

こんなところがありますので、このURLからエクスポートできるわけです。

次に、これをExcelに入れ込みたいのですが、この形式のままではインポートできませんので、iCal形式をcsvなどに変換する必要があります。

そんなときには生成AIにプログラムを組んでもらおう!ということで、
とりあえず、Pythonスクリプトを作成してもらいました。
Pythonの実行環境は別途入れておかなければいけませんので、もし入っていない場合は入れておいてください。(やり方がわからなければ生成AIにでも教えてもらっていただければ・・・)

import csv
import os
import re
from datetime import datetime, date
from icalendar import Calendar
from zoneinfo import ZoneInfo  # Python 3.9以降で使用

def convert_ics_to_csv(ics_file_path, csv_file_path, local_tz_str="Asia/Tokyo"):
    """
    .icsファイルを読み込み、タイムゾーンを考慮しつつ、複数行テキストは1行にまとめてCSVへ書き出す。
    
    :param ics_file_path: 読み込む .ics ファイルのパス
    :param csv_file_path: 出力する .csv ファイルのパス
    :param local_tz_str:  ローカルタイムゾーン名 (IANA形式、例: "Asia/Tokyo")
    """
    local_tz = ZoneInfo(local_tz_str)  # ローカルタイムとして使用するタイムゾーン

    if not os.path.exists(ics_file_path):
        raise FileNotFoundError(f"ICSファイルが見つかりません: {ics_file_path}")
    
    # icalendarで.icsファイルをパース
    with open(ics_file_path, 'rb') as f:
        cal = Calendar.from_ical(f.read())
    
    events_data = []
    
    # VEVENT を探す
    for component in cal.walk("VEVENT"):
        # 各項目を取得
        summary_raw = component.get('SUMMARY', '')
        location_raw = component.get('LOCATION', '')
        description_raw = component.get('DESCRIPTION', '')
        
        # DTSTART, DTEND 取得
        dtstart_raw = component.get('DTSTART').dt
        dtend_raw   = component.get('DTEND').dt
        
        # タイムゾーンを持たない datetime の場合は UTC と仮定して local_tz に変換
        dtstart_local = convert_to_local_datetime(dtstart_raw, local_tz)
        dtend_local   = convert_to_local_datetime(dtend_raw, local_tz)
        
        # 改行を含むテキストを1行にまとめる
        summary = normalize_multiline_text(str(summary_raw))
        location = normalize_multiline_text(str(location_raw))
        description = normalize_multiline_text(str(description_raw))
        
        # 文字列として出力したい形式に整形 (例: "2025-01-30 09:00:00")
        dtstart_str = dtstart_local.strftime("%Y-%m-%d %H:%M:%S")
        dtend_str   = dtend_local.strftime("%Y-%m-%d %H:%M:%S")
        
        events_data.append({
            "SUMMARY": summary,
            "START": dtstart_str,
            "END": dtend_str,
            "LOCATION": location,
            "DESCRIPTION": description,
        })
    
    # CSVに書き込み
    with open(csv_file_path, 'w', newline='', encoding='utf-8') as f:
        fieldnames = ["SUMMARY", "START", "END", "LOCATION", "DESCRIPTION"]
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(events_data)
    
    print(f"変換完了: {csv_file_path}")


def convert_to_local_datetime(dt_value, local_tz):
    """
    iCalendarから取得した dt_value を、指定のローカルタイムゾーンに変換して返す。
    dt_value が date 型なら終日イベントとして 0:00 時刻をセットして tzinfo を付与。
    dt_value が tzなし datetime 型なら UTC と仮定して local_tz に変換。
    """
    if isinstance(dt_value, date) and not isinstance(dt_value, datetime):
        # date 型 (終日) → 0:00としてdatetime化
        dt_value = datetime(dt_value.year, dt_value.month, dt_value.day)
    
    if dt_value.tzinfo is None:
        # タイムゾーン情報がない → UTCとみなしてからローカルに変換
        dt_value = dt_value.replace(tzinfo=ZoneInfo("UTC"))
    return dt_value.astimezone(local_tz)


def normalize_multiline_text(text):
    """
    複数行のテキストを1行にまとめるために、改行(\r, \n)を半角スペースに置き換える。
    連続する空白は1つにまとめ、前後の空白も削除する。
    """
    # \r\nや\nをすべて空白に
    text = re.sub(r'[\r\n]+', ' ', text)
    # 複数スペースが続く場合を1つのスペースに
    text = re.sub(r'\s+', ' ', text)
    # 前後の空白を削除
    return text.strip()


# 以下は、このファイルを直接実行したときのテスト用例です。
if __name__ == "__main__":
    ics_file = "sample.ics"   # 変換したい .ics ファイル
    csv_file = "output.csv"   # 出力したい .csv ファイル
    convert_ics_to_csv(ics_file, csv_file, local_tz_str="Asia/Tokyo")

こんなスクリプトができました。

実は、生成AIに作ってもらって、最初に出てきたスクリプトが、複数行のコメントが複数行になってしまってうまく読み込めなかったので、指摘したら、対応したコードが出てきた次第です。

最後のところに、ics_file と書いてあるのが入力ファイル名、csv_file と書いてあるのが出力ファイル名です。

これを適切に書き換えて、pythonで実行すれば、指定した出力ファイル名のCSVが出てきます。

このCSVは、そのままExcelで開くと、文字コードの関係で文字化けしますので注意です。テキストデータの流し込みで、カンマ区切りに設定する必要があります。

このままでは、Googleカレンダーのすべてのデータが入ってしまって、大変重くなりますので、最近のものだけにExcelでしていきます。本当はPythonコードで対応しても良いかもしれませんね。コードを修正すれば対応できそうではありますが、今回は取りいそぎそのままExcelでやります。

今回、「これに書いてほしい」と渡されたExcelのカレンダーがありまして、、、

こんな感じのカレンダー

このカレンダーに、シートを足して、先ほどのCSVを入れ込みます。
数を数えたかったのでA列を足しました。行数が多いと重くなるので、関係ある行以外は削除します。

予定表シート(A列は手で足しました)

この予定表、上記のExcelカレンダーに反映させるにはどうするか、終日のイベントなどにも対応できるように、、、ということで、これも生成AIと何度か対話して出してきた数式がこちら。ちなみに上の図だと5月3日のところのセルの式になります。これを各セルにコピーして使います。

=TEXTJOIN(", ", TRUE, FILTER(
    IF(INT(予定表!$D$2:$D$500) - INT(予定表!$C$2:$C$500) >= 1,
        予定表!$B$2:$B$500 & IF(INT(予定表!$D$2:$D$500) - INT(予定表!$C$2:$C$500) > 1,
            " (-" & TEXT(INT(予定表!$D$2:$D$500) - 1, "m/d") & ")",
            ""
        ),
        TEXT(予定表!$C$2:$C$500, "hh:mm") & "-" & TEXT(予定表!$D$2:$D$500, "hh:mm") & " " & 予定表!$B$2:$B$500
    ),
    IF(ISNUMBER(予定表!$C$2:$C$500), TRUNC(予定表!$C$2:$C$500) = DATE($B$2, $H$2, N5), FALSE),
    ""
))

重くなるので、500行目まで見るようにしています。最初、$C:$C などとやっていましたが、重くてまともに動きませんでしたので・・・

そんなわけで、ある程度自動化されて、Excelに転記することができるようになりました。

この数式の入った物を、値だけコピー、をして、その後編集してから印刷、とかやれば、だいぶ楽にできる感じです。

今回はとりとめの無いメモのような記事になりましたが、
どうやって省力化するのか、という例として挙げさせていただきました。

完璧な自動化、ではない、このような「プチ自動化」を行っていくのが、「内製化」の強みなのです。
プログラミングの知識など無くても、生成AIをちょっと使うことができれば、この程度のことはできるようになるわけです。

もちろん、根本的な方法としては、適切なGoogleカレンダーの共有、などで、皆で予定が随時見られるようにしていくこと、なのですが、理想論だけではなく、アナログな実世界と繋ぐ必要もあるときには、こんな解決法もありますよ、という一例と思って見て頂ければと思い、この記事を書いてみました。

今回は、ChatGPTを使ってみました。最初はVBAで何とかしようとしたのですが、うまく動かなかったのでPythonで、とした過程なども見えて恥ずかしいのですが、チャット履歴を公開しておきます。

何かの参考になれば幸いです!

最後までお読みいただき、ありがとうございました。

この記事を少しでも良いと思っていただいたら「♡」を押して応援お願いします!noteのアカウントが無くても押せますので、気軽に押していただければ助かります!

 もちろん、フォロー、フィードバックなどをいただければ、さらに喜びます。よろしくお願いします!

 共に学び、成長し、笑顔あふれる社会を作り上げていきましょう。

いいなと思ったら応援しよう!