粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

Python Gmail API を用いてメールの締切を自動でリストアップする①

あらまし

最近謎にものづくりのモチベがあります。昨日メール見ていて締切多すぎぃ!ってなったのでメールから自動で締切をリストアップしてくれる手法がなんかないかなと思ってGmail APIのドキュメントとにらめっこしてきました。

目指したい理想像

  • Gmail に届いているメールから「締切」に関係するワードを抽出
  • 同メールから「日付」情報を抽出

Gmail APIを用いたメール抽出

まずはQuickStartのガイドから入っていきます。

developers.google.com

ここではまず,Googleの情報にアクセスする認証キー的なものをダウンロードできます。 キーの仕様についてはこっちを参照すると良いでしょう。

APIの立ち上げ

上の手順でquickstart.pyというサンプルコードを実行すると思います。 実質的なサービスの立ち上げは,以下の箇所にあるようです。

service = build('gmail', 'v1', credentials=creds)

ここでcredentialsというのが初回の登録でDLしたcredential.jsonというAPIの認証キーです。 build関数の仕様に依るとgmailのサービスを'v1'のバージョンで起動するということみたいですね。

Queryとマッチングするメッセージを検索

メールの検索ですがAPIがきちんと準備されています。

developers.google.com

必要なものはだいたい以下の通りです。

  • 先程立ち上げたservice
  • ユーザID :メールアドレス or 'me'(自分自身のこと)
  • 検索クエリ:検索文字列。ここでは「締切」などとする
  • maxResults:検索上限。時間を気にするなら入れておくといい。

messagesに帰ってくるのはメール情報のidの配列です。

response = service.users().messages().list(userId=user_id,
                                           q=query,maxResults=maxlistnum).execute()
messages = []
if 'messages' in response:
  messages.extend(response['messages'])

サンプルコードでは nextPageTokenなるものを使ってますが,これは構造体や配列を辿る時の現在地を表すポインタ的なやつです。

idからメッセージの本文などを抽出

先程の検索ではメッセージのidを入手したのでget関数を用いてメッセージの中身を取得します。

developers.google.com

ここのサンプルコードの困った点はマルチバイト文字が含まれているときにエンコーディングの問題が発生するということです。

APIの記述的にはMIMEという形式の方を推していそうですが,メールに依って文字化けをしたりしなかったりして半日では解決できなかったので今回は採用を見送りました。

それで代わりに書いた関数がこれ。(クラスの形式になってるけど直すの面倒なので各自解釈してください)

    def GetMessageSubjectAndBody(self, msg_id):
        """Get a Message and use it to create a MIME Message.

        Args:

            msg_id: The ID of the Message required.

        Returns:
            A MIME Message, consisting of data from Message.
        """
        try:
            message = self.service.users().messages().get(userId=self.user_id, id=msg_id,
                                                    ).execute()
            body = ""
            subject = ""

            if message['payload']['body']['size']:
                msg_str = base64.urlsafe_b64decode(message['payload']['body']['data'])
                body = msg_str.decode('utf-8')

            if 'headers' in message['payload']:
                for header in message['payload']['headers']:
                    if header['name'] == 'Subject':
                        subject = header['value']

            return subject, body
        except errors.HttpError as error:
            print('An error occurred: %s' % error)

基本的にはmessage['payload']['body']['data']の中に本文がありますが,稀にこのデータが空の場合があります。今の所そういうのは真面目なメールではなかったので例外処理をして回避しています。

また,同様にmessage['payload']['header']の中に件名を保管している箇所があるのでそこも掘り起こしています。気になる方は一度Jupyterなどで中身を見てみると良いでしょう。

提案アルゴリズム

上記の手段でメールの本文を検索して見ることができるようになりました。 これをどのように使っていくかについて書いていきます。

締切に関する語句の検索(メール検索時)

締切と言っても様々な表記があります。その全てを検索クエリに含める必要があるのですが,その時の書き方は以下のようにORでつなぐことで実現します。

"締め切り OR 〆切 OR 締切 OR 締切り OR deadline OR DEADLINE"

もちろん,普段gmailの検索に用いるクエリも使用可能で,ここにあるように, 重要なメールであれば ”AND is:important”とかつけて検索することもできます。

締切の日時の検索(メール内検索)

メール本文を抽出した後,締切に関する日時を検索します。日時というのは意外とメールの中で自然に使われているので"締切"などのワードの近くを検索したいです。

従って,

  • 締切に関連する語句の付近の文章抽出(行単位or文字数で)
  • 抽出した文章から日付の抽出

という手順を取りました。

日付の抽出には非常によく整備されたツールがあるのですが,残念ながら日本語対応しておらず,2バイト文字に対して誤作動的なのを起こすので今回は採用をやめました。 もし使うなら2バイト文字があるかどうか判別してから用いればよいかと思います。海外の方と多く連絡するのであれば導入も考えます。(今のところはそんなにない) github.com

代わりに以下の正規表現を用意しreモジュールを用いて検索をかけています。(正規表現初めて使ったので深く理解していない)

['(\d{1,2})/(\d{1,2})', '(\d{1,2})月(\d{1,2})日']

この表現を用いることで「3/4」や「7月4日」といった表現をキャッチできます。半角全角にかかわらずキャッチできるので偉い。

まとめてデータ化

以上の手順で,締切に関する語句を含んだメールから締切の日時と件名を抽出できました。

これをまとめてデータ化してGoogleカレンダーにぶち込むなどしたいですが,ちょっと息切れしたので辞書型に詰めてひとまず完成とします。

ちょっと見せるとしうかつだらけですね。なおIDは加工しました。検索できないよ。 見学会系はちょっとたくさん締切を発行してきますね。他の用途にも使えそうなので今回遊んでみてまぁまぁよかったんじゃないかなと思ってます。

          {'deadlines': [[3, 18]],
           'message_id': '1697f3bcab',
           'subject': '【3月18日(月)午前9時締切】ソニー・インタラクティブエンタテインメント\u3000'
                      '技術系・事務系エントリーシート受付中'},
          {'deadlines': [[3, 8], [4, 3], [3, 8], [5, 13]],
           'message_id': '1695b47b1',
           'subject': '【日本精工(株)】ログインのお願い:【総合職】応募受付開始しました。'},
          {'deadlines': [[3, 1]],
           'message_id': '16933597b7',
           'subject': '【重要なお知らせ】3月1日0時より『まとめてプレエントリー』機能をご利用いただけます'},
          {'deadlines': [[3, 1]],
           'message_id': '16923774f',
           'subject': '【重要】2/27(水)~2/28(木)プレエントリー受付開始準備による一部機能停止のご案内'},
          {'deadlines': [[1, 31]],
           'message_id': '168944a8b6',
           'subject': ' 【特別号】有効期限が近づいているマイルやe JALポイントはございませんか? / 【先得】 '
                      'JMB会員先行予約サービス 受付間もなく開始!'},
          {'deadlines': [[1, 31]],
           'message_id': '16886a4406',
           'subject': '【UTAS】(周知)【1/31まで deadline Jan '
                      '31】情報セキュリティ教育について/e-learning(Information Security '
                      'Education)'},
          {'deadlines': [[1, 23]],
           'message_id': '1686095a6',
           'subject': '【ご好評につき2期募集開催!】マーケティング、商品企画などコースに特化した短期インターンプログラム"Business '
                      'Master Program"'},
          {'deadlines': [[1, 31]],
           'message_id': '1683af6025',
           'subject': '【2019年1月31日締切】 JAL旅行積立 ボーナスマイルキャンペーン実施中!'},
          {'deadlines': [[1, 8], [1, 8]],
           'message_id': '16827b89',
           'subject': '【締切1/8(火)10:00】(事務系・技術系)ソニー インターンシッププログラム エントリー受付中'},

今後やりたいこと(誰かにやってほしいこと)

今後の課題は,

  • メールのid,件名,抽出した締切 を含んだ適切なデータ構造
  • 件名や締切で検索をかけることができ,APIを叩いて本文全文を参照できるようにする
  • Google Calendarなどに見やすく一覧でマッピングする
  • 重要な締切かどうかまで判別してくれるとめちゃ助かる

といったところでしょうか。 コード公開したら誰か引き継いでくれたりしないだろうか。