本日主題:把昨天的作品優化、加錯誤處理、寫成可展示的成品 預計時間:2 小時(動手日) 對應主教材:第 6.3 節


一、今日學習目標

  • [ ] 為作品加上錯誤處理與訊息
  • [ ] 讓作品更通用、好用
  • [ ] 寫一份 README 說明
  • [ ] 學會用 logging 記錄執行日誌
  • [ ] 了解 Windows 工作排程器的完整設定

二、教材內容

2.1 加上錯誤處理(讓程式更穩)

import pandas as pd
import os
import logging
from datetime import datetime

# ---- 日誌設定 ----
logging.basicConfig(
    filename=f"report_{datetime.now().strftime('%Y%m%d')}.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    encoding="utf-8"
)

# ---- 設定區 ----
INPUT = "daily_data.xlsx"
GROUP_COL = "部門"
VALUE_COLS = {"數量": "sum", "金額": "sum"}

try:
    # 檢查輸入檔案
    if not os.path.exists(INPUT):
        raise FileNotFoundError(f"找不到檔案:{INPUT}")

    logging.info(f"開始處理:{INPUT}")
    df = pd.read_excel(INPUT)
    logging.info(f"讀取成功,共 {len(df)} 筆資料")

    # 檢查必要欄位
    required_cols = [GROUP_COL] + list(VALUE_COLS.keys())
    missing = [c for c in required_cols if c not in df.columns]
    if missing:
        raise ValueError(f"資料缺少欄位:{missing}")

    # 處理缺值
    df[GROUP_COL] = df[GROUP_COL].fillna("未分類")
    for col in VALUE_COLS:
        df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0)

    # 彙總
    agg_dict = {col: (col, func) for col, func in VALUE_COLS.items()}
    summary = df.groupby(GROUP_COL).agg(**{
        f"總{col}": (col, func) for col, func in VALUE_COLS.items()
    }).reset_index()

    # 輸出
    stamp = datetime.now().strftime("%Y%m%d_%H%M")
    output = f"彙總報表_{stamp}.xlsx"
    summary.to_excel(output, index=False)

    logging.info(f"完成!輸出:{output},共 {len(summary)} 個{GROUP_COL}")
    print(f"✅ 完成!共 {len(summary)} 個{GROUP_COL},輸出:{output}")

except FileNotFoundError as e:
    logging.error(f"檔案錯誤:{e}")
    print(f"❌ 檔案錯誤:{e}")
except ValueError as e:
    logging.error(f"資料錯誤:{e}")
    print(f"❌ 資料錯誤:{e}")
except Exception as e:
    logging.error(f"未預期錯誤:{e}", exc_info=True)
    print(f"❌ 發生錯誤:{e}")

2.2 讓作品更通用

  • 把檔名、欄位名抽成「設定變數」放在最上面,方便改。
  • 加上時間戳記輸出檔名(避免覆蓋):
from datetime import datetime
stamp = datetime.now().strftime("%Y%m%d")
summary.to_excel(f"彙總報表_{stamp}.xlsx", index=False)
  • 加上命令列參數(讓別人能指定輸入檔):
import sys

if len(sys.argv) > 1:
    INPUT = sys.argv[1]
else:
    INPUT = "daily_data.xlsx"

# 使用方式:python report.py custom_data.xlsx

2.3 用 logging 取代 print

為什麼要用 logging 而不是 print?

比較 print logging
輸出到哪 只顯示在畫面 可同時寫入檔案
嚴重等級 沒有 DEBUG/INFO/WARNING/ERROR/CRITICAL
時間戳記 要自己加 自動記錄
排程執行時 看不到 事後可查 log 檔
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler()  # 同時印在畫面
    ]
)

logging.info("開始處理報表")
logging.warning("找到 3 筆缺值資料")
logging.error("無法連線到資料庫")

2.4 寫 README(作品的門面)

建一個 README.md

# 每日報表彙總工具

## 功能
自動讀取每日資料,依部門彙總數量與金額,輸出 Excel 報表。

## 解決的問題
原本每天手動整理約需 30 分鐘且易出錯,現在執行幾秒完成。

## 使用方式
1. 把資料放成 daily_data.xlsx
2. 執行 `python report.py`
3. 產生 `彙總報表_YYYYMMDD.xlsx`

### 指定輸入檔

python report.py custom_data.xlsx


## 功能特色
- ✅ 自動錯誤處理(檔案不存在、欄位缺少)
- ✅ 缺值自動填補
- ✅ 輸出檔名含時間戳記,不覆蓋舊檔
- ✅ 執行日誌記錄(方便排程後查問題)
- ✅ 可搭配 Windows 工作排程器定時自動執行

## 使用技術
- Python 3.x
- pandas, openpyxl

## 環境建置

pip install -r requirements.txt


## 專案結構

├── report.py # 主程式 ├── daily_data.xlsx # 輸入資料(範例) ├── requirements.txt # 套件清單 ├── README.md # 本說明文件 └── logs/ # 日誌資料夾

2.5 Windows 工作排程器完整設定步驟

步驟一:建立批次檔(建議)

先建一個 run_report.bat,比直接設定排程更好管理:

@echo off
cd /d C:\Users\你\Documents\report_project
C:\Python39\python.exe report.py
if %errorlevel% neq 0 (
    echo [ERROR] 腳本執行失敗 >> error.log
)
pause

步驟二:設定工作排程器

1. Win+R → taskschd.msc → Enter
2. 右側「建立基本工作」
3. 名稱:每日報表彙總
4. 描述:每天 08:00 自動產生部門彙總報表
5. 觸發程序:每天 → 08:00:00
6. 動作:啟動程式
7. 程式或指令碼:C:\Users\你\Documents\report_project\run_report.bat
8. 勾選「完成時開啟此工作的內容」→ 完成

步驟三:進階設定

在「內容」視窗中: - 一般 索引標籤:勾選「不論使用者是否登入都要執行」(需輸入密碼) - 設定 索引標籤: - 勾選「若工作失敗,重新啟動間隔」→ 1 分鐘,重試 3 次 - 勾選「若工作執行超過以下時間就停止」→ 1 小時 - 勾選「若排程的開始時間已過,立即執行工作」

2.6 完整作品範例:彙總 + 寄信 + 日誌

"""
每日報表彙總自動化工具
功能:讀取資料 → 彙總 → 輸出 Excel → 自動寄信通知
"""

import pandas as pd
import os
import logging
import smtplib
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders

# ==== 設定區 ====
INPUT_FILE = "daily_data.xlsx"
GROUP_COL = "部門"
SMTP_SERVER = "smtp.company.com"
SMTP_PORT = 587
SENDER = "it@company.com"
SENDER_PWD = "password"
RECEIVERS = ["manager@company.com"]

# ==== 日誌 ====
stamp = datetime.now().strftime("%Y%m%d")
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(f"log_{stamp}.txt", encoding="utf-8"),
        logging.StreamHandler()
    ]
)

def generate_report():
    """產生彙總報表,回傳輸出檔名"""
    if not os.path.exists(INPUT_FILE):
        raise FileNotFoundError(f"找不到:{INPUT_FILE}")

    df = pd.read_excel(INPUT_FILE)
    logging.info(f"讀取 {len(df)} 筆資料")

    summary = df.groupby(GROUP_COL).agg(
        總數量=("數量", "sum"),
        總金額=("金額", "sum"),
    ).reset_index()

    output = f"彙總報表_{stamp}.xlsx"
    summary.to_excel(output, index=False)
    logging.info(f"報表產生:{output}")
    return output, summary

def send_email(report_path, summary):
    """寄送報表通知信"""
    msg = MIMEMultipart()
    msg["From"] = SENDER
    msg["To"] = ", ".join(RECEIVERS)
    msg["Subject"] = f"每日彙總報表 — {stamp}"

    body = f"主管您好,\n\n今日報表已產生,共 {len(summary)} 個部門。\n請參閱附件。\n\nIT"
    msg.attach(MIMEText(body, "plain", "utf-8"))

    with open(report_path, "rb") as f:
        part = MIMEBase("application", "octet-stream")
        part.set_payload(f.read())
        encoders.encode_base64(part)
        part.add_header("Content-Disposition", f"attachment; filename={report_path}")
        msg.attach(part)

    with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
        server.starttls()
        server.login(SENDER, SENDER_PWD)
        server.send_message(msg)
    logging.info("信件寄出完成")

# ==== 主流程 ====
if __name__ == "__main__":
    try:
        report_path, summary = generate_report()
        send_email(report_path, summary)
        print("✅ 報表產生並寄出完成!")
    except Exception as e:
        logging.error(f"執行失敗:{e}", exc_info=True)
        print(f"❌ 錯誤:{e}")

三、今日實作任務(產出)

  • [ ] 為作品加上錯誤處理
  • [ ] 抽出設定變數、加時間戳記
  • [ ] 加入 logging 日誌記錄
  • [ ] 寫好 README(含「解決的問題 + 成果」)
  • [ ] (加分)用「Windows 工作排程器」定時自動執行
  • [ ] (加分)加入自動寄信功能

完成後,這個作品 + README 就是你面試時可以打開來講的真實作品


四、今日練習

Q1. 為什麼自動化腳本應該用 logging 而不是 print?

Q2. README 至少應該包含哪些內容?

Q3. 在 Windows 工作排程器中,如果腳本排程後沒有正常執行,可能的原因有哪些?

參考解答 **A1.** 因為用排程自動執行時,畫面上沒有人看 print 輸出。logging 可以: 1. 把訊息寫入檔案,事後可查 2. 區分嚴重等級(INFO/WARNING/ERROR) 3. 自動加上時間戳記 4. 可以同時輸出到畫面和檔案 **A2.** README 至少包含: 1. **功能說明**:這個工具做什麼 2. **解決的問題**:原本痛點是什麼(最好有量化) 3. **使用方式**:怎麼執行 4. **環境需求**:需要安裝什麼套件 5. **專案結構**:有哪些檔案、各自用途 **A3.** 常見原因: 1. Python 路徑沒有填**完整路徑**(只填 `python` 會找不到) 2. 「開始位置」沒有設定,導致相對路徑的檔案找不到 3. 電腦處於關機/休眠/睡眠狀態 4. 排程工作被停用了(要確認「啟用」有勾選) 5. Python 環境缺少套件(排程用的環境不一定跟你平常用的一樣) 6. 權限不足(需要「以最高權限執行」)

五、常見誤解

誤解 正確觀念
「加錯誤處理太麻煩,程式能跑就好」 沒有錯誤處理的腳本在排程執行時會「靜默失敗」,出問題完全不知道
「README 可有可無」 README 是作品的門面,面試官第一眼看的就是 README;也是讓未來的自己看懂程式的關鍵
「排程設定好就不用管了」 需要定期檢查日誌,確認排程有正常執行;資料格式變了也要更新腳本

六、今日檢核

  • [ ] 我的作品有錯誤處理、不會一壞就崩
  • [ ] 我寫好了 README
  • [ ] 我能用 STAR 講這個作品
  • [ ] 我了解 logging 的用法
  • [ ] 我知道如何設定 Windows 工作排程器

七、延伸閱讀


⬅️ 上一天:Day 4 | 🏠 本週總覽 | ➡️ 下一天:Day 6 — 教育訓練與知識管理