正直に書くと、毎朝7時にPythonスクリプトを1本動かしたいだけで、3日溶かしました。
Macなら launchd か cron で5分の話です。Windowsだって、タスクスケジューラに登録するだけ。そう信じていた金曜の夜、私はPowerShellの真っ黒な画面と、何も書かれていない結果欄を見つめていました。
地方の静かな部屋で、コーヒーが3杯目に入っていました。
やりたかったのは本当に単純な話
業務で使うCSVを朝7時にダウンロードして、整形して、Slackに通知する。Pythonで書いた150行のスクリプトを、出勤前に終わらせておきたかっただけです。
スクリプト自体はPowerShellから手で叩けば完璧に動きます。
python C:\work\daily_report.py
戻り値0、出力もきれい、Slack通知もちゃんと飛んでくる。これを朝7時にタスクスケジューラから自動実行する。それだけの話のはずでした。
1日目: そもそも起動しない
最初、私は素直にタスクスケジューラの「基本タスクの作成」で進めました。
プログラム欄に python、引数に C:\work\daily_report.py。実行すると、結果欄に 0x1 という見慣れない数字が返ってきました。
ログをまず見ようとして、ここで2つ目の失敗をします。タスクスケジューラの「履歴」タブが無効化されていました。右側ペインの「すべてのタスク履歴を有効にする」を押してから、もう一度実行。
それでも 0x1。これは「一般的なエラー」という、何も言っていないに等しい返事です。
最終的にわかった原因は、python をフルパスで書いていなかったことでした。タスクスケジューラはユーザーの PATH をそのまま継承しません。コマンドプロンプトでは通っても、スケジューラからは別人として呼ばれます。
プログラム: C:\Users\taka\AppData\Local\Programs\Python\Python312\python.exe
引数: C:\work\daily_report.py
開始(オプション): C:\work
ポイントは3つ目の「開始」です。ここを空欄にしていると、相対パスでファイルを読むスクリプトが全部落ちます。私はここで2時間溶かしました。
2日目: 動いたけど文字化けで死ぬ
python.exe をフルパスにして、開始ディレクトリも入れて、これでようやく結果欄が 0x0 になりました。完了です。やった、と思いました。
ところが、ログファイルを開くと、こうなっていました。
?????? ???? ?? ?????
スクリプト内で print("処理開始: 朝のCSV取得") と書いている部分が、全部クエスチョンマークになっていました。
原因はWindowsコンソールの既定エンコーディングです。手動でPowerShellから叩いた時はUTF-8で動くのに、タスクスケジューラから起動されると cp932 環境で走り、UTF-8の絵文字や記号が化けます。
対策は2行で済みました。スクリプトの先頭に、こう入れます。
import sys
import io
if hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
これでstdoutもstderrもUTF-8で吐かれます。ファイルに logging で書く場合は、明示的に encoding="utf-8" を渡します。
import logging
from pathlib import Path
log_path = Path("C:/work/logs/daily_report.log")
log_path.parent.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
filename=str(log_path),
encoding="utf-8",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logging.info("処理開始: 朝のCSV取得")
地味ですが、encoding="utf-8" を書き忘れると、Windowsの既定で cp932 になります。日本語コメントを書いた瞬間に、ファイルが壊れます。私はここで90分溶かしました。
3日目: 動いたり動かなかったりする
3日目、文字化けは直りました。タスクスケジューラの結果欄も 0x0。完璧です。
ところが、月曜の朝、Slack通知が来ませんでした。
タスクスケジューラを開くと、ステータスは「正常終了」。でも、ログには何も残っていません。スクリプトが1行も実行されていない。
ここで30分悩んで、ようやく気づきました。タスクスケジューラの設定で、こうしていたのが原因です。
[全般] ユーザーがログオンしているときのみ実行する
これだと、PCがスリープしていたり、私がログオフしていると、タスクが「成功」したことになって何もしないで終わります。これが一番たちが悪い。
対策はチェックを切り替えます。
[全般] ユーザーがログオンしているかどうかにかかわらず実行する
[全般] 最上位の特権で実行する (必要な場合のみ)
[条件] コンピューターをAC電源で使用している場合のみタスクを開始する (オフ)
[条件] タスクを実行するためにスリープを解除する (オン)
[設定] タスクを要求時に実行する (オン)
[設定] スケジュールされた時刻にタスクを開始できなかった場合、すぐにタスクを実行する (オン)
「ログオンしているかどうかにかかわらず実行する」を選ぶと、Windowsはユーザーパスワードを要求します。アプリケーションパスワードではなく、Windowsログインのパスワードです。これを入れないと保存できません。
ここで企業環境だと、IT管理者と相談が要る場面が出てきます。個人PCなら自分のパスワードを入れて先へ進みます。
結局たどり着いた最小構成
最終的に、私の朝7時タスクはこの形に落ち着きました。
タスクスケジューラ側の設定はこうです。
名前: daily_report_7am
全般:
- ユーザーがログオンしているかどうかにかかわらず実行する
- 構成: Windows 10
トリガー:
- 毎日 07:00 開始
- 同期する時間帯: ローカル
操作:
- プログラム: C:\Users\taka\AppData\Local\Programs\Python\Python312\python.exe
- 引数: -X utf8 C:\work\daily_report.py
- 開始: C:\work
条件:
- AC電源条件: オフ
- スリープ解除: オン
設定:
- 失敗したら3分後に再試行を3回まで
- タスクが3時間を超えたら停止
- すぐ実行: スケジュール時刻にPCが落ちていたら起動後に補完実行
ポイントは引数の -X utf8 です。Python 3.7以降で使えるUTF-8モードで、内部のファイル読み書きやprintのデフォルトをUTF-8に統一できます。スクリプト側にエンコーディングのおまじないを書く必要が少し減ります。
そして実行されるPythonスクリプトの先頭は、こんな感じにしています。
# daily_report.py
from __future__ import annotations
import io
import os
import sys
import logging
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
JST = ZoneInfo("Asia/Tokyo")
BASE = Path(r"C:\work")
LOG_PATH = BASE / "logs" / "daily_report.log"
def setup_logging() -> logging.Logger:
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
if hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
logger = logging.getLogger("daily_report")
logger.setLevel(logging.INFO)
logger.handlers.clear()
fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
fh = logging.FileHandler(LOG_PATH, encoding="utf-8")
fh.setFormatter(fmt)
sh = logging.StreamHandler()
sh.setFormatter(fmt)
logger.addHandler(fh)
logger.addHandler(sh)
return logger
def main() -> int:
logger = setup_logging()
started_at = datetime.now(JST)
logger.info("daily_report 起動: %s", started_at.isoformat(timespec="seconds"))
try:
# ハマりポイント: 相対パスは絶対に書かない。BASEからの絶対パスで統一する
target = BASE / "input" / "today.csv"
if not target.exists():
logger.warning("入力ファイルが無い: %s", target)
return 0 # 失敗ではなく「やることが無い」で正常終了
# ここで実際の処理を呼ぶ
logger.info("CSV取得開始")
# ... 本体 ...
logger.info("Slack通知済み")
return 0
except Exception:
logger.exception("daily_report 失敗")
return 1
if __name__ == "__main__":
sys.exit(main())
この骨格にしてから、朝7時の通知は1回も落ちていません。
個人的に効いた4つの確認手順
3日溶かして得た教訓は、地味な4つです。
1つ目は、タスクスケジューラに登録する前に、必ず別の標準ユーザー権限で cmd /c "python C:\work\daily_report.py" をフルパスで叩いてみることです。手動で動いても、タスクスケジューラの実行ユーザーから動くかどうかは別の話です。
2つ目は、Pythonをフルパスで指定することです。python だけで書くと、PATHの解決に依存して罠だらけになります。仮想環境を使うなら、venvの中の python.exe を直接指定します。
3つ目は、ログをファイルに必ず吐くことです。タスクスケジューラの結果欄は 0x0 か 0x1 しか教えてくれません。本当の失敗理由は、自分でファイルに書かないと分かりません。
4つ目は、失敗を恐れずに 0 を返すことです。「ファイルが無い」「Slackが落ちている」みたいな「外因の正常」は、戻り値を1にしないほうが落ち着きます。タスクスケジューラの履歴が真っ赤になると、本当に直すべき事故が埋もれます。
やってよかったコマンド一発登録
GUIでポチポチやるのも疲れたので、最終的にPowerShellワンライナーで登録するようにしました。
$Action = New-ScheduledTaskAction `
-Execute "C:\Users\taka\AppData\Local\Programs\Python\Python312\python.exe" `
-Argument "-X utf8 C:\work\daily_report.py" `
-WorkingDirectory "C:\work"
$Trigger = New-ScheduledTaskTrigger -Daily -At 7:00am
$Settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-DontStopIfGoingOnBatteries `
-AllowStartIfOnBatteries `
-WakeToRun
Register-ScheduledTask `
-TaskName "daily_report_7am" `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-User "$env:USERDOMAIN\$env:USERNAME" `
-RunLevel Highest
これで、PCを買い替えても10秒で再現できます。GUIで設定した内容をスクショに残すより、コードで残したほうが楽です。3年後の自分が泣かなくて済みます。
実際にやってみた感想として、Windowsタスクスケジューラは決して悪い道具ではありません。ただ、隠れた前提が多すぎるだけです。私の環境では、上の構成で半年運用していて、停止は計画再起動の翌朝1回だけでした。
私が3日で学んだ唯一の教訓
最初はエラーが出るたびに、新しい設定項目を探していました。
でも、振り返ってみると、私が踏んだ罠は全部「実行ユーザーが違う」「文字コードが違う」「PCが起きていない」の3パターンに集約されます。
タスクスケジューラの設定画面には、本当にたくさんのチェックボックスがあります。全部を理解しようとすると、また3日溶かします。
最初に押さえるのは、フルパス、UTF-8、ログオン状態。この3つだけです。残りは動いてから1個ずつ理解すればいいと、私は思います。
まとめ
Windowsタスクスケジューラは、Macのcronより一段難しい道具です。
でも、フルパスでPythonを指定し、UTF-8で出力を統一し、ログオン状態に依存しない設定にすれば、毎朝7時に確実に動きます。私の場合、3日溶けましたが、それ以降は1回も止まっていません。
業務自動化を始める時、一番ハードルが高いのは「コードを書くこと」ではなく「毎日確実に動かすこと」です。
このしょぼいPowerShellスクリプト1つで、自分の生活が10分早く始まる。そういう小さな自動化を積み上げていくのが、地方で1人でコードを書く週末の楽しみになりました。
コメント