MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

用 openpyxl 複製 Worksheet 後 Excel 開起來兩個 Sheet 同時被選取?來看看這個冷門坑

最近在寫一個自動產出 Excel 報表的腳本,用到 openpyxlcopy_worksheet() 來複製上個月的 sheet 當模板,改一改數字就輸出新的當月 sheet。

功能是跑通了,但打開 Excel 之後發現一件很奇怪的事:

兩個 sheet tab 同時被選取,都是反白的狀態。

不是只有當月,連上個月的 sheet 也被選起來,看起來像使用者按著 Ctrl 手動多選了兩個 sheet。

一開始以為是我的 wb.active 設錯了,但改來改去都沒用。後來才發現這個問題根本不在 active,而在另一個我從來沒注意過的屬性。


問題:copy_worksheet() 會把 tabSelected 一起複製過來

openpyxlWorksheet 物件有一個屬性叫 sheet_view.tabSelected,這個值控制的是「這個 sheet 在 Excel 介面上有沒有被選取(反白)」。

當你對一個已被選取的 sheet 呼叫 copy_worksheet(),新複製出來的 sheet 也會繼承 tabSelected=True

更慘的是,舊的 sheet 的 tabSelected 也還是 True

所以兩個都是 True,Excel 就忠實地幫你「選取」了兩個 sheet,就像你按著 Ctrl 點了它們一樣。


為什麼這個問題很難找

copy_worksheet() 本來就是設計來複製 sheet 的所有屬性,tabSelected 是 sheet view 的一部分,被複製過去是 by design 的行為,不算 bug。

但問題是「選取狀態」這個屬性在平常的 Excel 操作情境下根本不會在意——你手動新增 sheet 的時候,Excel 會自動幫你清掉其他 sheet 的選取狀態。可是透過程式操作的時候,沒有人幫你做這件事。

所以這是一個「只有在特定操作流程下才會出現、而且症狀看起來很怪的問題」,一時間不知道從哪下手很正常。


修法:在複製前清掉所有 sheet 的 tabSelected

解法其實很直觀,就是在做任何 copy 或 active 操作之前,把所有 worksheet 的 tabSelected 全部設成 False,然後再設定你要的 active sheet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import openpyxl

wb = openpyxl.load_workbook("template.xlsx")

# 複製上個月的 sheet 來用
source_ws = wb["2026-03"]
new_ws = wb.copy_worksheet(source_ws)
new_ws.title = "2026-04"

# 修法:先清空所有 sheet 的選取狀態
for ws in wb.worksheets:
ws.sheet_view.tabSelected = False

# 再設定新的 active sheet
wb.active = new_ws

wb.save("output.xlsx")

就這樣,打開 Excel 之後只有 2026-04 是反白的,其他 sheet 都正常。


順手搞定「預設開啟哪個 sheet」

說到這裡,順便講一個相關的東西:wb.active

wb.active 決定的是「用 Excel 打開這個檔案時,預設停在哪個 sheet」。如果你不設,openpyxl 會用 index 0,也就是第一個 sheet。

但要注意一件事:**wb.active = ws 接受的是 worksheet 物件,不是 sheet 名稱字串。**

1
2
3
4
5
6
7
8
9
# ✅ 正確
wb.active = wb["2026-04"]

# ✅ 也正確(先拿到 ws 物件再設)
new_ws = wb.copy_worksheet(source_ws)
wb.active = new_ws

# ❌ 錯誤(不接受字串,不會報錯但也不會有效果)
wb.active = "2026-04"

最後這個錯我自己就犯過,而且不會報錯所以很難發現,只是開啟的 sheet 沒有照預期——記一下。


完整範例:每月自動複製 sheet 並正確設定開啟狀態

把上面的概念整合起來,假設你有一個每月跑的腳本,要從模板複製當月的 sheet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import openpyxl
from datetime import date

def get_month_label(year: int, month: int) -> str:
return f"{year}-{month:02d}"

def add_monthly_sheet(filepath: str, year: int, month: int, template_title: str = "TEMPLATE") -> None:
wb = openpyxl.load_workbook(filepath)

new_title = get_month_label(year, month)

# 防重複:如果已經存在就跳過
if new_title in wb.sheetnames:
print(f"Sheet '{new_title}' 已存在,跳過")
return

# 從模板複製
template_ws = wb[template_title]
new_ws = wb.copy_worksheet(template_ws)
new_ws.title = new_title

# 清掉所有 tabSelected,然後設定新的 active sheet
for ws in wb.worksheets:
ws.sheet_view.tabSelected = False
wb.active = new_ws

wb.save(filepath)
print(f"已新增 sheet '{new_title}',設為預設開啟")

# 使用方式
today = date.today()
add_monthly_sheet("monthly_report.xlsx", today.year, today.month)

延伸:日期計算的小技巧

既然在聊自動化腳本,順便記一個我最近用到的 date utility。

很多定期付款或截止日的情境需要「找出下一個還沒到的 N 號」——比如每月 5 號是結算日,你今天是 4 月 1 號,那下一個 5 號是 4/5;但如果今天已經是 4 月 10 號,那下一個 5 號就是 5/5 了。

這個邏輯很直白但每次都要想一下,不如直接包成函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from datetime import date
from calendar import monthrange

def next_nth_day(n: int, ref: date = None) -> date:
"""
回傳「從 ref 日期起,下一個還沒到的 n 號」。
如果今天就是 n 號,回傳今天(視為「還沒到」)。
"""
if ref is None:
ref = date.today()

# 試試看本月的 n 號
try:
candidate = ref.replace(day=n)
except ValueError:
# 本月沒有 n 號(例如 2 月 30 號),改用月底
last_day = monthrange(ref.year, ref.month)[1]
candidate = ref.replace(day=last_day)

if candidate >= ref:
return candidate

# 本月的已過去,找下個月的
if ref.month == 12:
next_year, next_month = ref.year + 1, 1
else:
next_year, next_month = ref.year, ref.month + 1

try:
return date(next_year, next_month, n)
except ValueError:
last_day = monthrange(next_year, next_month)[1]
return date(next_year, next_month, last_day)


# 測試
from datetime import date
print(next_nth_day(5, date(2026, 4, 1))) # → 2026-04-05
print(next_nth_day(5, date(2026, 4, 5))) # → 2026-04-05(當天算「還沒到」)
print(next_nth_day(5, date(2026, 4, 10))) # → 2026-05-05

邏輯很簡單:先算本月的那天,如果已經過了就跳到下個月。邊界情況(2 月、月底天數不同)用 monthrange 處理。


小結

今天這兩個坑都是「不報錯、但行為不對」的類型,最麻煩。

  • copy_worksheet() 複製 tabSelected 狀態 → 複製後記得清掉所有 sheet 的 tabSelected
  • wb.active 只接受 worksheet 物件,不接受字串 → 設定前先把 ws 物件拿好
  • 日期計算「下一個 N 號」→ 包成 next_nth_day(n) 函式,一次寫好以後就不用再想了

openpyxl 整體而言是個很好用的套件,但它的 sheet view 相關屬性文件比較稀疏,很多細節要踩過才會知道。希望這篇能幫你少踩幾個坑。😂