PythonでSlackに実験結果(テキストと画像)を通知する方法(Incoming Webhook)

PythonでSlackにテキストと画像を通知する方法を解説。Incoming Webhookの仕組み、レート制限・ペイロードサイズ・URL漏洩リスクへの対処まで実装します。

機械学習の実験など、時間のかかる処理が完了した際に、その結果をSlackに通知すると非常に便利です。この記事では、Pythonを使ってSlackにテキストメッセージと画像を送信する方法を解説します。あわせて、実運用で見落としがちなレート制限・ペイロードサイズ制限・Webhook URL漏洩リスクといったエッジケースについても、Slack公式ドキュメントの記述に基づいて解説します。

本記事の検証範囲について: セクション1・2の設定手順とスクリーンショット(fig1〜9)は、実際にSlackワークスペースへ通知を送信して取得したものです。一方、レート制限(3.1節)やペイロードサイズ超過時(3.2節)の挙動は、本記事の執筆環境に検証用のSlackワークスペース・Webhook URLが用意されていないため、Slack公式ドキュメントに記載された仕様の引用であり、実測ではありません。区別して記載しています。

Incoming Webhookの仕組み(プロトコルレベルの理解)

Incoming Webhookは、Slack側があらかじめ発行する一意のURL(https://hooks.slack.com/services/T000000/B000000/XXXXXXXXXXXXXXXXXXXXXXXX という形式)に対してHTTP POSTリクエストを送るだけでメッセージを投稿できる仕組みです。ここで重要なのは、このURLそのものが認証情報であるという点です。chat.postMessageのような通常のSlack Web APIとは異なり、Authorizationヘッダーにトークンを別途付与する必要はありません。URLパス末尾のランダムな文字列を知っていること自体が、そのチャンネルに投稿する権限を持っていることの証明になります。言い換えると、Webhook URLは「ベアラートークンがURLに埋め込まれたエンドポイント」であり、漏洩すればそのまま第三者になりすまし投稿を許すことになります(詳細は3.3節)。

リクエスト・レスポンスの形式は次の通りです。

  • リクエスト: POST / Content-Type: application/json / ボディはJSONオブジェクトで、トップレベルに text(プレーンテキストのフォールバック)、blocks(Block Kitのレイアウト配列)、attachments(レガシーな二次的装飾情報の配列)のいずれか、または組み合わせを持つ
  • 成功時のレスポンス: HTTP 200 + ボディはJSONではなくプレーンテキストの ok
  • 失敗時のレスポンス: HTTP 400系のステータス + invalid_payloadchannel_not_foundno_service などのプレーンテキストのエラーコードがボディに返る
  • レート制限超過時のレスポンス: HTTP 429 Too Many Requests + Retry-After ヘッダー(3.1節で詳述)

Incoming Webhookのリクエスト/レスポンスフロー

1. テキストを送信する (Incoming Webhook)

Incoming Webhookは、外部からSlackにメッセージを投稿するための最もシンプルな方法です。特定のURLにHTTPリクエストを送るだけで、指定したチャンネルにメッセージを投稿できます。

Slack側の設定

  1. Slackの「設定と管理」から「アプリを管理する」を選択します。 Slackの設定と管理メニュー

  2. Appディレクトリで「Incoming Webhooks」を検索し、Slackに追加します。 Incoming Webhooksの検索画面 Incoming WebhooksのSlack追加画面

  3. 「Slackに追加」ボタンを押した後、メッセージを投稿したいチャンネルを選択し、「Incoming Webhookインテグレーションの追加」をクリックします。 投稿チャンネルの選択画面

  4. 生成された「Webhook URL」をコピーします。このURLは外部に漏れないように注意して扱ってください。 Webhook URLの表示画面

  5. 設定ページでは、通知時のアイコンやボットの名前をカスタマイズすることも可能です。 Webhookのアイコンと名前のカスタマイズ画面

Pythonコード

slackweb ライブラリを使用すると、簡単に通知を実装できます。

pip install slackweb
import slackweb

# 設定でコピーしたWebhook URLをセット
slack = slackweb.Slack(url="YOUR_WEBHOOK_URL")

def notify_text(title, text, color):
    """
    Slackにテキストメッセージを通知する関数

    :param title: メッセージのタイトル
    :param text: メッセージの本文
    :param color: メッセージの左側に表示される線の色 ('good', 'warning', 'danger' または16進数カラーコード)
    """
    attachments = [{
        "title": title,
        "text": text,
        "color": color,
        "footer": "Sent from Python Script",
    }]
    slack.notify(attachments=attachments)

# --- 使用例 ---
notify_text("実験完了", "モデルAの学習が完了しました。", "good")
notify_text("警告", "ディスク容量が残りわずかです。", "warning")
notify_text("エラー発生", "学習プロセスで例外が発生しました。", "danger")
  • : attachments を使ったメッセージの書式設定は、Slack公式ドキュメントで「レガシー(legacy)」と位置づけられています。廃止が正式に宣言されているわけではありませんが、「将来的に表示や機能が縮小される方向で変更される可能性がある」と明記されており、新規実装では Block Kit の利用が推奨されます(4節で詳述)。

実行結果

Slackへのテキスト通知の実行結果

Block Kitによる現行推奨の書き方

上記の attachments はfig7の実行結果と対応させるためにあえてレガシー形式のまま残していますが、新規に実装する場合は slackweb を使わず requests で直接Block Kit形式のJSONを送るほうが、将来的な仕様変更の影響を受けにくくなります。

import requests

def notify_block_kit(webhook_url, title, text, emoji=":white_check_mark:"):
    """
    Block Kit形式でSlackにテキストメッセージを通知する関数(現行の推奨方式)

    :param webhook_url: Incoming WebhookのURL
    :param title: 見出しとして表示するタイトル
    :param text: 本文(mrkdwn形式。*太字* や `コード` が使える)
    :param emoji: タイトル先頭に付けるSlack絵文字コード
    """
    payload = {
        "blocks": [
            {
                "type": "header",
                "text": {"type": "plain_text", "text": f"{title}", "emoji": True},
            },
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": f"{emoji} {text}"},
            },
        ]
    }
    response = requests.post(webhook_url, json=payload, timeout=5)
    response.raise_for_status()  # 429など非200はここで例外化される
    return response

# --- 使用例 ---
WEBHOOK_URL = "YOUR_WEBHOOK_URL"
notify_block_kit(WEBHOOK_URL, "実験完了", "モデルAの学習が完了しました。")

2. 画像を送信する (files.upload API → files_upload_v2)

実験結果のグラフなどを画像ファイルとして送信したい場合は、Incoming Webhookとは別の認証情報(APIトークン)を使うSlack APIのファイルアップロード機能を利用します。

重要な仕様変更(2024〜2025年): 本記事で当初紹介していた files.upload メソッドは、Slack公式の変更履歴によると2024年5月16日以降に新規作成されたSlackアプリでは利用できなくなり、既存アプリも含めて2025年11月12日にAPI自体が完全に廃止(retire)されました。つまり、以前の requests.post(url="https://slack.com/api/files.upload", ...) という実装は、本記事執筆時点(2026年)では動作しません。後継として files.getUploadURLExternalfiles.completeUploadExternal の2段階呼び出しが公式に案内されており、Python版SDK(slack_sdk)はこれをラップした files_upload_v2 という便利メソッドを提供しています。以下のコードはこの現行方式に更新したものです。

Slack側の設定

  1. Slack APIサイト にアクセスし、「Create New App」から新しいアプリを作成します。
  2. アプリの管理画面で「OAuth & Permissions」に移動します。
  3. 「Scopes」の「Bot Token Scopes」セクションで、「files:write」スコープを追加します。これにより、アプリがファイルをアップロードする権限を持ちます。
  4. ページ上部の「Install to Workspace」をクリックして、アプリをワークスペースにインストールし、連携を許可します。
  5. インストール後、「Bot User OAuth Token」が表示されます。このトークン(通常 xoxb- で始まります)をコピーします。このトークンも外部に漏れないように厳重に管理してください。
  6. 投稿先チャンネルへ通知を送るには、事前にチャンネルのチャンネルID#general のような名前ではなく C0123456789 の形式)を控えておきます。files_upload_v2 はチャンネル名ではなくIDのみを受け付けます。

Pythonコード

公式Python SDK(slack_sdk)をインストールして使用します。

pip install slack_sdk
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

# Bot User OAuth Tokenは環境変数から読み込む(コードに直書きしない。3.3節を参照)
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])

def notify_image(channel_id, title, image_path):
    """
    Slackに画像をアップロードして通知する関数(files_upload_v2版)

    :param channel_id: 投稿したいチャンネルID (例: 'C0123456789'。#name形式は不可)
    :param title: 画像のタイトル
    :param image_path: アップロードする画像のファイルパス
    """
    try:
        response = client.files_upload_v2(
            channel=channel_id,
            file=image_path,
            title=title,
        )
        return response["file"]
    except SlackApiError as e:
        raise RuntimeError(f"アップロード失敗: {e.response['error']}") from e

# --- 使用例 ---
CHANNEL_ID = "C0123456789"  # 通知したいチャンネルのID
IMAGE_FILE_PATH = "test.png"  # 送信する画像のパス

notify_image(CHANNEL_ID, "学習結果のグラフ", IMAGE_FILE_PATH)
  • : files_upload_v2 は内部で files.getUploadURLExternal(アップロード先URLの発行)→ ファイル本体のPUT送信 → files.completeUploadExternal(アップロード完了通知)の3ステップを自動的に行います。旧 files.upload の1リクエスト方式に比べるとネットワーク往復は増えますが、大きなファイルでも安定してアップロードできるように設計されています。

実行結果

Slackへの画像アップロードの実行結果1 Slackへの画像アップロードの実行結果2

(上記スクリーンショットは旧 files.upload 方式で撮影したものですが、Slackチャンネル上での見え方は files_upload_v2 に切り替えても変わりません。)

3. 運用上の注意点(エッジケース)

実験完了通知程度の頻度であれば意識する必要はほとんどありませんが、CI/CDや監視システムなど通知の頻度・量が増える用途では、以下の制約を踏まえて実装する必要があります。本節の数値はSlack公式ドキュメント(2026年7月時点)に基づく記述であり、本記事の執筆環境には検証用のSlackワークスペース・Webhook URLが用意されていないため、実際にレート制限や巨大ペイロードを送りつけてSlack側の挙動を実測したものではありません。 実装前に必ず公式ドキュメントで最新の数値を確認してください。

3.1 レート制限

Slack公式のレート制限ドキュメントによると、Incoming Webhookは1チャンネルあたり**「1秒に1リクエスト」**に制限されており、短時間のバースト(瞬間的な複数リクエスト)はある程度許容されるとされています。この制限を超えると、Slackは次のようなレスポンスを返します。

HTTP/1.1 429 Too Many Requests
Retry-After: 30

Retry-After ヘッダーには、次のリクエストまで待つべき秒数が入っています。大量の通知を送るコードでは、このヘッダーの値を尊重したリトライを実装するのが標準的な対処法です。

import time
import requests

def notify_with_retry(webhook_url, payload, max_retries=3):
    """
    Incoming Webhookへの通知をリトライ付きで送信する。
    HTTP 429を受け取った場合はRetry-Afterヘッダーの秒数だけ待機し、
    さらに念のため試行回数に応じた指数バックオフを加算してから再送する。

    :param webhook_url: Incoming WebhookのURL
    :param payload: 送信するJSONペイロード(Block Kit/attachments)
    :param max_retries: 最大リトライ回数
    """
    for attempt in range(max_retries):
        response = requests.post(webhook_url, json=payload, timeout=5)
        if response.status_code == 200:
            return response
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", "1"))
            wait_sec = retry_after * (2**attempt)
            time.sleep(wait_sec)
            continue
        response.raise_for_status()  # 429以外の異常系はここで例外化
    raise RuntimeError(f"{max_retries}回リトライしてもSlackへの通知に失敗しました。")

3.2 ペイロードサイズの制限

  • メッセージの text フィールドは最大40,000文字を超えるとSlack側で切り詰められます。この上限は2018年に段階的に引き下げられた末(当初500,000文字→200,000文字→100,000文字→最終的に40,000文字)確定した現行値です。実用上は、表示崩れを避けるためもっと短い数千文字程度に収めるのが安全です。
  • レガシーな attachments を使う場合、1メッセージあたり最大20個まで、footer フィールドは最大300文字、添付画像は幅360pxまたは高さ500pxを超えると自動的に縮小表示されるという制約があります。
  • メッセージ本文がおおむね700文字または5行以上の改行を超えると、Slack UI側で「もっと見る(Show more)」に折りたたまれます。

これらの制限を超えた場合、多くはHTTPエラーにはならず、「切り詰め」や「折りたたみ」という形でSlack側が黙って表示を調整します。前述の通り、本記事では実際に上限超過のペイロードを送信した際のレスポンスは検証していません。ドキュメント上の仕様として扱ってください。

3.3 Webhook URLの漏洩というセキュリティリスク

Incoming WebhookのURLはそれ自体が認証情報です。このURLをフロントエンドのJavaScriptに埋め込んだり、公開GitHubリポジトリにハードコードしたままコミットしたりすると、第三者がそのURLを使って任意の内容をチャンネルに投稿できてしまいます(チャンネルの内容を読み取ることはできませんが、なりすまし投稿やフィッシングへの悪用は可能です)。

これは理論上のリスクにとどまりません。GitHubは2024年4月、Secret ScanningにおけるSlack Webhook検出機能に「有効性チェック(漏洩したURLが実際にまだ有効かどうかの検証)」を追加しました。公開リポジトリへのWebhook URLの誤コミットが実際に頻発する問題として認識され、専用の検出・検証機能が用意されているということです。

標準的な対策は次の通りです。

  1. 環境変数やシークレットマネージャーで管理し、コードやリポジトリに直接書かない(本記事のコード例中の "YOUR_WEBHOOK_URL" は、実運用では必ず os.environ["SLACK_WEBHOOK_URL"] のように環境変数から読み込む形に置き換えてください)
  2. クライアントサイド(ブラウザ)に埋め込まない。ブラウザから直接叩けるコードにURLを書けば、それは公開しているのと同じです
  3. サーバーサイドのプロキシ経由で呼び出す。フロントエンドは自社バックエンドのAPIを叩き、バックエンドがWebhook URLを保持してSlackへ転送する構成にする
  4. 漏洩したら直ちにWebhookを無効化・再発行する。Slackの「Incoming Webhooks」設定画面でURLを再生成すれば、古いURLは即座に無効になる

4. 最近の動向: Block Kit とレガシー Attachments

本記事のセクション1で使っている attachments によるメッセージ整形は、Slack公式ドキュメントの「Legacy secondary message attachments」ページで明確に「レガシー」と位置づけられています。廃止(deprecation)が正式に宣言されているわけではありませんが、ドキュメントには「将来的に表示や機能が縮小される方向で変更される可能性がある」と明記されており、新規実装では Block Kit のレイアウトブロック(blocks)を使うことが強く推奨されています。attachmentsblocks は同一メッセージ内で併用することも可能で、その場合 attachments の中の blocks は「二次的な補足情報」として扱われます。

本記事のコード例は、実際のスクリーンショット(fig7)と対応させるために意図的にレガシー形式のまま残していますが、新規実装では2節で紹介したBlock Kit版の notify_block_kit を使うことを推奨します。

関連記事

参考

公式ドキュメント

その他