一. 前言:三份表格,真的不用貼三次 #
每個人各交一份 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 裡開啟的總表,
也檢查資料夾是否可寫,再執行一次。
來源讀取或檢查失敗時不會進行最後的儲存;舊總表若存在,仍是舊結果,
不要看到資料夾裡有檔案就把失敗那次當成完成。
結語:把複製貼上的時間拿回來 #
這個小工具沒有替你做複雜分析,但它做完了一件很常見的工作: 多份同格式表格進來,一份有來源可查、標題清楚的總表出去。
拍拍君建議先把練習包跑通,再換兩份自己的副本。 當你確定欄位、筆數和順序都對了, 下一次收到更多份表格,就不用再逐一開檔貼到手痠啦。