2026年8月29日
2026年8月29日
Pythonスクリプトを定期実行する方法|cron・systemdタイマー・scheduleライブラリの使い分け
Python のスクリプトを毎日・毎時のように定期実行したいなら、まずは OS の仕組み(Linux なら cron か systemd のタイマー、Windows ならタスク スケジューラ)に任せるのが基本です。Python の中で待ち続ける schedule や APScheduler は、常駐するアプリの中で細かい間隔の処理を回したいときに選びます。この記事では、それぞれの書き方と、定期実行で起きやすい「手動では動くのに cron だと動かない」「前回の処理が終わる前に次が始まる」といった問題の防ぎ方を説明します。
どの方法を選ぶか
最初に使い分けを整理しておきます。
- cron:Linux サーバーで最も手軽。1行書けば動き、ほとんどの環境に最初から入っている。最小単位は1分
- systemd タイマー:systemd が動いている Linux で使える。ログが journalctl にまとまり、電源オフ中に逃した実行を起動後に補う設定もできる
- schedule ライブラリ:Python の中でループして時刻を待つ。依存が少なく読みやすいが、プロセスが落ちると止まるため、常駐の仕組みが別に要る
- APScheduler:アプリ内のスケジューラ。cron 形式の指定やジョブの永続化などを持つ。Web アプリなど常駐プロセスの中で使うことが多い
「1日1回のバッチ」「1時間ごとの集計」のような用途なら、cron か systemd タイマーで十分です。OS 側に任せると、スクリプトは「1回実行して終わる」単純な作りにでき、メモリリークや例外で常駐プロセスが止まる心配もありません。
前提:1回実行して終わるスクリプトにする
定期実行するスクリプトは、手動で python job.py と打って正しく終わる状態にしてから登録します。
#!/usr/bin/env python3
"""定期実行するジョブの例。成功で終了コード0、失敗で1を返す"""
import logging
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent
logging.basicConfig(
filename=BASE / "job.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def main() -> int:
logging.info("start")
try:
# ここに本来の処理を書く
data_file = BASE / "data" / "input.csv"
logging.info("processing %s", data_file)
except Exception:
logging.exception("job failed")
return 1
logging.info("done")
return 0
if __name__ == "__main__":
sys.exit(main())
ポイントは2つです。ファイルのパスは Path(__file__).resolve().parent を基準にして組み立てること、失敗時に終了コードを0以外で返すことです。cron はカレントディレクトリをホームディレクトリにして実行するため、open("data/input.csv") のような相対パスは手動実行では動いても cron では失敗します。終了コードを正しく返しておくと、systemd や監視ツールが失敗を検知できます。
cron で定期実行する
crontab -e で自分のユーザーの設定を開き、1行追加します。
# 分 時 日 月 曜日 コマンド
# 毎日 3:15 に実行
15 3 * * * /home/user/app/.venv/bin/python /home/user/app/job.py >> /home/user/app/cron.log 2>&1
# 10分ごとに実行
*/10 * * * * /home/user/app/.venv/bin/python /home/user/app/job.py >> /home/user/app/cron.log 2>&1
cron で動かないときの原因は、ほとんどが次のどれかです。
- Python のパスが違う:cron の PATH は最小限なので、
pythonと書くと仮想環境ではない別の Python が使われることがあります。仮想環境の Python をフルパスで書きます。仮想環境を activate する必要はありません。 - 環境変数が無い:対話シェルの
.bashrcで設定した環境変数は cron では読み込まれません。API のエンドポイントや設定ファイルの場所は、スクリプト内で読み込む設定ファイルに書くか、crontab の先頭でKEY=valueの形で定義します。秘密情報は crontab に直接書かず、権限を絞ったファイルから読み込むほうが安全です。 - 出力の行き先が無い:
>> ログファイル 2>&1を付けないと、エラー出力はメールで送られる設定になっていて、実際には誰も読めないことがよくあります。 - **
%をエスケープしていない**:crontab では%が改行扱いになります。date +%Y%m%dのような指定は\%と書く必要があります。
登録できたかは crontab -l で確認し、実行されたかはログファイルと、ディストリビューションによっては journalctl -u cron(または cron の代わりに crond)で確かめます。
前回の実行が終わる前に次が始まるのを防ぐ
10分ごとのジョブが、たまたま15分かかると、2つ目が並行して動き、同じデータを二重に処理することがあります。Linux なら flock で排他制御をかけるのが簡単です。
*/10 * * * * /usr/bin/flock -n /tmp/myjob.lock /home/user/app/.venv/bin/python /home/user/app/job.py >> /home/user/app/cron.log 2>&1
-n を付けると、ロックが取れないときは待たずにすぐ終了します。ロックはプロセスの終了とともに解放されるため、異常終了してもロックが残り続けることはありません。
タイムゾーンに注意する
cron はサーバーのタイムゾーンで動きます。クラウドのサーバーは UTC になっていることが多く、「3時に実行」が日本時間の12時になるという事故がよく起きます。timedatectl でサーバーの設定を確認してください。cron の実装によっては crontab 内で CRON_TZ を指定できますが、対応状況は実装ごとに異なるため、使う前に man 5 crontab で対応しているかを確認してください。
systemd タイマーで定期実行する
systemd が動いている環境なら、サービスとタイマーの2つのファイルを作ります。ここではユーザー単位ではなくシステム全体に置く例を示します。
# /etc/systemd/system/myjob.service
[Unit]
Description=My Python job
[Service]
Type=oneshot
User=appuser
WorkingDirectory=/home/appuser/app
ExecStart=/home/appuser/app/.venv/bin/python /home/appuser/app/job.py
# /etc/systemd/system/myjob.timer
[Unit]
Description=Run my Python job daily
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now myjob.timer
systemctl list-timers myjob.timer # 次回の実行予定を確認
sudo systemctl start myjob.service # 手動で1回動かして確認
journalctl -u myjob.service -n 50 # 実行ログを確認
Type=oneshot のサービスは、前回の実行が終わっていなければ同じサービスを重ねて起動しないため、flock を書かなくても二重起動を避けられます。Persistent=true は、サーバーが止まっていて逃した実行を、起動後に1回補う設定です。標準出力とエラー出力は journal に残るので、ログファイルのリダイレクトも不要になります。OnCalendar の書き方は systemd-analyze calendar "*-*-* 03:15:00" で次回の時刻を表示して確かめられます。
Python の中でスケジュールする:schedule と APScheduler
アプリの中で「30秒ごとにキューを見る」といった処理を回したい場合は、ライブラリを使います。schedule は書き方が素直です。
import time
import schedule
def job():
print("run")
schedule.every(10).minutes.do(job)
schedule.every().day.at("03:15").do(job)
while True:
schedule.run_pending()
time.sleep(1)
このループは、ジョブが終わるまで次のジョブを始めません。時間のかかる処理を入れると後続が遅れる点と、プロセスが例外やサーバー再起動で止まると何も実行されなくなる点に注意してください。常駐させるなら systemd のサービス(Restart=on-failure 付き)として動かし、止まっても再起動される形にします。
APScheduler は、cron 形式の指定、複数ジョブの並行実行、ジョブ情報の DB への保存などを持つ、より本格的なスケジューラです。ただし、長く使われてきた 3.x 系と、開発中の 4.x 系で API が大きく異なります(2026年10月現在)。ネット上のサンプルがどちらの系統向けかを確認し、使うバージョンの公式ドキュメントに沿って書いてください。
Web アプリを複数のワーカープロセスで動かしている場合、各プロセスの中でスケジューラを起動すると、同じジョブがワーカーの数だけ実行されます。この構成では、スケジューラを1つのプロセスに分けるか、cron や systemd タイマーに戻すほうが安全です。処理を複数台に分散したいなら、Celery などのタスクキューを検討します。
Windows で定期実行する
Windows ではタスク スケジューラを使います。GUI で「基本タスクの作成」から登録できるほか、コマンドでも登録できます。
schtasks /Create /TN "MyPythonJob" /TR "C:\app\.venv\Scripts\python.exe C:\app\job.py" /SC DAILY /ST 03:15
ここでも、仮想環境の python.exe をフルパスで指定し、スクリプト内のパスは __file__ 基準にしておきます。ログオンしていないときにも動かすかどうか、どのユーザーで動かすかの設定は、タスクのプロパティで確認してください。
失敗に気づける仕組みを作る
定期実行で一番困るのは、止まっていることに気づかないことです。最低限、次の3点を用意しておくと安心です。
- 失敗時に終了コード1を返し、ログに例外のスタックトレースを残す
- 成功したら最後に「成功時刻」をファイルや監視サービスに記録し、一定時間更新がなければ通知する
- ログファイルは logrotate などで肥大化を防ぐ
まとめ
- 1日1回・1時間ごとのようなバッチは、cron か systemd タイマーで動かすのが基本
- 仮想環境の Python をフルパスで指定し、スクリプト内のパスは
__file__を基準にする - cron では出力をログへリダイレクトし、
flock -nで二重起動を防ぐ - systemd タイマーは
Type=oneshotとPersistent=trueで、重複防止と逃した実行の補完ができる scheduleや APScheduler は常駐アプリ向け。複数ワーカー構成では重複実行に注意する- サーバーのタイムゾーンと、失敗時の通知の仕組みを最初に確認しておく