Python自動化 #5 結果を知らせる — メール・Slack・Discord通知

読了 6分

スクリプトがちゃんと動いたか確かめるために、毎朝ターミナルを開いてログを掘り返しているなら、その確認作業そのものがもう 1 つの手作業です。#2 で Excel を作り、#3#4 でデータを集めてきましたが、結果が人に届かなければ自動化は半分しか終わっていません。今回は、スクリプトが自分で結果を報告するようにします。Slack と Discord の Webhook、そして Excel ファイルを添付したメールまで扱います。

  • #1 スクリプト第一歩
  • #2 Excel 自動化
  • #3 Web スクレイピング — 静的ページ
  • #4 Web スクレイピング — 動的ページ
  • #5 結果を知らせる — メール・Slack・Discord 通知 ← 今回
  • #6 スケジューリング
  • #7 CLI ツール化

最も手軽な方法 — Webhook #

通知を送る方法の中で最も手軽なのは Webhook です。Slack や Discord が発行してくれる固有 URL に JSON を POST すると、その内容が指定したチャンネルにメッセージとして投稿されます。ボットアカウントも、OAuth も、SDK も必要ありません。Slack はアプリ設定で Incoming Webhooks を有効にしてチャンネルを指定すると https://hooks.slack.com/services/... 形式の URL を発行してくれて、Discord はチャンネル設定の連携メニューから Webhook を作れます。URL を入手したら、コードはこれだけです。

notify.py — Slack・Discord通知
import httpx

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T000/B000/XXXX"
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1234/abcd"


def notify_slack(text: str) -> None:
    httpx.post(SLACK_WEBHOOK_URL, json={"text": text}).raise_for_status()


def notify_discord(text: str) -> None:
    httpx.post(DISCORD_WEBHOOK_URL, json={"content": text}).raise_for_status()

これで notify_slack("価格収集 完了: 新規32件") の 1 行でチャンネルにメッセージが届きます。2 つのサービスの違いは JSON のキー 1 つだけです。Slack は text、Discord は content を使います。raise_for_status() を付けておくと、Webhook URL が失効していたり間違っていたりしたとき、黙って素通りせずすぐエラーになります。

メッセージを整える #

「完了」という一言よりも、開かなくても状況が分かる要約が良い通知です。成功なら核心の数字を、失敗ならエラー内容を載せて送ります。

要約とエラーを載せたメッセージ
import traceback

try:
    rows = collect_prices()  # 第3回で作った収集関数
    notify_slack(f"[OK] 価格収集 完了: {len(rows)}件")
except Exception:
    notify_slack(f"[FAIL] 価格収集 失敗\n{traceback.format_exc()[-1500:]}")
    raise

失敗通知の核心は traceback.format_exc() です。例外のスタックトレース全体を文字列で受け取ってメッセージにそのまま載せるので、通知を見るだけでどの行で何が壊れたか分かります。後ろから 1500 文字だけ切って送る理由は、Slack・Discord ともメッセージの長さに制限があり、トレースバックは末尾に原因が集まっているためです。最後の raise も重要です。通知を送ったからといって例外を握りつぶすと終了コードが 0 になり、次回扱うスケジューラが成功と勘違いします。

メール — smtplibとアプリパスワード #

Webhook は手軽ですが、ファイルを送るには不便です。#2 で作った Excel レポートを人に届ける仕事なら、メールが今でも最も確実です。標準ライブラリの smtplibemail だけで十分です。準備物が 1 つあります。Gmail は通常のパスワードでは SMTP ログインができないため、Google アカウントで 2 段階認証を有効にした上で、アカウント設定の「アプリパスワード」メニューから 16 桁のパスワードを発行する必要があります。この 16 桁がコードで使うパスワードです。

send_mail.py — Excel添付メール
import smtplib
import ssl
from email.message import EmailMessage
from pathlib import Path


def send_report(subject: str, body: str, attachment: Path) -> None:
    msg = EmailMessage()
    msg["From"] = "me@gmail.com"
    msg["To"] = "boss@example.com"
    msg["Subject"] = subject
    msg.set_content(body)
    msg.add_attachment(
        attachment.read_bytes(),
        maintype="application",
        subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        filename=attachment.name,
    )
    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
        server.login("me@gmail.com", "発行した16桁のアプリパスワード")
        server.send_message(msg)

これで send_report("週間価格レポート", "収集結果を添付します。", Path("価格まとめ.xlsx")) のように呼び出せます。ポイントを押さえておきます。SMTP_SSL と 465 ポートの組み合わせは最初から暗号化された接続を使い、ssl.create_default_context() は証明書検証まで含んだ基本のセキュリティ設定を作ってくれます。添付は add_attachment() 1 回で済み、subtype に書いた長い文字列は xlsx ファイルの MIME タイプです。PDF なら pdf、CSV なら csv に変えれば済みます。

シークレット管理 — トークンをコードに書かない #

上のコードには問題が 1 つあります。Webhook URL とアプリパスワードがコードにそのまま書かれています。Webhook URL はそれ自体がパスワードです。URL を知っている人は誰でもそのチャンネルにメッセージを送れますし、GitHub に上がった瞬間に流出です。シークレットは .env ファイルに切り出し、uv add python-dotenv でローダーをインストールします。

.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/XXXX
GMAIL_APP_PASSWORD=abcdabcdabcdabcd
環境変数として読む
import os
from dotenv import load_dotenv

load_dotenv()  # .env ファイルを環境変数としてロード
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
GMAIL_APP_PASSWORD = os.environ["GMAIL_APP_PASSWORD"]

os.environ["..."] は値がなければ即座に KeyError を出します。os.environ.get() で黙って None を受け取るより、設定漏れを起動時点ですぐ顕在化させる方が自動化スクリプトには安全です。そして .env は必ず .gitignore に追加します。このファイルがリポジトリに上がると、分離した意味がなくなります。

成功も知らせるか、失敗だけ知らせるか #

技術的にはどちらも送れます。しかし毎日届く「[OK] 完了」通知は、1 週間もすれば誰も読まなくなります。通知に慣れて感覚が鈍ると、肝心の失敗通知まで埋もれます。運用でよく使われる折衷案はこうです。

  • 失敗は即座に知らせます。通知の存在理由です。
  • 成功は数字がおかしいときだけ知らせます。普段 300 件収集されていたジョブが 5 件なら、エラーなしで終わっても実質失敗です。
  • 定期サマリーは週 1 回で十分です。「今週 7 回実行、7 回成功」の 1 行で生存確認になります。

総仕上げ — スクレイピング結果に通知を付ける #

第 3 回と第 4 回で作った収集スクリプトに、ここまでの内容を付けるとこんな形になります。

job.py — 収集 + 通知の総仕上げ
import sys
import traceback

from notify import notify_slack
from scraper import collect_prices  # 第3回の収集関数

try:
    rows = collect_prices()
    if len(rows) < 100:  # 普段の収集量の基準線
        notify_slack(f"[WARN] 収集件数が普段より少なめです: {len(rows)}件")
except Exception:
    notify_slack(f"[FAIL] 価格収集 失敗\n{traceback.format_exc()[-1500:]}")
    sys.exit(1)

成功すれば沈黙し、件数がおかしければ警告し、例外が出ればトレースバックとともに即座に知らせます。sys.exit(1) で失敗を終了コードにも残しておきました。この終了コードは次回のスケジューラが読むことになります。

まとめ #

今回作ったものを整理します。

  • Slack・Discord の Webhook — httpx.post 1 回で送信し、失敗時は traceback.format_exc() を載せてログ確認の手間を省略
  • smtplib + SMTP_SSL + アプリパスワードで Excel 添付メールを送信
  • Webhook URL とパスワードは .env に分離して .gitignore に登録
  • 失敗は即座に、成功は数字がおかしいときだけ知らせる運用基準

これでスクリプトは結果を作り、報告までこなします。残る問題は 1 つです。まだ人が手で実行しなければなりません。次回(#6 スケジューリング)では、cron とタスクスケジューラでスクリプトを決まった時刻に自動実行し、今回残した終了コードを活用する方法を扱います。

X