台账类 Excel 的经典形态:一个工作簿里「1月」「2月」「3月」各一个 Sheet,列都一样;更狠的是几十个文件、每个里面还有同名 Sheet,老板要一张全年总表 + 按人汇总。手动复制粘贴 Sheet 又是体力刑具。下面这份脚本本次完整实跑过:3 个月度 Sheet 共 7 行数据(含故意埋的脏数据),合并 + 按人汇总全部正确,非法行跳过并打印原因,重跑幂等。
#!/usr/bin/env python3
"""多 Sheet 汇总:把一个工作簿里所有月份 sheet 的销售记录合并成总表,并按人汇总。"""
import sys, os
from collections import defaultdict
from openpyxl import load_workbook, Workbook
def norm(s):
return str(s).strip() if s is not None else ""
def main(paths, out):
all_rows = []
for p in paths:
wb = load_workbook(p, data_only=True)
for ws in wb.worksheets:
header = None; col = None
for row in ws.iter_rows(values_only=True):
cells = [norm(c) for c in row]
if not any(cells):
continue # 坑①:空行不跳过会把 " 姓名 " 当数据
if header is None:
header = [norm(h) for h in row] # 坑②:表头不 strip 归一化," 姓名 " 对不上 "姓名"
col = {h: i for i, h in enumerate(header)}
missing = {"姓名","日期","金额"} - set(header)
if missing:
print(f"[跳过sheet] {p}::{ws.title} 缺列{missing}", file=sys.stderr)
header = None; col = None
continue
d = cells[col["日期"]]
if hasattr(row[col["日期"]], "strftime"): # 坑④:真日期单元格读出 datetime 对象
d = row[col["日期"]].strftime("%Y-%m-%d")
try:
amount = float(cells[col["金额"]]) # 坑③:非数字金额会 ValueError
except (KeyError, ValueError):
print(f"[跳过行] {p}::{ws.title} {cells}", file=sys.stderr)
continue
all_rows.append({
"来源sheet": f"{os.path.basename(p)}::{ws.title}",
"姓名": cells[col["姓名"]],
"日期": d,
"金额": amount,
})
total, cnt = defaultdict(float), defaultdict(int)
for r in all_rows:
total[r["姓名"]] += r["金额"]; cnt[r["姓名"]] += 1
outwb = Workbook()
ws1 = outwb.active; ws1.title = "明细汇总"
ws1.append(["来源sheet","姓名","日期","金额"])
for r in all_rows: ws1.append([r["来源sheet"],r["姓名"],r["日期"],r["金额"]])
ws2 = outwb.create_sheet("按人汇总")
ws2.append(["姓名","单数","总金额"])
for name in sorted(total): ws2.append([name,cnt[name],round(total[name],2)])
outwb.save(out)
print(f"共合并 {len(all_rows)} 行,来自 {len(set(r['来源sheet'] for r in all_rows))} 个sheet")
if __name__ == "__main__":
main(sys.argv[1:-1], sys.argv[-1])
构造 3 个 Sheet(1月/2月 正常,3月 故意埋雷:表头写成 " 姓名 " 带空格、一行整行空白、一行金额填 "abc"):
共合并 6 行,来自 3 个sheet [跳过行] 销售台账.xlsx::3月 ['赵六', '2026-03-15', 'abc'] -- 明细汇总 销售台账.xlsx::1月, 张三, 2026-01-03, 15000.5 销售台账.xlsx::1月, 李四, 2026-01-05, 8200 销售台账.xlsx::1月, 张三, 2026-01-20, 3000 销售台账.xlsx::2月, 李四, 2026-02-01, 9900 销售台账.xlsx::2月, 王五, 2026-02-11, 4560.75 销售台账.xlsx::3月, 张三, 2026-03-02, 12000 -- 按人汇总 张三, 3单, 30000.5 李四, 2单, 18100 王五, 1单, 4560.75
两个文件(A公司/B公司,各有同名「1月」Sheet)也实跑通过,来源列能区分是哪个文件的哪张表。重跑输出完全一致,幂等。
" 姓名 "——前后空格看不见但真实存在。用裸字符串做字典键 {"姓名":0},col["姓名"] 直接 KeyError,而且报错在数据处理循环里,不在表头那行,第一眼根本想不到是表头脏。所有表头读进来先 strip() 归一化。iter_rows(values_only=True) 读出的是 datetime.datetime(2026,1,3,0,0)。str() 一转就变成 "2026-01-03 00:00:00",混进全是 "2026-01-05" 文本日期的列里两种格式并存,排序、筛选全乱。修法:hasattr(c,'strftime') 判断后统一 strftime("%Y-%m-%d")。"abc" 太常见了。float(c) 遇到直接 ValueError,整个脚本崩在最后一行数据上,前面白跑。必须 try/except 单行跳过 + 打印原因到 stderr,让用户知道哪几行被跳过、为什么。f"{os.path.basename(p)}::{ws.title}"。顺带一提:空行不跳过的话,表头判断会错位——「下一条非空行当表头」的逻辑会把数据行误认成第二张表的表头。pandas.concat(pd.read_excel(path, sheet_name=None)) 确实一行搞定合并,但:目标机器装 pandas + openpyxl 是一大坨依赖;datetime/表头空格/非法金额这几个坑 pandas 一样要手写清洗逻辑;而且「按人汇总」这种带业务口径的统计,写出来还是十几行。对「在别人电脑上跑」的场景(详见 脚本在别人电脑上报错怎么办),openpyxl 一个包就够了,坑也全在自己看得见的地方。