How to 会議ビッグバン

こんにちは、10Xでコーポレートエンジニアをやっているハリールです。このブログは 会議全部ふっとばして社員の集中力を10xした話(バッグバン) の付録として会議ビッグバンの実現方法についてまとめたものになります。

はじめに

結果的に多くの成果とポジティブフィードバックで完遂できた10Xの会議ビッグバンですが、udonさんのアイデアを初めて聞いた時に、実は強めに反対していました。

実行することで得られるメリットは理解できるものの、安全に実行するためのロジックの準備やテスト期間が短いこと、なによりまずは段階的にルール策定と啓蒙を行い、それでもイシューが解決できない場合に、次のステップとして会議ビッグバンの流れがよいのではと当時は思っていました。

そんな私に対して、それならばとまずは特に会議が多いメンバーに対して実態をヒアリングし、そのほぼ全員が賛同している点、また、ロジック準備・テストのための時間確保に奔走し、なによりできない理由を探すのではなくどうすれば実行できるかに徹底的にこだわるudonさんの熱意に触発され、自分の考えを改め一緒に実現に向けた方法論を模索することになりました。

今思い返すと本当にこの施策に携われてよかったと思っています、この場を借りて udonさんへ感謝の意を表します!!!

流れ

会議ビッグバンでは大まかに

  1. 会議データの取得
  2. 会議データの分析
  3. 会議データの更新

のフェーズに分かれて処理していきます。以降それぞれのフェーズごとにまとめていきます。

会議データの取得

まずは全社員の会議予定を可視化していきます。

Step1. GASによる取得

Googleカレンダーのデータ操作でまず最初にアプローチするのはGAS(Google Apps Script)だと思います。以下がイベントデータ取得部分の抜粋です。

function getCalData(cal, date, email) {
  const list = cal.getEventsForDay(date, {author: email})
  for (let elm of list) {
    if (elm.getVisibility().toString() == 'CONFIDENTIAL' || elm.getVisibility().toString() == 'PRIVATE' ) {
      // 非公開イベントはスキップ
      continue;
    }

    // スプレッドシートへ書き込み(省略)
  }
}

カレンダーから日付とメールアドレスを指定してイベントを取得し、条件に合致したものだけをスプレッドシートに書き込んでいます。なお今回の要件では非公開イベントは除外しました。

あとはこのfunctionを取得したい日付・対象社員のメールアドレスのリストの組み合わせでループすればデータの取得はできそうです。

課題

GASでは一回の起動で最大6分しか実行ができません。そのため上記ロジックの場合分割して処理するなどの工夫が必要になってきます。

単発での実行ではなく、繰り返しでの実行、ましてビッグバンでは更新操作も必要となることを考えるとGASでの実行は現実的ではありません。

試しに一ヶ月間で全社員のデータを分割取得したところ2〜3時間かかってしまいました。

参考:https://developers.google.com/apps-script/guides/services/quotas?hl=ja

なお、実行時間の課題さえクリアできれば有用であり、現在この処理は日時で全社員の会議データを蓄積する処理として活用しています。

Step2. ICSファイルからcsvへのコンバート

GASによる制限・性能問題をクリアし、繰り返し何度も実行可能な状態を実現するため、次に試したのはICSファイルにエクスポートし、そのデータをcsvに変換する方式です。

Googleカレンダーは各自のカレンダーデータを簡単にicsファイルとしてエクスポートすることができます。

参考:https://support.google.com/calendar/answer/37111?hl=ja

また、自分のカレンダー以外にも特定の権限があれば他の人のカレンダーも一括でエクスポートすることができます。試したところ、複数人のカレンダーでも数秒でエクスポートが可能でした。

icsを扱うライブラリもいくつかあるので以下のようなイメージでローカルでcsvに変換してみます。

import icalendar

with open([iscファイル]) as f:
    calendar = icalendar.Calendar.from_ical(f.read())

    for event in calendar.walk('VEVENT'):
        if event.get('CLASS') == 'PRIVATE':
            # 非公開イベントは対象外
            continue
        
        # CSVへの書き込み

これでGASと同程度の柔軟さを持った上で実行時間を圧倒的に短縮することができる、、、はずでした。

課題

icsエクスポートは複数人でも可能なのですが、一定数を超えるとエクスポートされません。実際10人以下で検証した際には一括でエクスポートできましたが、いざ全社員分をとなるとエクスポートされない(エラーにもならない)事がわかりました。ドキュメントの記載は見つけられませんでしたが、何かしら件数の制限があるようです。

また、icsファイルには非公開イベントも含まれることから、一時的にとはいえそれらがファイルとして生成されることを避けるためため、この方法は見送ることにしました。

Step3. OAuth Clientでのデータ処理

最終的に採用したのは、ローカルマシン上からライブラリを経由してGoogle APIを呼び出す方式です。

Google公式ドキュメントの他にも様々な記事があるため環境周りは割愛しますが、この方式によっていつでも全社員分の最新スナップショットを2~3分で取得できるようになりました。

このデータを使えば、会議コスト算出や主催者ごとの傾向などをありのまま可視化することができます。

nextPageToken = '';
while(True):
    eventsResult = service.events().list(
        calendarId=[取得対象者のメールアドレス], 
        timeMin=[取得開始日時], 
        timeMax=[取得終了日時], 
        singleEvents=True,
        pageToken=nextPageToken,
        orderBy='startTime').execute()
    events = eventsResult.get('items', [])
    nextPageToken = eventsResult.get('nextPageToken', '')

    if not events:
        print('event not found.')
        continue

    for event in events:
        if event.get('visibility') == 'private':
            # 非公開イベントはスキップ
            continue
        if event.get('organizer').get('email') != [取得対象者のメールアドレス]:
            # 主催者とカレンダーIDが異なる場合はスキップ(招待されたイベント)
            continue
        
        id = event.get('id') if event.get('recurringEventId') is None else event.get('recurringEventId')
        cache = list(filter(lambda item: item['ID'] == id, result))
        if len(cache) > 0:
            # 繰り返しイベントの重複フィルター
            continue

        ## 書き込み用のデータ生成処理(省略)

    if nextPageToken == '':
        break

フィルター処理1. 招待されたイベント

Step2までと同様に非公開イベントは除外していますがそれ以外にも招待されたイベントは除外しています(コスト算出時のノイズとなるため)。

フィルター処理2. 繰り返しイベントの重複

今回抽出したデータは、最終的には目視などでフィルタした後は、後続となる更新処理(会議の削除や更新)のインプットデータとする予定です。

その際、繰り返しイベントは全件ある必要はないため、recurringEventIdで一意になるように抽出時点でフィルタしています。単純に出力後にフィルタしてもよいのでこのあたりはお好みとなります。

ハマった点1. OAuthユーザーや認可設定の変更方法

大まかな流れは ここ に従って作成しましたが、一度OAuth認証・認可した後にSCOPEを変更する手順が分からずにハマりました。

上記のクイックスタートに従っている場合には、token.jsonというファイルにaccess_tokenなどOAuthに必要な情報が書き込まれているので、このファイルを一度削除することで再度認証・認可を求められ、SCOPEや認証ユーザーを変更することができます。

※ 生成されるファイル名や場所は実装に依ります

ハマった点2. execute呼び出し漏れ

GoogleのAPIを呼ぶ際の作法として、listやgetなどの後にexecuteを実行してようやくAPIが呼ばれます。このexecuteはコールしなくてもエラーが起きるわけではなく、処理自体は正常に終了します(目的のデータが取れないだけ)。

サンプルとして提示されているコードもそうなっているのでコピペしている分には問題ないのですが、自分でコードを組み立てる場合にexecuteを忘れてしまい、原因調査に時間を取られました。

参照:https://github.com/googleapis/google-api-python-client

会議データの分析

取得した全会議データを分析し可視化していきます。詳細は こちら にまとまっています。参照先にもある通り、最終的には以下の条件で削除対象外となる会議を定めました。

ただ、機械的なフィルタではどうしてもヌケモレがあるため、安心安全なビッグバンのため上記にプラスして人手によるチェックで除外するイベントをFixさせました。

課題

会議ビッグバンを単発で終わらせずに、今後も再現可能な形で継続していくための一番の課題が実はこの人手によるチェック部分です。

どうやって目視チェックを減らせるかについては、極論会議ルールの整備と徹底(タグやプレフィックスのルール整備、繰り返しの場合は必ず期限を設定するなど)に尽きるため、今後運用をブラッシュアップさせていきます。

会議データの更新

インプットデータの準備ができたのでいよいよ会議ビッグバンになります。

当初は、事前のバックアップも取得済みのため未来の会議データはすべて思い切って削除する想定でした。

繰り返しではない単発イベントはそのまま削除し、繰り返しイベントも “これ以降のすべての予定” を選択して削除するイメージで考えていました。

ただ、残念ながらAPIではこのオプション相当の挙動を指定することができません

余談1. Google Calendar Events Deleteの挙動

Calendar Eventsオブジェクトは単発のイベントの場合には26桁のIDを持っていますが、繰り返しイベントの場合、以下の通り26桁のIDと日時データの組み合わせになります。

単発イベントのIDの例 繰り返しイベントのIDの例
7ed7uj5aou6b3m48t4kgodt2ql 7ed7uj5aou6b3m48t4kgodt2ql_20231030T200000Z

上記のように26桁のIDでイベントを識別し、その上で繰り返しの場合には日時を付与することで個別に特定できる仕様になっています。なお、繰り返しの場合 recurringEventId というフィールドに別途26桁のIDが設定されています。

そして余談の本題(?)ですが、Calendar Events DeleteAPIではパラメタには上記IDを指定できますが、 繰り返しイベントの場合、26桁のIDを指定すると繰り返しイベントが過去のイベントも含めて全て削除されます。日時付きのフルIDを指定すると、繰り返しイベントのうちの特定イベントのみを削除することができます。

余談2. GoogleカレンダーからEvent IDを確認する方法

APIで取得する以外にもブラウザからイベントIDを確認する方法があります。通常アクセスしているGoogleカレンダーのURLの末尾に ?eventdeb=1 を付与してアクセスします。

IDを確認したいイベントをクリックし、3点ケバブメニュー(?)をクリックすると、通常は表示されない トラブルシューティング情報 というメニューが表示されるのでクリックします。

すると以下の情報が確認できます。このeidがEventIDです。その他、繰り返しイベントの詳細な設定であるRRULEなども確認することができます。

イベントの削除. 単発イベント

ここから本題へ戻ります。上記の通り、単発イベントの場合には単純にdeleteできます。

前述の分析フェーズでの成果物であるcsvファイルをループで確認しながら、単発イベントでかつ削除対象外の場合のみ以下のコードで削除していきます。

service.events().delete(
    calendarId=[対象メールアドレス], 
    eventId=[カレンダーID], 
    sendUpdates='none').execute()

なお、バックアップがあるとはいえ、更新操作となるため入念に準備をしてから実行することをおすすめします。

イベントの更新. 繰り返しイベント

繰り返しイベントに対する削除は、特定イベントかもしくは全件しか対象にはできません。そこで、削除するのではなく繰り返しイベントの終了日を指定する(更新する)ことで将来のイベントを全てなくすことにします。

  1. 対象繰り返しイベントのrecurrenceを取得

    フェーズ1の会議データの取得では実は繰り返しの詳細な設定データは取得していません。単純な削除であれば不要だからです。

    ただ、繰り返しイベントの終了日を設定するためには、現在の繰り返しの設定が必要になります。

    以下のコードでイベントの詳細データを取得し、そこから繰り返しの設定である recurrence を取得することができます。

     service.events().get(
         calendarId=[メールアドレス],
         eventId=[イベントIDの]).execute()
    
  2. recurrenceの更新準備

    上記で取得したデータの recurrence には以下のような繰り返しの詳細が設定されています。

    RRULE:FREQ=WEEKLY;WKST=SU;COUNT=4;BYDAY=SA

    もし期限がある場合には

    RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20240331T145959Z;BYDAY=SA

    のように UNTILというエントリが設定されています。イベントにUNTILがあってもなくても、指定した日付を指定したUNTILをUpsertして準備します。

  3. イベント更新

    イベント更新はUpdatePatchの2つの方法がありますが、特定属性だけを更新するためPatchを利用します。

     service.events().patch(
         calendarId=[メールアドレス],
         eventId=[対象イベントID],
         body={"recurrence": [2で準備したrecurrence]}).execute()
    

これで会議ビッグバンの完成です、、、と言いたいところですが未解決な課題があるのでそれも記しておきます。

未解決の課題

上記の削除・更新で概ね意図した状態にはなったものの、一部で繰り返し期限が設定されているにも関わらずその期限を超えても存在しているイベントがありました。

具体的な条件としては、繰り返しイベントのイレギュラーパターンでこの事象が起きていると考えられます。例えば毎週月曜日開催だが、特定の週だけは火曜日開催となっているケースなどです。

このケースにおける recurrence の指定方法については、もうひと工夫必要なようですが、現在調査中となっており、次回ビッグバンまでには解消させたいと思います。

まとめ

ここまで、会議ビッグバンのためのデータ取得から更新までの試行錯誤やTIPS、ハマった点をまとめました。

ちなみに終始とてもスマート・スムーズにことが運んだような書き方になっているかもしれませんが、準備期間含めてビッグバン当日は想定外のデータが噴出し、一部は手作業で(震えながら)対応しました。

udonさんの記事にある

何がいいたいかというと、再発行できるものはなんとかなるということです。不可逆な人の信頼や家族、大切な友人は失ってはいけません。会議をなくすことはこわい気持ちもあるかもしれませんが、いつでも再発行できるので強い気持ちで明日も生きていきましょう。

この気持ちで完遂することができました。

次回開催時にスムーズに実行できるよう(震えなくていいよう)に、記録としてまとめています、いつか誰かの参考になれば幸いです。

なお、いきなりビッグバンは難しい場合でも、全社的な会議開催状況を取得して可視化するだけでも相当のインサイトが期待できます、ぜひみなさん会議にビッグバンを!