PythonでGmail APIに触れてみる: 検索してみよう

PythonでGmail APIに触れてみる: 検索してみよう


PythonでGmail APIに触れてみるシリーズ。前回までメールの本文を見たり添付ファイルを取得する を扱いました。

今回は一歩手前の段階として、メールの検索をしてみましょう。

過去シリーズはこちら

検索してみよう

Gmailの検索はGmailのWEBアプリやスマホアプリでもできる機能ですが、同様にAPIでもできます。まずはラベルを使って絞る方法です。

受信トレイもラベル

まず前提として、Gmailはメールをフォルダーで整理するのではなくラベルを使って整理します。フォルダーのような意味にもなりますが、メールに対して複数のグルーピングができる意味です。Gmailが登場した当初から利用できたので当たり前に使う方も多いと思います。

Gmailのラベルのシステムは面白くて、受信トレイと呼ばれる一見フォルダーっぽいものもラベルとして扱われています。受信トレイにあるメールはINBOXというラベルがついています。

ラベル自体は、ラベル一覧を取得することでわかります。ラベル一覧はusers.labels.listで取得できます。これをもとに検索を行うことも可能です

# get_labels.py
# ラベル収集のコードを載せる

# get_cledential関数はこの記事の付録を参照
from googleauth_util import get_cledential

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]


def main():
    creds = get_cledential(SCOPES)
    try:
        service = build("gmail", "v1", credentials=creds)

        # ラベルの一覧を収集
        results = service.users().labels().list(userId="me").execute()
        labels = results.get("labels", [])
        print("Labels:")
        # ラベル名とIDを表示
        for label in labels:
            print(f"{label['name']}: {label['id']}")

    # エラーハンドリング
    except HttpError as error:
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    main()

出力結果はこんな感じです。

Labels:
CHAT: CHAT
SENT: SENT
INBOX: INBOX
IMPORTANT: IMPORTANT
TRASH: TRASH
DRAFT: DRAFT
SPAM: SPAM
CATEGORY_FORUMS: CATEGORY_FORUMS
CATEGORY_UPDATES: CATEGORY_UPDATES
CATEGORY_PERSONAL: CATEGORY_PERSONAL
CATEGORY_PROMOTIONS: CATEGORY_PROMOTIONS
CATEGORY_SOCIAL: CATEGORY_SOCIAL
STARRED: STARRED
UNREAD: UNREAD
# この先にユーザーが作ったラベルが続く...
# 多くは `Label_[数字]` という名前になっている
# 例: 
# 各サービス: Label_1249324570491865015
# Later: Label_18
# メールマガジン: Label_3

ラベルを指定してメール一覧を取得する

メール一覧を取得するにはusers.messages.listを使います。これにラベルを指定することで、ラベルに属するメールを取得できます。

まずラベルを使ったメール一覧の取得方法です。このAPIのパラメーターにはlabelIdsがあります。これにラベルのIDを指定することで、そのラベルに属するメールを取得できます。

Gmailが標準で持っているラベルのIDは名前とほぼ同じです。ユーザーが作ったラベルにはIDの文字列が割り振られています。

# get_messages_by_label.py
# inboxラベルのIDを指定してメール一覧を取得する

from googleauth_util import get_cledential

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

LABEL_ID = "INBOX"


# MessagePartからヘッダー>sujbectを取得
def get_subject(message_detail):
    return next(
        (
            header["value"]
            for header in message_detail["payload"]["headers"]
            if header["name"] == "Subject"
        ),
        None,
    )


def main():
    creds = get_cledential(SCOPES)
    try:
        service = build("gmail", "v1", credentials=creds)

        # ラベルからメッセージ一覧を取得
        results = (
            service.users().messages().list(userId="me", labelIds=LABEL_ID).execute()
        )
        messages = results.get("messages", [])
        print(f"Messages by {LABEL_ID}")

        # メッセージIDを表示(10件まで)
        for message in messages[:10]:
            # メッセージのサブジェクトを表示
            message_detail = (
                service.users().messages().get(userId="me", id=message["id"]).execute()
            )
            print(f"ID:{message['id']}  - {get_subject(message_detail)}")

    # エラーハンドリング
    except HttpError as error:
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    main()

Gmailのフィルターとラベル機能を使う

WEBアプリ側のフィルター機能を使ってラベル付けをすることで、APIでの検索結果にも反映されます。

この先で扱うクエリーを使う手段もありますが、実はこのクエリーはWEBアプリ側のフィルター機能とほぼ同じものです。(違いはあるので後述します)

個人的にはクエリーを使っての絞り込みより、ラベルをもとにすることがオススメです。WEBアプリ検索結果をアプリ上で見ながら、自動化しながらも中身のメールを見るときにも便利なので。

クエリーで絞る

users.messages.listで取得するメール一覧は、クエリーを使っても絞ることができます。クエリーはGmail WEBアプリで使う検索ボックスに入力する検索演算子を組み合わせたものと同じものを使います。

クエリーはqパラメーターで指定します。クエリーの書き方はWEBアプリの検索ボックスと同じです。タイトルにGoogleが入ったメールの検索クエリーはsubject:Googleになります。

## クエリーを使ってメール一覧を取得する
from googleauth_util import get_cledential

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

SEARCHQUERY = "subject:Google"


# MessagePartからヘッダー>sujbectを取得
def get_subject(message_detail):
    return next(
        (
            header["value"]
            for header in message_detail["payload"]["headers"]
            if header["name"] == "Subject"
        ),
        None,
    )


def main():
    creds = get_cledential(SCOPES)
    try:
        service = build("gmail", "v1", credentials=creds)

        # ラベルからメッセージ一覧を取得
        results = service.users().messages().list(userId="me", q=SEARCHQUERY).execute()
        messages = results.get("messages", [])

        # メッセージIDを表示(10件まで)
        for message in messages[:10]:
            # メッセージのサブジェクトを表示
            message_detail = (
                service.users().messages().get(userId="me", id=message["id"]).execute()
            )
            print(f"ID:{message['id']}  - {get_subject(message_detail)}")

    # エラーハンドリング
    except HttpError as error:
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    main()

Gmailの検索時に使える検索演算子はこちらのヘルプから見られます。Gmail WEBアプリではフォームで入力する部分も実際はこの演算子の組み合わせで作られています。

Gmail で使用できる検索演算子 - Gmail ヘルプ

この検索演算子ですが、綺麗にメールのフィルターを行うのが難儀な時があります。演算子の結果がどのように反映されているかが把握できないのと、ヘルプに説明がある言葉も、実際に使ってみないとわからないことが多いです。

(画像の例はGmail WEBアプリでの検索結果です。多分検索結果的にはsubjectのみの結果と思われますが、本文にも検索が入ってるように見えてる)

とくにキーワード検索は曖昧な結果を返してくるようです。タイトル綺麗にフィルターが出来ずに結構困っていました。この場合はfromtoなどの検索演算子を使ってキーワード以外の情報で絞り込むなど方法をとりましょう(もしくは大雑把にフィルターをした上でPythonの正規表現などで絞り込むのも1つですね)

また、APIのドキュメントにもWEBアプリの挙動が違う点もあります。Google Workspace内でメールエイリアスを推測する機能やスレッド全体の検索はアプリでしかできないようです。

検索とフィルタの違い: Gmail UI と Gmail API

まとめ

メールの検索について、Gmail WEBアプリ側のラベルを使った絞り込みやクエリーを使う方法を紹介しました。

メール検索と今まで紹介してきた本文内容が見られるようになると、特定の業務や連絡を抽出してデータ活用に繋げられます。そこからメール自体の操作もできますね。

次回はメールスレッドの扱いについて紹介します。

参考

宣伝

所属会社の宣伝です! 自動車プレス金型の機械設計を行う設計事務所で、製品や試作品の3Dモデリングのお仕事も受け付けています。

また製造業の業務、バックオフィスの自動化やその先に繋がるデジタル化、DXに取り組みたい方もご相談承っております。 今回の記事で扱ったGmail等のメールの扱いや、データを抽出して作業の自動化を行いたい方は、ぜひお気軽にお問い合わせください!

付録: get_cledential関数について

get_cledential関数はGmail APIを使うための認証情報を取得する自作関数です。詳しくは前回の記事をご覧ください。