※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
「手動で実行したら動くのに、cronに登録すると動かない」——Linuxでの自動実行に挑戦した人が、ほぼ全員が一度は通る道です。しかもエラーメッセージすら出ないので、何が悪いのかまったく分かりません。
原因はほぼ決まっています。cronは、あなたが普段使っているシェルとはまったく別の環境で動くからです。この一点を理解すれば、トラブルの9割は自力で解決できるようになります。
この記事では、株価分析スクリプトを毎日確実に自動実行させる設定を、つまずきポイントを潰しながら解説します。設定の書き方だけでなく、動かないときの原因の切り分け方に重点を置きました。
cronの基本と書式
cronはLinuxに標準で入っている定期実行の仕組みです。設定は crontab -e で編集します。
# 編集する
crontab -e
# 現在の設定を表示する
crontab -l
# 設定をファイルに保存しておく(バックアップ)
crontab -l > ~/crontab.bak
書式は5つの数字+実行コマンドです。左から「分・時・日・月・曜日」を指定します。
┌───── 分 (0-59)
│ ┌─── 時 (0-23)
│ │ ┌─ 日 (1-31)
│ │ │ ┌─ 月 (1-12)
│ │ │ │ ┌─ 曜日 (0-7 0と7が日曜)
│ │ │ │ │
30 15 * * 1-5 実行したいコマンド
株価分析でよく使う指定をまとめておきます。
| 指定 | 意味 | 用途 |
|---|---|---|
30 15 * * 1-5 | 平日15:30 | 大引け後のデータ取得 |
50 8 * * 1-5 | 平日8:50 | 寄り付き前の準備 |
*/30 9-15 * * 1-5 | 平日9〜15時の30分ごと | ザラ場中の監視 |
0 6 * * 6 | 土曜6時 | 週次のレポート作成 |
0 0 1 * * | 毎月1日0時 | 月次の集計 |
曜日指定の 1-5(月〜金)を必ず入れてください。市場が閉まっている土日に実行しても意味がないうえ、データが取得できずエラーログだけが溜まります。
書式で迷ったときは、crontab.guru のようなオンラインツールが便利です。指定を入力すると「次にいつ実行されるか」を日本語ではなく英語で教えてくれるので、意図と合っているかを確認できます。
もう1つ、cronを触るときの心得を書いておきます。設定を編集する前に、必ず現在の内容をバックアップしてください。crontab -e の操作を誤ると、登録済みの設定がまるごと消えることがあります。
# 編集前に必ず退避する(習慣にする)
crontab -l > ~/crontab_$(date +%Y%m%d).bak
# 万一消してしまったら、バックアップから戻せる
# crontab ~/crontab_20260820.bak
📘 外部参考:Ubuntu Server Documentation / crontab.guru(書式の確認ツール)
なぜcronでPythonが動かないのか
ここが本記事の核心です。原因を理解するために、まず自分の環境とcronの環境の違いを目で見てみましょう。
# 手元のシェルでの環境変数
echo $PATH
# → /home/trader/.local/bin:/usr/local/bin:/usr/bin:/bin:...
# cronでの環境変数を確認する(一時的に登録して確認)
# * * * * * env > /tmp/cron_env.txt
# 1分待ってから
cat /tmp/cron_env.txt
実行すると衝撃を受けるはずです。cronのPATHは /usr/bin:/bin しかありません。普段使っている環境変数のほとんどが存在しないのです。
| 項目 | 普段のシェル | cron |
|---|---|---|
| PATH | 豊富 | /usr/bin:/bin のみ |
| 環境変数 | .bashrc が読まれる | 読まれない |
| カレントディレクトリ | いまいる場所 | ホームディレクトリ |
| 仮想環境 | activate済み | 無効 |
| 標準出力 | 画面に表示 | どこにも出ない |
この表を見れば、なぜ動かないかが分かります。手動実行のつもりで書いたコマンドは、cronでは前提がすべて崩れているのです。
つまずきの原因トップ5
- pythonが見つからない: PATHが違うので
pythonというコマンドが存在しない - 仮想環境が効いていない:
source venv/bin/activateはcronでは意味がない - 相対パスが解決できない: カレントディレクトリがホームなので
./data.csvが見つからない - 環境変数が読めない: APIキーを
.bashrcに書いていると取得できない - エラーが見えない: ログを取っていないので、失敗しても気づけない
確実に動く書き方
原因が分かれば対処は簡単です。すべてを明示的に指定する——これに尽きます。
# 仮想環境のPythonの絶対パスを確認する
cd ~/stock
source venv/bin/activate
which python
# → /home/trader/stock/venv/bin/python
# ログ用のディレクトリを作っておく
mkdir -p ~/stock/logs
# crontab -e で以下を記述する
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
# 平日15:30 に株価を取得する
30 15 * * 1-5 cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py >> /home/trader/stock/logs/fetch.log 2>&1
この1行に、対策がすべて詰まっています。分解して見ていきます。
| 部分 | 役割 |
|---|---|
cd /home/trader/stock | 作業場所を固定し、相対パスを使えるようにする |
/home/.../venv/bin/python | 仮想環境のPythonを直接指定。activateは不要になる |
>> ...log | 標準出力をログに追記する |
2>&1 | エラーも同じログに記録する。これが最重要 |
MAILTO="" | 実行のたびにメールが飛ぶのを止める |
特に2>&1 は絶対に省略しないでください。これがないとエラー内容が消え、「動かないけど理由が分からない」という最悪の状態になります。
また、source venv/bin/activate を書く必要はありません。仮想環境のPythonを直接指定すれば、その環境のライブラリが自動的に使われます。activateは単にPATHを書き換えているだけなので、絶対パスで呼べば同じことです。
動作確認は「数分後」で試す
設定したら、翌日まで待たずにすぐ確認します。待ってから動いていないと分かるのは、時間の無駄です。
# 現在時刻を確認する
date
# → 水 8月 20 16:33:12 JST
# 2〜3分後を指定してテスト用の行を追加する
# 36 16 * * * cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py >> /home/trader/stock/logs/test.log 2>&1
# 時刻が過ぎたら結果を確認する
cat ~/stock/logs/test.log
# cron自体が起動したかを確認する
grep CRON /var/log/syslog | tail -5
確認できたら、テスト用の行は必ず削除してください。毎日不要な実行が走り続けることになります。
切り分けの手順
それでも動かない場合、上から順に確認すれば原因は必ず特定できます。
# ① cronサービス自体が動いているか
systemctl status cron
# ② 設定が正しく登録されているか
crontab -l
# ③ cronが実行を試みたか
grep CRON /var/log/syslog | tail -20
# ④ ログに何が書かれているか
cat ~/stock/logs/fetch.log
# ⑤ 同じコマンドを手で叩いて動くか
cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py
| 症状 | 原因 | 対処 |
|---|---|---|
| ③にログがない | cronが起動していない | sudo systemctl start cron |
| ③にあるが④が空 | コマンドが即座に失敗 | パスの誤りを確認 |
④に command not found | PATHの問題 | 絶対パスで指定する |
④に ModuleNotFoundError | 仮想環境が違う | venvのPythonを指定 |
④に FileNotFoundError | カレントディレクトリ | cd を先頭に付ける |
| ⑤も動かない | スクリプト自体のバグ | cronは無関係。コードを修正 |
この表があれば、ほとんどのトラブルは5分で解決できます。特に⑤を先に試すと、cronの問題かコードの問題かがすぐ分かるので効率的です。
環境変数(APIキー)をどう渡すか
通知用のWebhook URLや証券会社のAPIキーは、cronでは読み込まれません。.bashrc に書いてある環境変数は、cronからは見えないのです。
対処法は3つあり、それぞれ一長一短があります。
| 方法 | 安全性 | 手軽さ |
|---|---|---|
| crontabに直接書く | △ crontab -l で見える | ◎ |
| スクリプト内で.envを読む | ◎ 権限600で保護できる | ○ |
| ラッパースクリプト経由 | ○ | ○ |
おすすめは2番目です。スクリプト側で読み込む形にしておけば、cronでも手動でも同じように動きます。
# ~/stock/.env に秘密情報を置き、権限を絞る
cat > ~/stock/.env << 'EOF'
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxxx
EOF
chmod 600 ~/stock/.env
# ~/stock/env_loader.py
import os
from pathlib import Path
def load_env():
"""スクリプトと同じ場所の .env を読み込む
cron から実行してもカレントディレクトリに依存しないよう、
__file__ を基準にパスを解決するのがポイント
"""
path = Path(__file__).resolve().parent / ".env"
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip())
__file__ を基準にしているのが重要です。カレントディレクトリに依存しないので、どこから実行しても確実に読み込めます。
APIキーの管理方針はVPS契約後に最初にやるUbuntu初期設定でも扱っています。
実務で使えるスクリプトの書き方
cronで動かす前提のスクリプトには、手動実行とは違う配慮が必要です。誰も見ていない場所で動くからです。
# ~/stock/fetch.py
import logging
import sys
import datetime as dt
from pathlib import Path
import yfinance as yf
import pandas as pd
from env_loader import load_env
load_env()
BASE = Path(__file__).resolve().parent
LOG_DIR = BASE / "logs"
DATA_DIR = BASE / "data"
LOG_DIR.mkdir(exist_ok=True)
DATA_DIR.mkdir(exist_ok=True)
# ログの体裁を整える(時刻付きで残す)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_DIR / "fetch.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
TICKERS = {"7203.T": "トヨタ", "6501.T": "日立", "^N225": "日経平均"}
def main():
logging.info("=== 処理開始 ===")
ok, ng = 0, 0
for code, name in TICKERS.items():
try:
df = yf.download(code, period="5d", auto_adjust=True,
progress=False)
if df.empty:
raise ValueError("データが空です")
out = DATA_DIR / f"{code.replace('.', '_')}.csv"
df.to_csv(out)
logging.info(f"{name}: 取得成功 終値 {float(df['Close'].iloc[-1]):,.1f}")
ok += 1
except Exception as e:
logging.error(f"{name}: 取得失敗 {e}")
ng += 1
logging.info(f"=== 処理終了 成功{ok} / 失敗{ng} ===")
return 1 if ng else 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
logging.exception(f"想定外のエラー: {e}")
sys.exit(1)
押さえるべき点は4つです。
- パスは
__file__基準: どこから実行されても壊れない loggingで時刻付きログ:printより情報量が多く、後から追える- 1銘柄失敗しても続行:
try/exceptを個別に置き、全体が止まらないようにする - 終了コードを返す: 失敗時に1を返せば、後続処理で成否を判定できる
3番目が特に重要です。1銘柄の取得に失敗しただけで全体が止まる設計だと、その日のデータがまるごと欠けます。個別に例外を捕まえておけば、取れた分は保存されます。
止まっていることに気づく仕組み
自動化の最大の落とし穴は、止まっていても誰も教えてくれないことです。異常時だけ通知する設計だと、完全に停止したときに何の連絡も来ません。
対策は単純で、正常時にも1日1回「動いています」と通知することです。通知が来ない日=止まっている日、と判断できます。
# ~/stock/notify.py
import os
import sys
import datetime as dt
import requests
from env_loader import load_env
load_env()
def send(text):
url = os.environ.get("DISCORD_WEBHOOK_URL")
if not url:
print("Webhook URLが未設定です")
return False
try:
r = requests.post(url, json={"content": text}, timeout=10)
return r.status_code < 300
except Exception as e:
print(f"通知失敗: {e}")
return False
if __name__ == "__main__":
# 引数で受け取った終了コードに応じてメッセージを変える
code = int(sys.argv[1]) if len(sys.argv) > 1 else 0
now = dt.datetime.now().strftime("%m/%d %H:%M")
mark = "✅ 正常終了" if code == 0 else "⚠️ エラーあり"
send(f"{mark} 株価取得 ({now})")
# crontab: 取得の結果を受けて通知する
30 15 * * 1-5 cd /home/trader/stock && ./venv/bin/python fetch.py >> logs/fetch.log 2>&1; ./venv/bin/python notify.py $? >> logs/notify.log 2>&1
$? は直前のコマンドの終了コードです。これを通知スクリプトに渡すことで、成功したか失敗したかを判別できます。
通知の実装方法はDiscordに株価シグナルを自動送信するコード実装にまとめています。
運用を続けるための整備
ログが無限に増えないようにする
毎日追記していると、数年後にはログが巨大になります。ディスクが埋まってサーバーが止まるという事故は、意外とよく起こります。
sudo tee /etc/logrotate.d/stock > /dev/null << 'EOF'
/home/trader/stock/logs/*.log {
weekly
rotate 8
compress
missingok
notifempty
copytruncate
}
EOF
# 設定を検証する(実際には実行しない)
sudo logrotate -d /etc/logrotate.d/stock
copytruncate を指定しているのは、実行中のプロセスがログを掴んだままでも安全にローテートできるためです。
処理の重複を防ぐ
前回の処理が終わる前に次が始まると、二重に動いてしまいます。データの取得が長引いたときに起こりがちです。
# flock を使うと、多重起動を防げる
*/30 9-15 * * 1-5 /usr/bin/flock -n /tmp/stock.lock -c "cd /home/trader/stock && ./venv/bin/python monitor.py >> logs/monitor.log 2>&1"
flock -n は、すでに実行中なら何もせず終了します。30分ごとに監視するような設定では、これを入れておくと安心です。
祝日に実行しないようにする
cronは曜日しか判定できないので、日本の祝日はスクリプト側で判定します。
import datetime as dt
import sys
# 主要な祝日を列挙しておく(jpholidayライブラリを使う方法もあります)
HOLIDAYS = {
"01-01", "01-02", "01-03", # 年始
"12-31", # 大納会翌日以降
}
def is_market_open(date=None):
"""市場が開いているかを判定する"""
date = date or dt.date.today()
if date.weekday() >= 5: # 土日
return False
if date.strftime("%m-%d") in HOLIDAYS:
return False
return True
if not is_market_open():
print("本日は市場が休場です。処理をスキップします")
sys.exit(0)
祝日を正確に扱いたい場合は jpholiday というライブラリが便利です。エラーログを無駄に増やさないためにも、休場日の判定は入れておくことをおすすめします。
まとめ
cronでPython株価分析スクリプトを確実に自動実行する方法をまとめました。
- cronは普段のシェルとは別の環境で動く。PATHも環境変数も引き継がれない
- 対策はすべてを絶対パスで書くこと。
cdを先頭に付けると相対パスも使える source activateは不要。venvのPythonを直接指定すればよい2>&1は絶対に省略しない。エラーが見えないと原因を追えない- 動作確認は数分後の時刻を指定して即座に行う。翌日まで待たない
- APIキーは
.envに置き、__file__基準で読み込む - 正常時にも通知する。異常検知だけだと停止に気づけない
cronでつまずくのは、能力の問題ではなく「環境が違う」という前提を知らないだけです。これさえ分かってしまえば、あとは機械的に対処できます。
まずは * * * * * env > /tmp/cron_env.txt を1分だけ仕掛けて、cronの環境を自分の目で見てみてください。「これでは動かないはずだ」と腑に落ちるはずです。そこから先の設定は、驚くほどスムーズに進みます。
最後に、自動化全般に言えることを1つ。「動くようにする」より「止まったと気づけるようにする」ほうが、長期的にはずっと重要です。動かす部分は一度作れば終わりますが、止まったことに気づけない状態は何か月も続きます。
本記事の通知スクリプトは10行程度です。この10行があるかないかで、半年後にデータが揃っているかどうかが決まります。設定が終わったら、忘れないうちに入れておいてください。

