2019年7月14日日曜日

CRONでPythonを定期実行

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実行となります。

0 件のコメント:

コメントを投稿