📊零基礎也能做!把多個 Excel 自動合併並打包成 Windows .exe:完整圖解、CMD/PowerShell 差異、.bat 一鍵執行與常見錯誤排除

 

內容(Content)

為什麼寫這篇?

在公司或專案中,我們常會收到一堆散落各處的 Excel 檔案:每位同事一份、每台產線一份、每天一份……手動合併既耗時又容易出錯。本文用非工程師也能看懂的方式,帶你做一個「一鍵合併 Excel」的小工具,最後再把它打包成 Windows 可執行檔(.exe),同事就算沒裝 Python 也能用。

你將學到:

  • 怎麼把多個 Excel 自動合併成一本,每個來源檔一張工作表

  • CMD vs PowerShell 的輸入差異(為什麼同一串指令,有時候會報錯?)

  • 怎麼把 Python 腳本打包成 .exe(PyInstaller)

  • .bat 一鍵執行,雙擊就跑

  • 常見錯誤與快速排除

注意:本文的所有範例碼都是全新撰寫,與你先前使用的版本不同,避免任何隱私或授權疑慮。


成品長這樣(快速總覽)

  • 放檔案到 in/ 資料夾 → 按一下 run.bat → 在 out/merged.xlsx 得到彙整檔

  • 每個來源 Excel 會變成輸出活頁簿中的不同工作表

  • 支援副檔名:.xlsx/.xlsm/.xls/.xlsb

  • 支援過濾(只收 *.xlsx 等)

  • 可將日誌寫到檔案,方便追查


1. 準備環境(一次搞定)

有網路的情況下,用最新版 Python 3.12 與 PyInstaller 會最省事。

:: 建議在新資料夾中執行 py -3.12 -m venv .venv .\.venv\Scripts\activate python -m pip install -U pip python -m pip install pandas openpyxl xlrd pyxlsb pyinstaller

2. 合併 Excel 的 Python 腳本(全新版本)

檔名:excel_collector.py(你可以自取)


這支會把 in/ 內的 Excel 檔合併到 out/merged.xlsx,每個來源檔成為一張工作表;未指定參數時,預設以程式所在位置為基準。

#!/usr/bin/env python3 """ ExcelCollector:把多個 Excel 合併成一本,每個來源檔一張工作表。 (全新示例碼,避免與任何既有版本重複) """ from __future__ import annotations import argparse, sys, re, logging from pathlib import Path from datetime import datetime import pandas as pd # 不合法的工作表字元 _ILLEGAL = r'[:\\/?*\[\]]' # 各副檔名對應的 pandas 引擎 _ENGINES = {'.xlsx': 'openpyxl', '.xlsm': 'openpyxl', '.xls': 'xlrd', '.xlsb': 'pyxlsb'} def _prog_dir() -> Path: """回傳程式所在目錄(打包後為 exe 所在目錄)""" if getattr(sys, 'frozen', False): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parent def _clean_sheet(name: str, used: set[str]) -> str: """清理 sheet 名稱,限制 31 字且去重""" s = re.sub(_ILLEGAL, '_', name).strip() or 'Sheet' s = s[:31] base, n = s, 1 while s in used: suffix = f'_{n}' s = (base[:31 - len(suffix)]) + suffix n += 1 used.add(s) return s def _list_sources(root: Path, pattern: str | None) -> list[Path]: exts = {'.xlsx', '.xlsm', '.xls', '.xlsb'} if pattern: files = sorted([p for p in root.glob(pattern) if not p.name.startswith('~$')]) else: items = [] for ext in exts: items.extend(root.glob(f'*{ext}')) files = sorted([p for p in items if not p.name.startswith('~$')]) return files def _engine_for(p: Path) -> str | None: return _ENGINES.get(p.suffix.lower()) def _parse(argv): ap = argparse.ArgumentParser(description='ExcelCollector:合併多個 Excel 到單一本 .xlsx') ap.add_argument('--input', help='來源資料夾(預設:程式同層的 ./in)') ap.add_argument('--output', help='輸出活頁簿路徑 .xlsx(預設:./out/merged.xlsx)') ap.add_argument('--pattern', help='檔名過濾,例如:*.xlsx 或 *2025*.xlsb') ap.add_argument('--sheet', help='要讀的工作表名稱;未提供則讀第一張') ap.add_argument('--logfile', help='日誌檔路徑;未提供則輸出到主控台') return ap.parse_args(argv) def _run(args) -> int: base = _prog_dir() src = (Path(args.input) if args.input else base / 'in').resolve() dst = (Path(args.output) if args.output else base / 'out' / 'merged.xlsx').resolve() src.mkdir(parents=True, exist_ok=True) dst.parent.mkdir(parents=True, exist_ok=True) logging.info('Source folder: %s', src) files = _list_sources(src, args.pattern) if not files: logging.warning('未找到來源檔,請把 Excel 放到:%s', src) return 0 used = set() with pd.ExcelWriter(dst, engine='openpyxl', mode='w') as writer: meta = pd.DataFrame([{ 'MergedAt': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'SourceDir': str(src), 'FileCount': len(files), }]) meta.to_excel(writer, index=False, sheet_name=_clean_sheet('INFO', used)) for f in files: eng = _engine_for(f) if eng is None: logging.info('跳過不支援的副檔名:%s', f.name) continue try: target_sheet = args.sheet if args.sheet else 0 df = pd.read_excel(f, sheet_name=target_sheet, engine=eng) df.to_excel(writer, index=False, sheet_name=_clean_sheet(f.stem, used)) logging.info('合併完成:%s(%d 列)', f.name, len(df)) except Exception as e: logging.exception('讀取失敗 %s:%s', f.name, e) logging.info('輸出完成:%s', dst) return 0 def main(argv=None): args = _parse(argv or sys.argv[1:]) handlers = [logging.StreamHandler()] if args.logfile: Path(args.logfile).parent.mkdir(parents=True, exist_ok=True) handlers = [logging.FileHandler(args.logfile, encoding='utf-8')] logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)s | %(message)s', handlers=handlers) return _run(args) if __name__ == '__main__': raise SystemExit(main())

怎麼用?

# 看說明 python excel_collector.py --help # 最簡單:用預設的 ./in -> ./out/merged.xlsx python excel_collector.py # 只收 .xlsx,並指定工作表名稱 python excel_collector.py --pattern "*.xlsx" --sheet "報表"

3. 打包成 .exe(PyInstaller)

讓同事不裝 Python 也能用


:: 仍在虛擬環境中 (.venv): pyinstaller -F -n ExcelCollector ^ --hidden-import openpyxl ^ --hidden-import xlrd ^ --hidden-import pyxlsb ^ excel_collector.py
  • 成品在:dist/ExcelCollector.exe

  • 測試:

    .\dist\ExcelCollector.exe --help .\dist\ExcelCollector.exe --pattern "*.xls*" --sheet "報表"
為什麼加 --hidden-import

因為 pandas 會動態載入讀檔引擎,打包時容易漏掉,手動指定最穩妥。



4. CMD 與 PowerShell 的差異(超多人在這裡卡關)

  • 執行目前資料夾的程式

    • CMD:ExcelCollector.exe.\ExcelCollector.exe

    • PowerShell:一定要 .\ExcelCollector.exe

  • 換行符號

    • CMD:用 ^ 續行

    • PowerShell:用 反引號「`」續行,或乾脆打一行

  • 有疑問先試:

    .\ExcelCollector.exe --help

5. 一鍵執行的 .bat(全新寫法)

檔名:run.bat,放在 ExcelCollector.exe 同資料夾


@echo off setlocal EnableExtensions REM 以 bat 所在資料夾為基準(支援中文/空白路徑) set "ROOT=%~dp0" set "BIN=%ROOT%ExcelCollector.exe" if not exist "%BIN%" ( echo [ERROR] 找不到執行檔:%BIN% pause & exit /b 1 ) REM 預設路徑 set "SRC=%ROOT%in" set "DST=%ROOT%out\merged.xlsx" set "LOG=%ROOT%logs\run.log" REM 確保父資料夾存在 for %%F in ("%DST%") do if not exist "%%~dpF" mkdir "%%~dpF" 2>nul for %%F in ("%LOG%") do if not exist "%%~dpF" mkdir "%%~dpF" 2>nul if not exist "%SRC%" mkdir "%SRC%" 2>nul REM 執行(想改條件,這裡改 pattern、sheet 等) "%BIN%" --input "%SRC%" --output "%DST%" --pattern "*.xls*" --logfile "%LOG%" set "RC=%ERRORLEVEL%" if not "%RC%"=="0" echo [ERROR] 程式回傳碼:%RC% pause exit /b %RC%

用法:把 Excel 放到 in/,雙擊 run.bat,結果會出現在 out/merged.xlsx



6. 也可以用 PowerShell 啟動(可選)

檔名:run.ps1


$exe = Join-Path $PSScriptRoot 'ExcelCollector.exe' $src = Join-Path $PSScriptRoot 'in' $dst = Join-Path $PSScriptRoot 'out\merged.xlsx' $log = Join-Path $PSScriptRoot 'logs\run.log' New-Item -ItemType Directory -Force -Path (Split-Path $dst) | Out-Null New-Item -ItemType Directory -Force -Path (Split-Path $log) | Out-Null New-Item -ItemType Directory -Force -Path $src | Out-Null & $exe --input $src --output $dst --pattern "*.xls*" --logfile $log
若遇到「執行原則」限制,可在該視窗暫時放行:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass



7. 常見錯誤對照表

症狀/訊息 可能原因 快速解法
... is not recognized as an internal or external command(找不到 .exe) 未加 .\(PowerShell)或不在該資料夾 cd 到 .exe 所在資料夾,PowerShell 用 .\ExcelCollector.exe
the following arguments are required 少帶必要參數 先跑 --help 看有哪些參數;或用 run.bat
.xls 失敗 沒有 xlrd 或版本不符 python -m pip install xlrd,或把檔案另存為 .xlsx
.xlsb 失敗 沒有 pyxlsb python -m pip install pyxlsb
中文/空白路徑出錯 沒加引號 路徑一律加雙引號 "..."
合併後工作表名稱重複/非法 Excel 限制(31字/非法字元) 程式已自動處理:替換非法字元、超長截斷、撞名加尾碼 _1/_2/...
執行檔被防毒誤判 未簽章的單檔 .exe 改用資料夾模式(不加 -F),或對 .exe 進行簽章(正式發佈建議)

8. SEO 重點關鍵字(內容已自然置入)

  • Excel 合併、自動化合併 Excel、Windows .exe、PyInstaller、CMD 與 PowerShell 差異、批次檔 .bat、新手也能做、企業內部工具、自動資料整合、pandas openpyxl xlrd pyxlsb


結語

把重複又枯燥的工作自動化,是工程師能帶來的直接價值。這篇以最少門檻的方式,從「合併 Excel 腳本」一路走到「打包 .exe」「.bat 一鍵執行」,同事端也能輕鬆使用。你可以在此基礎上再擴充:像是把所有資料合併到同一張表、加入檔名/日期欄、甚至加上 GUI 介面。若你想把這個工具做成公司內部的標準流程,我也可以把腳本改成無參數即自動使用預設路徑+互動式問答的版本,並附上更完整的錯誤訊息與日誌旋轉機制。

留言

這個網誌中的熱門文章

🔍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 錯誤與資料欄位動態插入顯示問題