はじめに
職場の人に頼まれて業務自動化プログラムをPythonで作成した際,コストをかけずに定期実行するため,「常時起動しているPCにデプロイし,cronジョブとして登録する」方法を選択しました。
しかし,定期実行のタイミングが「毎月20日(ただし20日が土日祝日の場合は20日以前の最終営業日)の午前10時30分」という複雑なものだったため,cronジョブの登録の仕方でつまづきました。
いろいろ調べた結果,おそらく一番スマートだと思われる方法を見つけたので共有しようと思います。
管見の限りでは,「土日祝日だけ実行」や「平日だけ実行」といったソリューションはあるものの,今回のパターンにベストマッチするソリューションはなかったように思いますが,万が一既出だったらすみません。
実行環境
以下の環境では動作が確認できています。
- WSL2 (Windows10) & Python3.6
- Mac OS 12 & Python3.10
前提
今回は例として,yagao
ユーザーでホームディレクトリ直下のsample.sh
を上に書いたタイミングで実行するcronジョブを登録しようと思います。sample.sh
は以下のように同階層に配置されたファイルhoge
のタイムスタンプを更新するだけのシンプルなスクリプトです。
touch ./hoge
ディレクトリ構成は以下のようになっています。
yagao/
├── hoge
└── sample.sh
事前準備
今回はcronジョブを動的に登録するためのPythonスクリプトを作成するため,Python3を実行できる環境が必要になります。
Python3とpip3がインストールできたら以下のコマンドで必要なライブラリをインストールします。
pip3 install -y python-crontab jpholiday python-dateutil
また,Macの場合はcronに対してフルディスクアクセス権限を付与する必要があります。やり方は以下をご参照ください。
crontabコマンドでシェルスクリプトが実行できないときの対処法。mac osのシステム環境設定を変更する方法。
解法
今回は,当月のスクリプト実行日を判定してcronジョブを登録するPythonスクリプト(set_cron_job.py
)を作成し,それを毎月1日に実行されるようcronに登録するという方式を採ります(スクリプトの格納場所はどこでも構いませんが,今回はsample.sh
と同じディレクトリ配下に置くことにします)。
yagao~$ crontab -l
* * 1 * * cd /home/yagao; /usr/bin/python3 ./set_cron_job.py
この方法だとcrobジョブの実行が月に2回だけで済むので,今日が実行日かどうか毎日判定する方式よりイケてるんじゃないかと思います。
Pythonスクリプトのサンプルは以下のようになります。
from datetime import datetime, date
from crontab import CronTab
import jpholiday
from dateutil.relativedelta import relativedelta
def isBizDay(d):
# d: "yyyymmdd"
the_date = date(int(d[0:4]), int(d[4:6]), int(d[6:8]))
if the_date.weekday() >= 5 or jpholiday.is_holiday(the_date):
return False
else:
return True
# crontabを操作するOSユーザーを指定
cron = CronTab(user='yagao')
# 前回登録したジョブを削除
cron.remove_all(command="sample.sh")
# 当月20日のDatetimeオブジェクト
twentieth = datetime.now().replace(day=20)
# ジョブ登録用に時刻を調整
job_day = twentieth.replace(minute=30, hour=10)
# 20日が非営業日だった場合,1日ずつ遡った最初の営業日をジョブ実行日とする
if not isBizDay(twentieth.strftime("%Y%m%d")):
while True:
job_day = job_day - relativedelta(days=1)
if isBizDay(job_day.strftime("%Y%m%d")):
break
# ジョブの登録
job = cron.new(command="cd /home/yagao; sample.sh")
# 実行タイミングの設定
job.setall(job_day)
# ジョブを保存
cron.write()
そんなに難しいことはしていないので,コメントを読めばだいたい理解できると思います。
なお関数isBizDay()
は,以下の記事で紹介されていたものをお借りして少しだけ修正したものになります。
datetimeとjpholidayを組み合わせて、平日か土日祝日かを判定するスクリプト - Qiita
このスクリプトを実行すると以下のようになります。
yagao~$ python3 set_cron_job.py
yagao~$ crontab -l
* * 1 * * cd /home/yagao; /usr/bin/python3 ./set_cron_job.py
30 10 20 9 * cd /home/yagao; sample.sh
2022年9月20日は平日なので,これで問題ないですね。1行空行が挿入されてしまうのは仕様のようです1。
スクリプトの最初の方で前回登録したsample.sh
のジョブを削除しているので,再度set_cron_job.py
を実行しても結果は同じになります。
次に,twentieth = datetime.now().replace(month=11, day=20)
として再度実行してみます。
yagao~$ python3 set_cron_job.py
yagao~$ crontab -l
* * 1 * * cd /home/yagao; /usr/bin/python3 ./set_cron_job.py
30 10 18 11 * cd /home/yagao; sample.sh
2022年11月20日は日曜日なので,それ以前の最終営業日である11月18日(金)を実行日として登録されました。
更に,twentieth = datetime.now().replace(year=2021, day=20)
として再度実行してみます。
yagao~$ python3 set_cron_job.py
yagao~$ crontab -l
* * 1 * * cd /home/yagao; /usr/bin/python3 ./set_cron_job.py
30 10 17 9 * cd /home/yagao; sample.sh
2021年9月20日(月)は敬老の日だったため,それ以前の最終営業日である9月17日(金)を実行日として登録されました。
めでたしめでたし。
補足
創立記念日などを考慮する必要がある場合は,jpholiday
の機能を使って独自の休日を追加することができます。
Lalcs/jpholiday: 日本の祝日を取得するライブラリ#独自の休日を追加
おわりに
crontabをPythonで操作するライブラリがあって助かりました。定期実行の幅が広がりそうです。
isBizDay()
はjpholiday
に実装されてほしいですね・・・。
今回は前倒しのパターンでしたが,while
ループの中の-
を+
に変えれば後ろ倒しのパターンも簡単に作れます。
よかったら取り入れてみてください。
参考
Martin Owens / python-crontab · GitLab
Pythonで月初・月末(初日・最終日)、最終X曜日の日付を取得 | note.nkmk.me