LinuxのcronコマンドでPythonを定期実行させます。自己流なのであしからずご了承ください。
よく使っているやり方
8:45に定期実行する例
45 8 * * * (cd /path/to/file/ && python file.py >> cronlog.log 2>&1)
- python file.pyを実行しますが、先にcdでfile.pyのあるディレクトリに移動します。コマンドは&&で繋ぎます。これで.py内でのカレントディレクトリを気にしなくてよくなります。
- >> cronlog.log 2>&1で同じディレクトリにあるcron.log.logにstd.out, std.errともログを書き足していきます。(微妙なので下記のPythonのlogging参照)
- cronではローカルと環境変数が異なるので、ローカルの環境変数を使いたいときは. * * * * * (~/.profile && command) というようにprofile読み込みコマンドを前に入れます。
ログはPython側で
ログはPython側で設定しています。std.errだとタイムスタンプが入らないなど使いにくかったので(入れる方法は後述)。
testlog.pyというファイルに下記を保存して実行すると、testlog.py.logというログファイルが生成されます。exceptは端折ってますが本当は指定しないといけません。Pythonのロギングについては、こちらで詳しく書いています(
python loggingメモ - shimo lab2 )
import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("log/" + __file__ + ".log")
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s, %(levelname)s, %(message)s", "%Y-%m-%d %H:%M:%S"
)
fh.setFormatter(formatter)
logger.addHandler(fh)
def main():
1 / 0
if __name__ == "__main__":
try:
main()
except:
logger.exception("-" * 10)
ログファイルtestlog.py.logの中身
2021-02-19 18:09:01, ERROR, ----------
Traceback (most recent call last):
File "testlog.py", line 29, in
main()
File "testlog.py", line 23, in main
1 / 0
ZeroDivisionError: division by zero
cronについて
- 再起動しても自動でCRON処理を実行してくれる(スタートアップでcrond起動)
- 実行ファイルとcronは別なので独立しているので、タイマー設定後にファイルを更新してもよい
- 複数の処理を1つのファイル(crontab)で管理できる
- 枯れた技術で安心
crontabで実行するスクリプトの準備
まず定期実行するpyスクリプトを用意します。
crontab_test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import datetime
now = datetime.datetime.now()
text = now.strftime("%Y/%m/%d %H:%M:%S\n")
with open('file.txt', 'a') as f:
f.write(text)
今回例として使うのは、時刻をテキストファイルに書き込むスクリプトです。
crontabを実行する
$ crontab -e
実行の時間やコマンドを記述するための、エディットモードが現れます。
※初めての実行では、エディタを何にするか聞いてきます(select-editorコマンドと同じ)。4のvim.gtkを使いました。5のvim.tinyではプラグインのエラーが出ていました。
$ select-editor
Select an editor. To change later, run 'select-editor'.
1. /bin/ed
2. /bin/nano <---- easiest
3. /usr/bin/vim.basic
4. /usr/bin/vim.gtk
5. /usr/bin/vim.tiny
注意
$ crontab -r
crontabの内容が消えるので注意です(Remove)。自分はcrontab -eを辞書登録して呼び出していて、直接コマンドを打たないようにしています。
crontab -eの編集
定期実行する日時とコマンドを1行で書きます。毎時45分にpython crontab_test.pyを実行するCRONの書き方は・・・
45 * * * * python /path/to/crontab_test.py
ファイル名はフルパスで書いたほうがよいと言われています(/home/user/ から呼ばれています)
日時の書き方は順に
m h dom mon dow command
分時 日月 曜日
※dow曜日0, 7が日曜, 1が月曜,...)
※アスタリスク* は「全ての」という意味
CRONのログを確認する
動作したか確認したいとき、システムログ /var/log/syslogを確認します。
$ less /var/log/syslog | grep CRON
上記で毎時45分に設定したpythonの結果はこのように出ます。
Jul 14 17:45:01 XXXX CRON[23197]: (user) CMD (python /path/to/crontab_test.py)
std.out std.errorをテキスト出力
stdoutのprint文はコンソールに出力されません。また、Pythonスクリプトが失敗してもエラーは表示されません。標準出力、標準エラーを、ファイルに書き込みます。
30 8 * * * /../../..py >>log.log 2>>err.log
log.logの前の>> は1>> とも書けます。
これで8:30に実行するpyスクリプトの標準出力と標準エラーがファイルに書き込まれます。
同じファイルに出力する
上記で >>log.log 2>>err.log を >>log.log 2>&1
と書き換えると、stderrも同じlog.logに書き込まれます。
stdout にcron実行時間を書き込む
ロギングのところで実行時間が入らないと書きましたが、無理やり入れることは可能です。実行前にこれを入れます。
echo $(date +\%Y-\%m-\%d\ \%H:\%M:\%S) >> cronlog.log && <実行したいコマンド>
単に date表記を書き込んでるだけです。%とスペースはバックスラッシュでエスケープしています。
簡単なものならこれでもいいかな、と思っています。
長いので、変数をcrontabの中に書いて
ECHO_DATE=$(echo $(date +\%Y-\%m-\%d\ \%H:\%M:\%S) >> cronlog.log)
evalを使って呼び出したりしています。
eval $ECHO_DATE
実行ファイルの絶対パスを取得する
cronでは絶対パスでpyファイルを呼び出しますが、このpyファイルの存在するディレクトリ名を出したいとき、os.getcwd()だと失敗します※カレントディレクトリは/home/user/になる。
os.path.abspath(os.path.dirname(__file__))
で実行しているファイルの絶対パスが出せます。
CRONのバックアップ
こちらのブログが参考になります。crontab -lで設定の内容が表示できますが、それをバックアップファイルにリダイレクトして、それ自体をcronで定期的実行するといいみたいです。
CRONTABで環境変数設定
通常の環境では実行できるのに、CRONでは止まっているようです。エラーログを見てみると、
ImportError: No module named numpy
html = urllib.request.urlopen(url).read().decode('utf-8')
AttributeError: 'module' object has no attribute 'request'
numpyがない、rullibでrequestがないとのことです。
結論: CRONで環境変数を設定する必要がある。
crontabの設定ファイル内に
PATH=/home/...:/home/.../
などと、記入すると解決しました。
--
cronで使っているPythonが別の場所にある可能性があるとき。
which python を使ったりしてpythonのフルパスを調べたりもできます。
環境変数を入れずに、フルパスでpythonを呼び出してもOK。
CRON JOBで実行するたびに環境変数設定をprofileから読み込む
例えば環境変数が/.profileにあってそれをハードコードしたくない場合、都度読み込んでからファイルを実行すればよいです。
0 * * * * (. ~/.profile && cd /path/to/file/ && python file.py >> cronlog.log 2>> cronerr.log)
これで毎時間0分に、/.profileを読み込んでからディレクトリ移動してpython実行となります。