跳至主要內容
問題與解答
網站維運的自動化腳本

網站維運的自動化腳本

問:
備份腳本要怎麼寫? 排程沒執行但也沒有錯誤通知 cron 執行找不到指令怎麼辦? 怎麼避免腳本重複執行?
答:

自動化的三個原則

  1. 失敗要能被知道——靜默失敗是最糟的情況
  2. 可以重複執行——重跑一次不應造成問題
  3. 留下紀錄——出事時能追溯

第一項最常被忽略。排程腳本停止運作而沒人發現,比沒有這個腳本更危險——因為你以為它在跑。

用 logging 而不是 print

import logging
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
LOG_FILE = BASE_DIR / 'logs' / 'task.log'
LOG_FILE.parent.mkdir(exist_ok=True)

logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    encoding='utf-8',
)

logging.info('開始執行')
logging.warning('略過 3 筆資料')
logging.error('連線失敗')

為什麼不用 print

  • 排程執行時 print 的輸出可能不知去向
  • 沒有時間戳記
  • 無法分級
  • 無法在不改程式的情況下調整詳細程度

錯誤處理與通知

import logging, sys, traceback

def main():
    # 實際工作
    ...

if __name__ == '__main__':
    try:
        main()
        logging.info('執行完成')
    except Exception:
        logging.error('執行失敗\n' + traceback.format_exc())
        notify('備份腳本執行失敗,請查看日誌')
        sys.exit(1)

通知的方式

  • 寄送電子郵件
  • 推送到通訊軟體
  • 寫入監控系統

離開時要回傳非零的結束碼——排程系統與監控工具可以據此判斷失敗。

只監控失敗是不夠的

這一點很重要:如果排程根本沒執行,就不會有失敗通知。

建議的做法

  • 成功時也記錄一筆,並定期確認有這筆紀錄
  • 檢查產出的檔案——備份檔的時間戳是否為最新
  • 設定「超過 N 小時未成功執行就通知」的監控

這與 API 串接的靜默失效是同樣的問題。

資料庫備份腳本

import subprocess, gzip, shutil, os
from pathlib import Path
from datetime import datetime, timedelta

BACKUP_DIR = Path('/backup/db')
KEEP_DAYS = 14

def backup(dbname):
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime('%Y%m%d_%H%M%S')
    sql_path = BACKUP_DIR / f'{dbname}_{ts}.sql'

    cmd = [
        'mysqldump',
        '--defaults-extra-file=/etc/mysql/backup.cnf',
        '--single-transaction', '--routines', '--triggers', '--events',
        '--default-character-set=utf8mb4',
        dbname,
    ]
    with open(sql_path, 'wb') as out:
        subprocess.run(cmd, stdout=out, check=True)

    # 壓縮
    gz_path = sql_path.with_suffix('.sql.gz')
    with open(sql_path, 'rb') as f_in, gzip.open(gz_path, 'wb') as f_out:
        shutil.copyfileobj(f_in, f_out)
    sql_path.unlink()

    # 驗證檔案不是空的
    if gz_path.stat().st_size < 1024:
        raise RuntimeError('備份檔案異常過小')

    return gz_path

三個重點

  • 密碼放在設定檔而非指令列——指令列參數會出現在行程列表
  • check=True——指令失敗時會拋出例外,不會靜默continue
  • 驗證產出檔案——大小異常就視為失敗

備份的完整要求見資料庫備份與還原的技術要點

清理過期檔案

from datetime import datetime, timedelta

def cleanup(folder, keep_days=14):
    cutoff = datetime.now() - timedelta(days=keep_days)
    removed = 0
    for f in Path(folder).glob('*.gz'):
        if datetime.fromtimestamp(f.stat().st_mtime) < cutoff:
            f.unlink()
            removed += 1
    logging.info(f'清除過期備份 {removed} 個')

務必先用「只列出不刪除」的版本確認,刪錯檔案是無法復原的。

日誌分析

import re
from collections import Counter

pattern = re.compile(r'^(\S+) .* "(\w+) (\S+).*" (\d{3})')

def analyze(log_path, top=20):
    ips, paths, status = Counter(), Counter(), Counter()
    with open(log_path, encoding='utf-8', errors='replace') as f:
        for line in f:
            m = pattern.match(line)
            if not m:
                continue
            ip, method, path, code = m.groups()
            ips[ip] += 1
            status[code] += 1
            if code == '404':
                paths[path] += 1
    return ips.most_common(top), status, paths.most_common(top)

實用的分析方向

  • 請求量異常的來源——可能是掃描或攻擊
  • 404 最多的路徑——改版後的遺漏轉址
  • 狀態碼分布——5xx 突然增加代表有問題

日誌判讀見Nginx 與 Apache 的疑難排解

排程設定

# crontab -e

# 每日凌晨 3 點備份
0 3 * * * /path/to/.venv/bin/python /path/to/scripts/backup.py

# 每小時檢查
0 * * * * /path/to/.venv/bin/python /path/to/scripts/check.py

四個常見的排程陷阱

  1. 沒用絕對路徑——排程的工作目錄與環境變數與登入時不同
  2. 沒指定虛擬環境的直譯器——會用到系統的 Python 而找不到套件
  3. 時區設定不同——確認伺服器時區
  4. 執行時間重疊——上一次還沒跑完就啟動下一次

防止重複執行

import fcntl, sys

lock_file = open('/tmp/mytask.lock', 'w')
try:
    fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
    logging.warning('前一次執行尚未結束,本次略過')
    sys.exit(0)

可重複執行的設計

腳本應該重跑一次也不會造成問題

  • 寫入前先檢查是否已存在
  • 用「更新或新增」而非單純新增
  • 檔案輸出時包含時間戳,或先寫暫存檔再改名
# 先寫暫存檔,完成後才改名
tmp = target.with_suffix('.tmp')
write_data(tmp)
tmp.replace(target)   # 原子性操作

這樣即使中途失敗,也不會留下半完成的檔案。

先做不會造成傷害的版本

任何會刪除、覆蓋、修改資料的腳本,建議都先做一個「只顯示會做什麼」的模式

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--dry-run', action='store_true',
                    help='只顯示會執行的動作,不實際執行')
args = parser.parse_args()

if args.dry_run:
    print(f'[模擬] 將刪除 {f}')
else:
    f.unlink()

這個習慣可以避免大部分的災難。

本文的範例以 Python 3 為例,實際的套件版本與 API 可能隨版本更新而異。在正式環境執行任何批次處理前,請先以少量資料測試並確實備份。

發表於2026-08-05   更新於2026-08-24
免費諮詢 · 1 個工作天內回覆

準備好讓網站 開始幫你帶生意了嗎?

不論是要做新網站、救舊網站,還是只想先聊聊方向——先諮詢,不用先付錢,我們照實給你建議。

1 個工作天回覆免費諮詢與報價費用白紙黑字26 年找得到人
免費諮詢
免費諮詢 LINE諮詢 03-4681000 臉書傳訊
免費諮詢 LINE諮詢 03-4681000 臉書傳訊
免費諮詢
免費諮詢 LINE諮詢 03-4681000 臉書傳訊
免費諮詢 LINE諮詢 03-4681000 臉書傳訊