Python自動化 #6 スケジューリング — 寝ている間に動かす

読了 7分

#5 まで追いかけてきたなら、データを集め、Excel に整理し、結果をメールやメッセンジャーで送ってくれるスクリプトが手元にあります。ただ、まだ 1 つ残っています。そのスクリプトの実行ボタンを毎日押している人が、相変わらず自分だという点です。実行まで自動化してこそ本当の自動化です。今回は、スクリプトを決まった時刻に勝手に動くようにする方法を整理します。

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

方法は大きく 2 つに分かれます。cron やタスクスケジューラのように OS が時間を計ってスクリプトを起こしてくれる方式 と、APScheduler のように Python プロセスが常駐して自分で時間を計る方式 です。まず、単発の自動化の大半に十分な OS スケジューラから見ていきます。

cron — macOSとLinuxの基本 #

cron は Unix 系 OS に数十年入り続けているスケジューラです。ターミナルで crontab -e を実行してエディタを開き、1 行に 1 つずつ「時刻フィールド 5 個 + 実行するコマンド」を登録します。

フィールド意味許容値
10〜59
20〜23
31〜31
41〜12
5曜日0〜7 (0 と 7 が日曜)

* は「毎回」、*/5 は「5 ごと」、1-5 は範囲です。平日朝 7 時に日次レポートを回す行はこうなります。

crontabの例
0 7 * * 1-5 /home/me/scripts/.venv/bin/python /home/me/scripts/daily_report.py >> /home/me/scripts/cron.log 2>&1

後ろに付けた >> は標準出力をファイルに追記し、2>&1 はエラー出力まで同じファイルへ送る処理です。最初に登録するときこうして出力を受けておくと、デバッグがずっと楽になります。そしてここで最もよくぶつかる落とし穴が 環境変数 です。cron は自分のターミナルの .zshrc.bashrc を読まず、PATH がほぼ空の状態でコマンドを実行します。ターミナルでは動いていた python daily_report.py が cron では “command not found” で死ぬ理由です。対応の原則は 1 つ、すべて絶対パスで書くこと です。

  • Python 実行ファイルとスクリプト: python daily_report.py ではなく、仮想環境内の /home/me/scripts/.venv/bin/python /home/me/scripts/daily_report.py
  • スクリプトが読み書きするファイル: cron の作業ディレクトリは自分のプロジェクトフォルダではないので、相対パスの代わりに pathlib.Path(__file__).parent 基準で作ります

macOSのlaunchd、Windowsのタスクスケジューラ #

macOS でも crontab は動きますが、Apple が公式に推す方式は launchd です。実用上の違いは スリープの扱い です。cron は指定時刻に Mac がスリープしているとその回をそのまま飛ばしますが、launchd の StartCalendarInterval は復帰したときに逃した作業を 1 回実行してくれます。設定が plist ファイルの作成で cron より長くなるので、ノートパソコンで回していてこの違いが必要になったら調べる、くらいに覚えておけば十分です。

Windows には cron の代わりに タスクスケジューラ(Task Scheduler)があります。GUI で「基本タスクの作成」を押し、トリガー(毎日午前 7 時)、操作(プログラム欄に C:\Users\me\scripts\.venv\Scripts\python.exe、引数欄にスクリプトのパス)、開始フォルダ(スクリプトのフォルダ)を指定すれば終わりです。絶対パスの原則は Windows でも同じように適用されます。

Pythonの中で解決する — APScheduler #

OS スケジューラが「OS がスクリプトを起こす」構造だとすれば、pip install apscheduler でインストールする APScheduler は Python プロセスが自分で時間を計る 構造です。

scheduler.py
from apscheduler.schedulers.blocking import BlockingScheduler

scheduler = BlockingScheduler()

@scheduler.scheduled_job("cron", day_of_week="mon-fri", hour=7, minute=0)
def daily_report():
    ...  # 第5回までに作った作業関数をここで呼び出します

scheduler.start()

実行するとプロセスが終了せずに常駐し、登録した時刻ごとに関数を呼び出します。cron 文法をそのまま使う cron トリガーのほかに「30 分ごと」式の interval トリガーもあり、同じ Python コードなので仮想環境やパスの問題がありません。その代わり決定的な違いがあります。このプロセスが死ぬとすべてのスケジュールが止まるという点 です。ターミナルを閉じたり再起動したりすれば終わりです。cron は OS が生きている限り動きますが、APScheduler はプロセスの生存について自分で責任を持つ必要があります。

どちらを使うか #

状況おすすめ
1 日 1〜2 回回る単発スクリプトcron / タスクスケジューラ
ノートパソコンでスリープが多いlaunchd (macOS)
常駐アプリ(ボット、Web サーバー)内の定期作業APScheduler
自分のコンピュータを点けておけないGitHub Actions (下記)

動いたか確かめる — printの代わりにlogging #

スケジューラに載せた瞬間、スクリプトは自分の目の前では動かなくなります。print はターミナルがなければ行き場がないので、実行記録をファイルに残す logging に切り替えます。

loggingの設定
import logging
from pathlib import Path

logging.basicConfig(
    filename=Path(__file__).parent / "automation.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s")

ログファイルのパスも Path(__file__).parent 基準の絶対パスで作っている点に注意してください。作業のあちこちに logging.info("32件処理") のように残しておけば、朝 automation.log を開いて、昨夜動いたか、何件処理したかをすぐ確認できます。

失敗への備え — 死んだら通知で受け取る #

自動化が怖い瞬間は失敗したときではなく、失敗に気づかないまま過ぎていくとき です。サイトの構造が変わってスクレイピングが死んだのに、1 ヶ月後に気づく状況が典型例です。#5 で作った通知関数を再利用し、メインロジックを包むラッパーを置きます。

失敗通知ラッパー
import logging
import traceback
from notify import notify_slack  # 第5回で作った通知関数

if __name__ == "__main__":
    try:
        main()
        logging.info("ジョブ成功")
    except Exception:
        logging.exception("ジョブ失敗")
        notify_slack(f"[自動化失敗] daily_report\n{traceback.format_exc()[-500:]}")
        raise

成功すればログ 1 行、失敗すればログに全トレースバックを残し、メッセンジャーへ末尾 500 文字を送ります。最後の raise は例外を投げ直して終了コードを 0 以外にし、スケジューラ側にも失敗として記録されるようにします。

サーバーなしで無料に — GitHub Actions #

ここまでの方法はすべて、自分のコンピュータが点いている必要があります。コンピュータを切って眠りたいなら、GitHub Actions の schedule トリガーが最も簡単な無料の選択肢です。スクリプトをリポジトリに上げ、.github/workflows/daily.yml ファイルを 1 つ追加します。

.github/workflows/daily.yml
name: daily-report
on:
  schedule:
    - cron: "0 22 * * *"   # UTC 基準なので JST 午前 7 時
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt
      - run: python daily_report.py

cron 文法をそのまま使いますが、3 つ覚えておく必要があります。時刻が UTC 基準 なので JST から 9 時間引く必要があり、正確な時刻の実行は保証されず数分から数十分遅れて回ることがあり、リポジトリに 60 日間活動がないとスケジュールが自動で無効化されます。トークンや Webhook URL のようなシークレットはコードに書かず、リポジトリ Settings の Secrets に入れて環境変数として受け取ります。

まとめ #

今回作った流れを整理します。

  • 単発スクリプトは OS スケジューラ(cron / タスクスケジューラ)で、落とし穴の環境変数とパスは絶対パスへの統一で解決
  • 常駐アプリ内の定期作業は APScheduler
  • print の代わりに logging で実行記録をファイルに残す
  • try/except ラッパーで失敗を第 5 回の通知につなぎ、「気づかないまま過ぎる失敗」を遮断
  • 自分のコンピュータを切りたいなら GitHub Actions schedule

次回(#7 CLIツール化)がこのシリーズの最終回です。ここまで作ってきたスクリプトたちに argparse の先のオプション体系を付け、1 つのコマンドラインツールにまとめて、どこからでも一言で呼べるように仕上げます。

X