概要(「決まった処理をまとめてやるスクリプト」がバッチ)
ここで言う「Pythonバッチ」は、
「人がボタンを押さなくても、決まった処理をまとめて実行するスクリプト」のことです。
例えば、毎朝こんなことを勝手にやってくれるものをイメージしてください。
データベースから前日のデータを取ってくる。
ExcelやCSVを読み込んで集計する。
結果をファイルに出力したり、メールで送る。
ログに成功・失敗を書き残す。
これを「1つのスクリプトとしてしっかり設計する」のが、Pythonバッチです。
cron やタスクスケジューラから呼ばれる前提で、エラーやログも含めて「放置しても回り続ける」ように作ります。
基本構成(main関数・設定・ログを最初に決める)
典型的な「Pythonバッチの骨組み」
最初に「雛形」を持っておくと、毎回ゼロから考えなくて済みます。
最低限、main関数・ログ出力・設定の3つを用意します。
# batch_example.py
import logging
from datetime import datetime
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
LOG_FILE = BASE_DIR / "logs" / "batch_example.log"
def setup_logging():
LOG_FILE.parent.mkdir(exist_ok=True)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
def main():
logging.info("バッチ開始")
try:
run_job()
logging.info("バッチ正常終了")
except Exception as e:
logging.exception(f"バッチ異常終了: {e}")
raise
def run_job():
logging.info("ここにメイン処理を書く")
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logging.info(f"現在時刻: {now}")
if __name__ == "__main__":
setup_logging()
main()
Pythonこの骨組みで大事なのは、
「ログの場所を固定する」「main で try/except して必ずログに残す」ことです。
これだけで「夜中に失敗していたのに気づけない」という事故が一気に減ります。
実例1:CSVを毎日集計してレポートを出すバッチ
要件をシンプルに言語化する
例として、こんなバッチを作ってみます。
毎日、data/input/ に「日別売上CSV」が増えていく。
ファイル名は sales_YYYYMMDD.csv とする。
毎朝9時に「前日分のCSV」を読み込み、カテゴリ別売上を集計し、output/report_YYYYMMDD.csv として保存する。
この要件を、そのままコードに落とします。
pandasを使った集計バッチの例
# daily_sales_batch.py
import logging
from datetime import datetime, timedelta
from pathlib import Path
import pandas as pd
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data" / "input"
OUT_DIR = BASE_DIR / "output"
LOG_FILE = BASE_DIR / "logs" / "daily_sales_batch.log"
def setup_logging():
LOG_FILE.parent.mkdir(exist_ok=True)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
def get_target_date(today=None):
if today is None:
today = datetime.today()
return (today - timedelta(days=1)).date()
def build_input_path(target_date):
fname = f"sales_{target_date.strftime('%Y%m%d')}.csv"
return DATA_DIR / fname
def build_output_path(target_date):
fname = f"report_{target_date.strftime('%Y%m%d')}.csv"
return OUT_DIR / fname
def run_job():
OUT_DIR.mkdir(exist_ok=True)
target_date = get_target_date()
logging.info(f"対象日: {target_date}")
input_path = build_input_path(target_date)
output_path = build_output_path(target_date)
if not input_path.exists():
logging.warning(f"入力ファイルがありません: {input_path}")
return
logging.info(f"入力ファイル読み込み: {input_path}")
df = pd.read_csv(input_path)
required_cols = {"date", "category", "amount"}
if not required_cols.issubset(df.columns):
missing = required_cols - set(df.columns)
raise ValueError(f"必要な列が足りません: {missing}")
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
summary = (
df.groupby("category", as_index=False)["amount"]
.sum()
.rename(columns={"amount": "total_amount"})
)
logging.info(f"出力ファイル保存: {output_path}")
summary.to_csv(output_path, index=False, encoding="utf-8-sig")
def main():
logging.info("日次売上バッチ開始")
try:
run_job()
logging.info("日次売上バッチ正常終了")
except Exception as e:
logging.exception(f"日次売上バッチ異常終了: {e}")
raise
if __name__ == "__main__":
setup_logging()
main()
Pythonここで深掘りしたいポイントは4つあります。
一つ目は「対象日」を関数で切り出していることです。
本番は「昨日」ですが、テストしたいときに任意の日付を渡せる設計にしておくと、後から楽になります。
二つ目は「パスを組み立てる関数」を作っていることです。
ファイル名の規則が変わっても、その関数だけ直せばよくなります。
三つ目は「入力ファイルが無いときの扱い」を意図的に決めていることです。
この例では警告ログだけ出して終了にしていますが、
「必ずある前提ならエラーで止める」という設計もありです。
四つ目は「列チェック」と「型変換」を明示的に行っていることです。
テンプレが壊れていても気づけるように、「必須列が無ければ即例外」を投げています。
実例2:APIを叩いてデータ取得 → DB保存するバッチ
外部サービスと連携するバッチのイメージ
もう少し“バッチ感”の強い例を出します。
外部API(例えばWebサービスのレポートAPI)からデータを取ってくる。
JSONを整形して、ローカルのSQLiteやPostgreSQLへ保存する。
毎時間実行して、差分を蓄積していく。
この場合も、「やること」は同じです。
設定を読み込む。
ログを初期化する。
APIを呼ぶ。
レスポンスを検証する。
DBに保存する。
これを一つの流れとして main にまとめます。
超シンプルなAPIバッチの骨組み
# fetch_api_batch.py
import logging
from datetime import datetime
from pathlib import Path
import requests
import sqlite3
BASE_DIR = Path(__file__).resolve().parent
LOG_FILE = BASE_DIR / "logs" / "fetch_api_batch.log"
DB_FILE = BASE_DIR / "data" / "api_data.db"
API_URL = "https://example.com/api/data"
def setup_logging():
LOG_FILE.parent.mkdir(exist_ok=True)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
def get_db_connection():
DB_FILE.parent.mkdir(exist_ok=True)
conn = sqlite3.connect(DB_FILE)
return conn
def init_db(conn):
conn.execute("""
CREATE TABLE IF NOT EXISTS api_data (
id INTEGER PRIMARY KEY,
value TEXT,
fetched_at TEXT
)
""")
conn.commit()
def fetch_data():
logging.info(f"API呼び出し: {API_URL}")
resp = requests.get(API_URL, timeout=10)
resp.raise_for_status()
return resp.json()
def save_data(conn, data):
now = datetime.utcnow().isoformat()
conn.execute(
"INSERT INTO api_data (value, fetched_at) VALUES (?, ?)",
(str(data), now)
)
conn.commit()
def run_job():
conn = get_db_connection()
try:
init_db(conn)
data = fetch_data()
save_data(conn, data)
logging.info("データ保存完了")
finally:
conn.close()
def main():
logging.info("API取得バッチ開始")
try:
run_job()
logging.info("API取得バッチ正常終了")
except Exception as e:
logging.exception(f"API取得バッチ異常終了: {e}")
raise
if __name__ == "__main__":
setup_logging()
main()
Pythonここで重要なのは、「ネットワークエラーやAPIエラーを logging.exception で必ずログに落とす」ことと、「DB接続のクローズを finally で保証する」ことです。
バッチは失敗すること自体は避けられませんが、「失敗の痕跡が残るか」が運用での生き死にを分けます。
設計の重要ポイントを深掘り(堅いバッチにするための考え方)
絶対パスとPathで「どこから実行しても動く」ようにする
バッチは cron やタスクスケジューラから実行されることが多く、
カレントディレクトリがどこか分からない状態で動きます。
open("input.csv") のような相対パスは、
実行環境によって簡単に壊れます。
__file__ から「スクリプトが置かれているディレクトリ」を基準に、
Pathで絶対パスを組み立てる癖をつけると、どこから実行しても動くようになります。
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
Pythonこの書き方を「バッチの雛形」にしておくと、後から自分を助けてくれます。
ログレベルとメッセージ粒度
loggingには DEBUG / INFO / WARNING / ERROR / CRITICAL があります。
バッチでは、普段は INFO レベルで十分ですが、
開発中は DEBUG まで出したくなることもあります。
最初から format やレベルを一箇所で設定し、
「何をINFOにするか」「何をWARNINGにするか」を決めておくと、
ログファイルが読みやすくなります。
大事なのは、「誰が見ても、いつ、何を、どこまでやったか分かるメッセージ」にすることです。
「処理開始」「入力ファイル名」「件数」「処理終了」「異常終了の理由」
このあたりを書く癖をつければ、運用のストレスがかなり減ります。
失敗したときに再実行しやすい設計(冪等性)
バッチは途中で失敗することがあります。
例えば、半分までDBに書いたところでエラーになった、とか。
そのときに「もう一度実行してもおかしくならない」ように設計しておくと、運用が楽です。
例えば、こんな工夫があります。
対象データを「日付やID」単位で完全に上書きする。
処理前に既存データを削除してから入れ直す。
一度処理したファイルを「done」フォルダへ移動する。
処理済みフラグをDBに持っておく。
「再実行したときに何が起きるか」を最初に想像しながら、
データの持ち方を決めると、後から「変な二重登録」に悩まされにくくなります。
バッチとcron / タスクスケジューラのつなぎこみ
Linux / Macでの定期実行イメージ
先ほどの daily_sales_batch.py を毎朝9時に動かしたい場合は、
cronに次のような行を登録します。
0 9 * * * /home/you/project/venv/bin/python /home/you/project/daily_sales_batch.py >> /home/you/project/logs/cron_stdout.log 2>&1
「どのPythonで」「どのスクリプトを」「どのログに出すか」を、
バッチ側ではなく cron 側で決めるイメージです。
Windowsでの定期実行イメージ
タスクスケジューラでは、
プログラム/スクリプトにC:\project\venv\Scripts\python.exe
引数にC:\project\daily_sales_batch.py
開始フォルダにC:\project
を指定し、トリガーを「毎日9:00」にする、という感じです。
重要なのは、「スクリプトは普通のPythonとして書いて、スケジュールはOSが担当する」という分担をきっちり分けることです。
バッチ側に「時間管理」を書かない方がすっきりします。
まとめ(「雛形を決める」「ログとパスをきちんと設計する」だけで一気に“バッチらしく”なる)
Pythonバッチの本質は、特別な技術ではなく「きちんとしたスクリプトの書き方」です。
main関数を作り、ログを初期化し、絶対パスでファイルを扱い、
エラー時には logging.exception で理由を残す。
対象日やファイル名の規則を関数に切り出し、
再実行したときに壊れないようなデータ設計をする。
この型を一度作ってしまえば、後は「中身の処理」を入れ替えるだけで
いろいろなバッチを作れるようになります。
