🧾懶人也能一次合併多個 Excel:從「雙擊就跳掉」到一鍵執行的完整指南(含程式碼範例)

 

為什麼你一雙擊 .py 視窗就「跳掉」?

很多人直接雙擊 Python 檔,結果視窗一閃而逝。常見原因是:
程式需要帶參數執行(例如「來源資料夾」「輸出檔名」),沒帶參數時程式就結束了。
解法很簡單:用命令列帶參數跑,或做一個 .bat 包裝,甚至改成互動式一鍵 .exe

下面我用「工程師講給完全不懂的人」的方式,帶你一步完成。文中的所有程式碼都是重新改寫過的範例,不含任何私人資訊。


你會學到什麼?

  • 如何用 Python 把「一堆 Excel」合併成「一個 Excel,分多個工作表」。

  • 3 種啟動方式:命令列、雙擊 .bat、雙擊 .py(互動式)。

  • 打包成 .exe,完全不需要使用者裝 Python。

  • 常見錯誤與排查清單。


先安裝需要的工具(一次搞定)

開啟「命令提示字元(CMD)」或 PowerShell,安裝下列套件:

pip install pandas openpyxl xlrd pyxlsb
說人話版:
  • pandas 是處理表格資料的超級工具。
  • openpyxl.xlsx / .xlsm
  • xlrd.xls
  • pyxlsb.xlsb


都裝起來就對了。



範例程式 1:命令列版本(專業又穩)

檔名建議:excel_gluer.py

功能:把資料夾裡的 Excel 檔,每個檔做成一張工作表,且自動避開重名、非法字元。


# excel_gluer.py (全新改寫的示例程式)
import argparse import logging from pathlib import Path import sys import re import shutil from datetime import datetime import pandas as pd SUPPORTED = {".xlsx", ".xls", ".xlsm", ".xlsb"} def safe_sheet(title: str, used: set) -> str: # Excel 禁用字元: []:*?/\ t = re.sub(r'[\[\]\:\*\?\/\\]', '_', (title or 'Sheet')).strip() t = t[:31] or "Sheet" base, i = t, 1 while t in used: sfx = f"_{i}" t = (base[:31 - len(sfx)]) + sfx i += 1 used.add(t) return t def reader_engine_by_suffix(suf: str) -> str | None: s = suf.lower() if s in (".xlsx", ".xlsm"): return "openpyxl" if s == ".xls": return "xlrd" if s == ".xlsb": return "pyxlsb" return None def list_sources(folder: Path, pattern: str | None): if pattern: paths = sorted(folder.glob(pattern)) else: paths = [] for ext in SUPPORTED: paths += folder.glob(f"*{ext}") paths = sorted(paths) # 排除暫存檔(~$ 開頭) return [p for p in paths if not p.name.startswith("~$") and p.is_file()] def parse_args(): ap = argparse.ArgumentParser(description="Merge multiple Excel files into one workbook (one file = one sheet).") ap.add_argument("--src", required=True, help="來源資料夾") ap.add_argument("--out", required=True, help="輸出檔(.xlsx)") ap.add_argument("--only-sheet", default="", help="只讀取來源檔指定工作表名稱(可留空)") ap.add_argument("--first-sheet", action="store_true", help="只讀第一張工作表(與 --only-sheet 二擇一)") ap.add_argument("--match", default="", help='只合併符合檔名樣式的檔案,例如 "*.xlsx"') ap.add_argument("--archive", default="", help="合併成功後,將來源檔移至此資料夾") ap.add_argument("--log", default="", help="輸出日誌到檔案;未指定則印在螢幕") return ap.parse_args() def main(): args = parse_args() src = Path(args.src).resolve() out = Path(args.out).resolve() arc = Path(args.archive).resolve() if args.archive else None handlers = [logging.FileHandler(args.log, encoding="utf-8")] if args.log else [logging.StreamHandler()] logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", handlers=handlers) if not src.exists(): logging.error(f"來源資料夾不存在:{src}") sys.exit(2) files = list_sources(src, args.match) if not files: logging.warning("找不到可合併的 Excel 檔。") return out.parent.mkdir(parents=True, exist_ok=True) used_names = set() merged_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with pd.ExcelWriter(out, engine="openpyxl", mode="w") as xw: # 寫入 INFO 頁 info = pd.DataFrame({"MergedAt": [merged_time], "SourceDir": [str(src)], "FileCount": [len(files)]}) info.to_excel(xw, index=False, sheet_name=safe_sheet("INFO", used_names)) # 逐檔寫入 for fp in files: eng = reader_engine_by_suffix(fp.suffix) if not eng: logging.info(f"略過不支援副檔名:{fp.name}") continue target_sheet = safe_sheet(fp.stem, used_names) try: if args.only_sheet: df = pd.read_excel(fp, sheet_name=args.only_sheet, engine=eng) elif args.first_sheet: df = pd.read_excel(fp, sheet_name=0, engine=eng) else: # 預設行為:第一張 df = pd.read_excel(fp, sheet_name=0, engine=eng) df.to_excel(xw, index=False, sheet_name=target_sheet) logging.info(f"寫入:{fp.name} -> {target_sheet}(rows={len(df)})") except Exception as e: logging.exception(f"處理失敗:{fp.name}{e}") logging.info(f"合併完成:{out.name}") if arc: arc.mkdir(parents=True, exist_ok=True) for fp in files: try: dest = arc / fp.name if dest.exists(): stamp = datetime.now().strftime("%Y%m%d_%H%M%S") dest = arc / f"{fp.stem}_{stamp}{fp.suffix}" shutil.move(str(fp), str(dest)) logging.info(f"已封存:{fp.name} -> {dest.name}") except Exception as e: logging.exception(f"封存失敗:{fp.name}{e}") if __name__ == "__main__": main()

如何執行(命令列)

python excel_gluer.py ^ --src "D:\excel\來源" ^ --out "D:\excel\彙整\all.xlsx" ^ --first-sheet ^ --archive "D:\excel\來源\archive" ^ --log "D:\excel\彙整\merge.log"

小提醒:--src--out 必填;--first-sheet--only-sheet 擇一(或都不填,預設讀第一張)。



範例程式 2:互動式版本(雙擊 .py 也能用)

你想直接雙擊 .py 就跑?可以把下面這段「互動式入口」加在程式底部,沒帶參數時自動詢問並執行(此段與上面主程式邏輯相容、獨立可用)。

# interactive_bootstrap.py 片段(加到 excel_gluer.py 最底部 main() 之後) import sys def ask_then_rerun(): print("未提供參數,進入互動式模式:") src = input("來源資料夾(--src):").strip('" ') outp = input("輸出檔(.xlsx,--out):").strip('" ') mode = input("只讀第一張?輸入 y/n(y 等同 --first-sheet):").lower().strip() only = "" if mode == "n": only = input("若要指定來源工作表名稱(可空白,--only-sheet):").strip() pat = input('檔名過濾(可空白,例如 *.xlsx,--match):').strip() arc = input("封存資料夾(可空白,--archive):").strip() logf = input("日誌檔路徑(可空白,--log):").strip() argv = [sys.argv[0], "--src", src, "--out", outp] if mode == "y": argv += ["--first-sheet"] if only: argv += ["--only-sheet", only] if pat: argv += ["--match", pat] if arc: argv += ["--archive", arc] if logf: argv += ["--log", logf] sys.argv = argv if __name__ == "__main__": try: if len(sys.argv) == 1: # 沒帶參數,多半是雙擊 ask_then_rerun() main() except SystemExit: input("\n參數有誤或缺少必要值。請按 Enter 關閉視窗...") except Exception as e: print(f"\n發生錯誤:{e}") input("按 Enter 關閉視窗...") else: if len(sys.argv) == 1: input("\n處理完成,按 Enter 關閉視窗...")

範例程式 3:一鍵 .bat 包裝(給不愛命令列的人)

檔名:run_glue.bat(雙擊即可)


@echo off chcp 65001 >nul setlocal REM === 依你環境修改以下參數 === set PY="C:\Path\to\python.exe" set SCRIPT="D:\scripts\excel_gluer.py" set SRC="D:\excel\來源" set OUT="D:\excel\彙整\all.xlsx" set ARC="D:\excel\來源\archive" set LOG="D:\excel\彙整\merge.log" REM ============================= %PY% %SCRIPT% --src %SRC% --out %OUT% --first-sheet --archive %ARC% --log %LOG% echo. echo 完成!按任意鍵關閉... pause >nul
  • chcp 65001:讓中文路徑/日誌不會亂碼。

  • pause:即使失敗也不會秒關,能看錯誤訊息。


想給使用者更省事?打包成 .exe

  1. 安裝打包工具:

    pip install pyinstaller
  2. 在程式所在資料夾執行:

    pyinstaller -F excel_gluer.py
    • dist/excel_gluer.exe 就會出現了。

    • 有互動式入口時,雙擊 .exe 也能跑;或你另外做 GUI 都行。


常見錯誤與排查清單

  1. 視窗一閃即關

    • 沒帶參數:用命令列或 .bat、或加互動式入口。

    • .bat 包住、結尾 pause,至少看得到錯誤。

  2. 檔案讀不到/格式不支援

    • 安裝對應引擎:

      • .xlsx / .xlsmopenpyxl

      • .xlsxlrd

      • .xlsbpyxlsb

  3. 工作表名稱無效或重複

    • Excel 工作表限制 31 字元、不能有 []:*?/\ 等字元。

    • 範例程式已自動替換、截斷、並避免重名。

  4. 檔案被占用

    • 確認 Excel 沒開著來源檔或輸出檔。

  5. 中文字亂碼

    • .bat 開頭加 chcp 65001

    • 日誌檔使用 UTF-8(程式已設定)。


進階需求怎麼做?

  • 只合併符合檔名規則的檔案:加 --match "*.xlsx"

  • 只要來源檔的某張表:加 --only-sheet "Summary"

  • 處理後自動把來源檔移到封存夾:加 --archive "D:\archive"

  • 保留執行紀錄:加 --log "D:\logs\merge.log"


範例情境:三種啟動方式一次看懂

  1. 命令列(專業派)

python excel_gluer.py --src "D:\reports" --out "D:\out\merged.xlsx" --first-sheet --log "D:\out\merge.log"
  1. 雙擊 .bat(懶人派)

  • 測一次成功後,天天雙擊就行。

  1. 雙擊 .py(互動式)

  • 使用者不懂命令列?互動式入口會逐步詢問路徑與選項。


總結

  • 問題本質:需要帶參數的 Python 腳本被「直接雙擊」,自然會跳掉。

  • 三條路:命令列正確執行、.bat 包裝、一鍵互動式。

  • 終極方案:打包成 .exe,發給同事即可用。

留言

這個網誌中的熱門文章

🔍Vue.js 專案錯誤排查:解決 numericFields is not defined 與合併儲存格邏輯最佳化

🔎EF Core 連 Oracle 出現 ORA-00600 [kpp_concatq:2] 的完整排錯指南(含 EF Core ToString/CultureInfo 錯誤)

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題