問題提起:あなたは毎日どれだけの時間を無駄にしているか?
サーバーを扱う上で、ほぼ毎日やらなければならない作業がある:古いログのチェック、一時ファイルの削除、データベースのバックアップ、そしてチームへのレポート送信。1つひとつは5〜10分程度。でも1週間分を合計すると、約1時間になる——しかもスクリプト化できる単純作業のために。
問題は時間だけではない。手動でやると忘れやすく、ステップを飛ばしやすい——サーバーがクラッシュした日に限って、バックアップを忘れていたりする。だからこそ、こうした繰り返し作業を少しずつスクリプト化してきた。
この記事では、実際にcrontabで毎日動かしているスクリプトを紹介する——教科書的なサンプルではなく。ブログのAPSchedulerやREST APIの記事を読んだことがあれば、この記事はシステム層でのPythonの活用を示すものだ:ファイルシステム、プロセス、そしてプッシュ通知。
基本概念:Pythonでシステムを操作する
自動化スクリプトを書くには、標準ライブラリの重要な4つのモジュールを押さえておく必要がある:
- os / pathlib — ディレクトリのトラバース、ファイルの確認、メタデータの取得
- shutil — ファイルやディレクトリのコピー、移動、削除
- subprocess — PythonからシェルコマンドをE行する(tar、mysqldump、rsync…)
- smtplib / requests — メール送信や通知Webhookの呼び出し
Pythonはすべての面でbashに取って代わるものではないが、より複雑なタスク——複数条件でのファイルフィルタリング、JSONレスポンスのパース、HTTP APIの呼び出し——においては、コードがずっとシンプルになりデバッグもしやすい。
実践:実際に使える3つの自動化スクリプト
スクリプト1 — Downloadsフォルダの自動整理
~/Downloads フォルダは、あらゆるものが溜まる一方で誰も片付けない場所だ。このスクリプトは拡張子でファイルを分類し、対応するフォルダへ移動する:
from pathlib import Path
import shutil
DOWNLOADS = Path.home() / "Downloads"
CATEGORIES = {
"images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
"documents": [".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md"],
"archives": [".zip", ".tar", ".gz", ".rar", ".7z"],
"scripts": [".py", ".sh", ".js", ".ts"],
"videos": [".mp4", ".mkv", ".mov", ".avi"],
}
def organize_downloads():
moved = 0
for file in DOWNLOADS.iterdir():
if not file.is_file():
continue
ext = file.suffix.lower()
for category, extensions in CATEGORIES.items():
if ext in extensions:
dest_dir = DOWNLOADS / category
dest_dir.mkdir(exist_ok=True)
dest = dest_dir / file.name
# 同名ファイルが既に存在する場合は上書きを避ける
if dest.exists():
dest = dest_dir / f"{file.stem}_{file.stat().st_mtime_ns}{ext}"
shutil.move(str(file), str(dest))
moved += 1
break
print(f"{moved} 個のファイルを移動しました。")
if __name__ == "__main__":
organize_downloads()
初めて実行したとき、数ヶ月分溜まっていた340個のファイルをまとめてくれた。crontabに追加するだけで完成——毎朝きれいな状態で始められる。
スクリプト2 — ローテーション付きMySQLデータベースの自動バックアップ
実際にVPSで使っているスクリプトだ。便利なのは、N日以上古いバックアップを自動削除してディスク容量を節約してくれる点:
import subprocess
import os
from pathlib import Path
from datetime import datetime, timedelta
DB_HOST = "localhost"
DB_USER = "backup_user"
DB_PASS = os.environ["MYSQL_BACKUP_PASS"] # 環境変数から読み込む、絶対にハードコードしない
DB_NAME = "myapp_db"
BACKUP_DIR = Path("/var/backups/mysql")
RETENTION_DAYS = 7
def backup_mysql():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = BACKUP_DIR / f"{DB_NAME}_{timestamp}.sql.gz"
cmd = [
"mysqldump",
f"--host={DB_HOST}",
f"--user={DB_USER}",
f"--password={DB_PASS}",
"--single-transaction",
"--quick",
DB_NAME,
]
with open(filename, "wb") as f:
dump = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
gzip = subprocess.Popen(["gzip"], stdin=dump.stdout, stdout=f)
dump.stdout.close()
gzip.communicate()
dump.wait() # mysqldumpの終了を待ち、正確なreturncodeを取得する
if dump.returncode != 0:
raise RuntimeError(f"mysqldumpが失敗しました: {dump.stderr.read().decode()}")
print(f"バックアップ完了: {filename} ({filename.stat().st_size / 1024:.1f} KB)")
return filename
def cleanup_old_backups():
cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
removed = 0
for f in BACKUP_DIR.glob(f"{DB_NAME}_*.sql.gz"):
mtime = datetime.fromtimestamp(f.stat().st_mtime)
if mtime < cutoff:
f.unlink()
removed += 1
if removed:
print(f"{RETENTION_DAYS}日以上前の古いバックアップを{removed}件削除しました。")
if __name__ == "__main__":
backup_mysql()
cleanup_old_backups()
重要な注意点:パスワードは環境変数 MYSQL_BACKUP_PASS から読み込み、スクリプトに直接書かないこと。かつてこのミスを犯し、パスワードごとGitにコミットしてしまった——高い授業料だった。
スクリプト3 — フォルダ監視とTelegram通知送信
このスクリプトは30秒ごとにフォルダをポーリングする。新しいファイルを検出すると、すぐにTelegram Botで通知を送る。クライアントのアップロードフォルダ、夜間に自動出力されるレポートフォルダ、新しいファイルが入ったらすぐに知りたい場所に最適だ:
import time
import requests
import os
from pathlib import Path
WATCH_DIR = Path("/var/www/uploads")
TG_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TG_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
POLL_INTERVAL = 30 # 秒
def send_telegram(message: str):
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
requests.post(url, json={"chat_id": TG_CHAT_ID, "text": message}, timeout=10)
def monitor_directory():
print(f"監視中: {WATCH_DIR}")
known_files = set(WATCH_DIR.iterdir())
while True:
time.sleep(POLL_INTERVAL)
current_files = set(WATCH_DIR.iterdir())
new_files = current_files - known_files
for f in new_files:
size_kb = f.stat().st_size / 1024
msg = f"📁 新しいファイル: {f.name}\nサイズ: {size_kb:.1f} KB\nフォルダ: {WATCH_DIR}"
send_telegram(msg)
print(msg)
known_files = current_files
if __name__ == "__main__":
monitor_directory()
crontabで自動実行する
これらのスクリプトを定期実行するには、crontabに追加する:
# crontabを開く
crontab -e
# 毎朝8時にDownloadsを整理
0 8 * * * /usr/bin/python3 /home/user/scripts/organize_downloads.py >> /var/log/organize.log 2>&1
# 毎日午前2時にMySQLをバックアップ
0 2 * * * /usr/bin/python3 /home/user/scripts/backup_mysql.py >> /var/log/backup.log 2>&1
stdoutとstderrの両方をログファイルにリダイレクト(2>&1)しておくと、深夜にエラーが発生したときのデバッグが楽になる。
実践から学んだこと:大量ファイル処理時のパフォーマンス
nginxの約500MBのアクセスログを処理していたとき、ファイル全体を一度にRAMに読み込んでいたため、スクリプトが MemoryError でクラッシュした。修正は実はとても単純だった:readlines() をやめて、Pythonに1行ずつストリーミングさせる。2つの書き方を比べてみよう——結果は同じでも、メモリ使用量は大きく異なる:
# ナイーブな方法 — 大きなファイルでOOMになる
with open("big_file.csv") as f:
lines = f.readlines() # RAM全体にロードする!
for line in lines:
process(line)
# 正しい方法 — 1行ずつストリーミング
with open("big_file.csv") as f:
for line in f: # ジェネレータ、常に1行だけロードする
process(line)
差は歴然だ:最初の方法では500MBのファイルが1〜2GBのRAMを消費するのに対し、2番目の方法はファイルがどんなに大きくてもほぼ変わらない。毎晩のログ処理スクリプトは、この方法に切り替えてからクラッシュしていたのが50MB以下の安定したメモリ使用量に改善された。
まとめ
3つのスクリプト、3つの異なる問題——でも共通の哲学がある:一度書けば、ずっと動く。構文やモジュールは難しくない。難しいのは習慣を身につけること:「これは来週もまたやる——スクリプト化しよう。」
自分が守っているルール:同じやり方で3回以上やった作業は、スクリプト化する。今日30分かけると、数週間後には元が取れる——それ以上に大切なのは、「手動チェックリスト」を頭の中で管理しなくていいようになることだ。

