↓快轉到主要內容
  1. 教學文章/

不想再複製貼上:用 Python 合併多個 Excel 表格

·8 分鐘· loading · loading · ·
Python Openpyxl Excel 辦公室自動化
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 157: 本文

一. 前言:三份表格,真的不用貼三次
#

每個人各交一份 Excel,最後要交一張總表。 你打開第一份、複製資料,打開第二份、跳過標題,再貼到最下面。 貼到第三份時,開始懷疑剛剛到底有沒有漏一列。

拍拍君今天想把這件小事交給 Python。 不做資料庫、不架網站,也不先學一大套資料分析框架。 我們只做一件事:把多份欄位相同的 .xlsx,從上到下接起來。

做完會拿到 combined.xlsx:標題只出現一次,每列都有來源檔名, 還附上篩選按鈕與固定標題列,打開就能繼續用。

下載完整練習包,解壓縮到一個新的資料夾, 裡面有程式、兩份示範 Excel 和預期總表;全部都是虛構資料。 也可以跟著下面從零建立,不必上傳任何工作文件。

這裡的「合併」是往下增加資料列,不是依姓名把兩張表左右配對, 也不是把相同品項加總。如果你要的是分組統計,請看 Pandas GroupBy 與報表整理。

二. 安裝:只需要 openpyxl
#

openpyxl 可以直接建立和讀寫 .xlsx,不需要先安裝 Microsoft Excel。 本文用 Python 3.12、openpyxl 3.1.5 實際執行過。

先開一個新的終端機,在練習資料夾裡安裝:

python -m venv .venv

macOS / Linux 啟用環境:

source .venv/bin/activate

Windows PowerShell 則用:

.\.venv\Scripts\Activate.ps1

接著安裝套件:

python -m pip install openpyxl==3.1.5

如果電腦只有 python3 指令,建立環境時把 python 換成 python3。 啟用後再執行後面的 python 即可。 已經習慣 uv 的讀者也能用它管理環境,參考 uv 教學。

三. 先準備兩份小表格
#

新建 make_demo.py,貼上這段程式:

from pathlib import Path
from openpyxl import Workbook

folder = Path("input")
folder.mkdir(exist_ok=True)
header = ["日期", "登記人", "品項", "數量"]
samples = {
    "01-monday.xlsx": [
        ["2026-11-02", "拍拍君", "筆記本", 2],
        ["2026-11-02", "拍拍醬", "便利貼", 5],
    ],
    "02-tuesday.xlsx": [
        ["2026-11-03", "chatPTT", "筆記本", 3],
        ["2026-11-03", "拍拍君", "資料夾", 1],
    ],
}
for name, rows in samples.items():
    wb = Workbook()
    ws = wb.active
    ws.title = "登記表"
    ws.append(header)
    for row in rows:
        ws.append(row)
    wb.save(folder / name)
print("已建立兩份練習表格")

執行一次:

python make_demo.py

資料夾會變成這樣:

excel-demo/
├── make_demo.py
└── input/
    ├── 01-monday.xlsx
    └── 02-tuesday.xlsx

兩份檔案都有「登記表」工作表,第一列都是日期、登記人、品項、數量。 日期先存成好辨識的文字,數量則是真正的數字。 這樣練習時不用先處理 Excel 日期顯示格式。

make_demo.py 只拿來產生練習檔。 重跑會覆寫那兩個同名檔案, 所以不要把它放進真實工作資料夾執行。

四. 第一份成果:把資料接成一張總表
#

接著建立 merge_excel.py。 這是完整程式,先照貼、跑出結果,再回頭看它做了什麼:

from pathlib import Path
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill

INPUT = Path("input")
OUTPUT = Path("combined.xlsx")
SHEET = "登記表"
HEADER = ("日期", "登記人", "品項", "數量")

files = sorted(
    p for p in INPUT.glob("*.xlsx")
    if not p.name.startswith("~$")
)
if not files:
    raise SystemExit("input 資料夾裡沒有 .xlsx 檔案")

result = Workbook()
merged = result.active
merged.title = "合併總表"
merged.append([*HEADER, "來源檔名"])
total = 0

for path in files:
    source = load_workbook(path, read_only=True, data_only=False)
    try:
        if SHEET not in source.sheetnames:
            raise ValueError(f"{path.name} 找不到工作表:{SHEET}")
        ws = source[SHEET]
        rows = ws.iter_rows()
        first = next(rows, ())
        if tuple(cell.value for cell in first) != HEADER:
            raise ValueError(f"{path.name} 的第一列欄位不一致")
        count = 0
        for cells in rows:
            values = tuple(cell.value for cell in cells)
            if all(value is None for value in values):
                continue
            if any(cell.data_type == "f" for cell in cells):
                raise ValueError(f"{path.name} 含有公式,請先轉成值")
            merged.append([*values, path.name])
            count += 1
        total += count
        print(f"{path.name}:加入 {count} 筆")
    finally:
        source.close()

for cell in merged[1]:
    cell.font = Font(bold=True, color="FFFFFFFF")
    cell.fill = PatternFill("solid", fgColor="FF376B59")
for column, width in {"A": 16, "B": 16, "C": 20, "D": 12, "E": 28}.items():
    merged.column_dimensions[column].width = width
merged.freeze_panes = "A2"
merged.auto_filter.ref = merged.dimensions
result.save(OUTPUT)
print(f"完成:{OUTPUT},共 {total} 筆資料")

在與 input 同一層的資料夾執行:

python merge_excel.py

你應該看到:

01-monday.xlsx:加入 2 筆
02-tuesday.xlsx:加入 2 筆
完成:combined.xlsx,共 4 筆資料

打開 combined.xlsx,內容會是:

日期 登記人 品項 數量 來源檔名
2026-11-02 拍拍君 筆記本 2 01-monday.xlsx
2026-11-02 拍拍醬 便利貼 5 01-monday.xlsx
2026-11-03 chatPTT 筆記本 3 02-tuesday.xlsx
2026-11-03 拍拍君 資料夾 1 02-tuesday.xlsx

四筆資料,加上一列標題,就是五列。 原本兩份 Excel 不會被儲存或修改;結果另存到 input 外面。 重跑時會覆寫總表,而不是在舊總表後面繼續加資料。 若想保留前一次結果,先把舊總表另存新名字。

五. 看懂合併的四個動作
#

1. 找檔案,再決定順序
#

INPUT.glob("*.xlsx") 找出 input 裡符合副檔名的檔案, 不會繼續往子資料夾搜尋。本文請統一使用小寫 .xlsx。 sorted() 則讓檔案依名稱排序,不依檔案總管當下的排列方式。

所以範例刻意叫 01-monday.xlsx、02-tuesday.xlsx。 如果有很多份,請用 01、02、10 這種補零編號, 不要期待文字排序自動把 2.xlsx 放在 10.xlsx 前面。

~$ 開頭通常是 Excel 產生的暫存鎖定檔,這裡不把它當資料來源。 輸出檔又放在外層,下一次自然不會把總表也合併進自己。 更多路徑操作可參考 pathlib 教學。

2. 選對工作表,不猜目前開在哪一頁
#

來源檔用 source[SHEET] 明確選取「登記表」, 而不是讀 source.active,避免不同檔案的作用中頁籤剛好不同。 新建總表才用 result.active,因為此時只有一個預設工作表。

read_only=True 讓來源以唯讀模式讀取,結束後用 close() 關閉。 總表仍然是在記憶體建立;這不表示整支程式可以無限量處理大檔案。

3. 第一列檢查,後面逐列追加
#

iter_rows() 依序取出儲存格;cell.value 才是格子裡的內容。 next(rows, ()) 先拿走第一列,所以後面的迴圈不會再貼一次標題。 總表的標題已經在迴圈外用 append() 寫好了。

四個欄位必須連順序都一致。 這比「看起來都是四欄,所以直接貼」多一點確認, 能避免把數量放進品項欄卻還以為成功。

None 表示空格;整列都是空格才會略過。 數量 0 不會被當成空白,部分欄位空白的資料也會保留。 這裡不會自動刪重、修正錯字,或刪掉看起來不重要的列。

4. 多加一欄,日後才知道從哪裡來
#

[*values, path.name] 是把一列原始內容展開,再放上來源檔名。 這不是額外裝飾:總表有一筆數量怪怪的, 你至少知道該回頭打開哪份檔案,不必逐份搜尋。

六. 簡單排版,讓總表打開就能用
#

完整程式最後幾行做了三件小事。 標題換成深綠底白字、欄寬調整到容易閱讀, 再加上凍結列與篩選範圍。

freeze_panes = "A2" 表示捲動時保留第一列,並不是凍結第二列。 auto_filter.ref = merged.dimensions 則把篩選範圍設成整塊總表; 它提供 Excel 篩選控制,不會在 Python 階段幫你隱藏某些資料。

這份程式只搬內容,不複製原本的字體、底色、欄寬或圖表。 我們是在新總表重新套一套簡單排版,不是在複製整本活頁簿。 像「數字 123 顯示為 00123」這種格式也不會跟著搬; 編號若必須保留前面的零,請先在來源存成文字。

七. 換成自己的檔案:先改這三個地方
#

確認練習成功後,另開一個工作資料夾, 複製 merge_excel.py 過去,把真實檔案的副本放進 input。 不要再跑產生範例資料的 make_demo.py。

接著改程式最上方:

  • SHEET:改成每份來源都有的工作表名稱。
  • HEADER:改成第一列實際欄名,包含順序與空白。
  • OUTPUT:改成你想要的總表檔名,仍留在 input 外面。

如果新增第五個來源欄位,還要把排版的欄寬設定往後調, 讓新增的「來源檔名」欄有足夠寬度。 先拿兩份小檔試跑,核對筆數與開頭、結尾幾筆,再放入其餘檔案。

這個簡單版本假設第一列就是標題,沒有合併儲存格、說明列或小計列。 若來源最上方有大標題,請先整理成單一標題列的表格再使用。 也不要把來源自己的「總計」列混進來,因為程式會把它視為一般資料。

八. 公式和檔案格式,先講清楚
#

openpyxl 不會幫你計算 Excel 公式。 所以本文用 data_only=False 讀取,發現公式儲存格就停下來, 請你先在副本中「複製 → 貼上為值」,確認數字正確後再合併。

你也許看過 data_only=True:那是讀取檔案先前儲存的公式結果, 不是當場重算;結果可能過期,沒有快取時還可能拿到 None。 不要只換個參數,就以為總表裡的數字一定是最新答案。

這篇只處理普通 .xlsx,不是舊式 .xls 或含巨集的 .xlsm。 不保留巨集、圖片、合併儲存格、超連結等完整文件功能, 也不是修復損壞 Excel 的工具。特別大的資料,先拆批處理更實際。 詳細讀取選項可以查 openpyxl 官方教學。

跑不動時,先看最常見的三件事
#

找不到檔案:確認終端機所在位置,input 必須在目前資料夾下。 找不到工作表:檢查頁籤名稱,不是檔案名稱。 欄位不一致:打開來源第一列,檢查順序、空白與額外有格式的空欄。 程式不會自己猜測欄位對應,請先整理來源再重跑。

如果儲存時出現 PermissionError,先關閉正在 Excel 裡開啟的總表, 也檢查資料夾是否可寫,再執行一次。 來源讀取或檢查失敗時不會進行最後的儲存;舊總表若存在,仍是舊結果, 不要看到資料夾裡有檔案就把失敗那次當成完成。

結語:把複製貼上的時間拿回來
#

這個小工具沒有替你做複雜分析,但它做完了一件很常見的工作: 多份同格式表格進來,一份有來源可查、標題清楚的總表出去。

拍拍君建議先把練習包跑通,再換兩份自己的副本。 當你確定欄位、筆數和順序都對了, 下一次收到更多份表格,就不用再逐一開檔貼到手痠啦。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 157: 本文

相關文章

Python subprocess 超時治理:POSIX Process Group、升級終止與輸出收尾
·7 分鐘· loading · loading
Python Subprocess POSIX Process Groups Timeout Testing
Python SQLite 線上備份:WAL、Connection.backup 與還原驗收
·7 分鐘· loading · loading
Python SQLite WAL Backup Restore Testing
Polars Pivot / Unpivot:長寬表轉換、重複鍵與 Round-trip 契約
·7 分鐘· loading · loading
Python Polars Pivot Unpivot Data-Contracts Testing
Rich 報表匯出:Record Buffer、HTML/SVG 與可重現分享
·8 分鐘· loading · loading
Python Rich HTML SVG Reports Testing
Streamlit Form 提交生命週期:草稿、已套用狀態與失敗保留
·9 分鐘· loading · loading
Python Streamlit Forms Session State Validation Testing
Plotly 時間軸實戰:缺測斷線、日期排序與 Rangebreaks
·7 分鐘· loading · loading
Python Plotly Time-Series Missing-Data Rangebreaks Data-Visualization