超简单:用 Python 让 Excel 飞起来:用 openpyxl 把重复报表整理交给脚本
1. 为什么我想写这一篇?做办公自动化这几年,我见过太多这样的场景:每天早上的固定流程,是把十几个业务群发来的 Excel 下载下来,手动复制、粘贴、对齐格式、核对数字,最后拼成一份日报。整件事耗时约 40 分钟,其中真正需要"思考"的环节可能只有 5 分钟,其余 35 分钟都是在做低价值的机械搬运。这类工作,恰恰是 Python 最擅长接管的部分。《超简单:用 Python 让 Excel 飞起来》这本书给我的最大启发,不是某个具体函数的用法,而是一套判断标准:凡是"有规律、可重复、需批量"的 Excel 操作,都值得停下来问一句——它是不是本来就该由脚本完成?这一篇,我想聚焦其中最实用也最典型的一个场景:用 openpyxl 批量读取 Excel、规整数据、再写回新表。这是几乎所有 Excel 自动化的起点。掌握了它,合并报表、数据汇总、工作表拆分等需求,都可以顺着同一套模式扩展出去。https://cdn.nlark.com/yuque/0/2026/png/1313150/1788423215815-6a58ced9-3e95-46a0-9109-375f8238ead2.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_33%2Ctext_5Lq_54mb5LqRIHd3dy4xNnl1bi5jbg%3D%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_102. openpyxl 到底解决了什么问题?先澄清一个技术前提:.xlsx 文件本质是一个 ZIP 压缩包,内部封装了一组 XML 描述文件。人通过 Excel 软件来操作它,而程序可以直接解析它的内容。openpyxl 正是 Python 生态中专门承担这一职责的库——它不依赖 Excel 软件本身,甚至无需安装 Office,即可完成对 .xlsx 的读写。一个常见疑问是:pandas 不也能处理 Excel 吗?能,但两者的定位差异非常明显:[*]pandas 长于计算——读取后即转换为 DataFrame,适合统计、筛选、建模等数据分析链路;
[*]openpyxl 长于控制——精确操作文件本身,单元格取值、样式、行高列宽、多工作表结构,皆在掌控范围之内。
因此实践中的分工通常是:pandas 负责"把数据算出来",openpyxl 负责"把表整理成可以直接交付的样子"。两者各司其职、配合使用,才是完整的工程方案。3. 上手第一步:读取一个 Excel 文件安装只需一条命令:pip install openpyxl读取文件的核心流程分三步:加载工作簿 → 定位工作表 → 遍历单元格。看一个最小可运行的例子:from openpyxl import load_workbook# read_only=True:以只读流式模式加载,可显著降低大文件的内存占用# data_only=True:读取公式单元格的缓存结果,而非公式表达式本身wb = load_workbook("销售明细.xlsx", read_only=True, data_only=True)ws = wb.active# 获取当前默认激活的工作表# 逐行迭代,每行以元组形式返回for row in ws.iter_rows(min_row=2, values_only=True): print(row)# 输出示例:(1, '华东', '笔记本', 32, 8999.0)wb.close()其中有三个参数,是我建议所有读者优先记住的:
[*]data_only=True:返回单元格的计算结果。若省略,读取含公式的单元格时,拿到的将是 "=SUM(B2:B10)" 这样的公式文本;
[*]read_only=True:启用流式读取,处理数万行的表格时不会因全量载入内存而卡顿;
[*]min_row=2:从第二行开始迭代,等价于跳过表头行,只取数据区。
这三个参数是我早期踩坑最集中的地方——第一次处理报表时,金额列读出来全是公式字符串,排查许久才发现问题出在遗漏了 data_only。4. 实战:把散落的月度报表合并成一张汇总表场景设定如下:目录 reports/ 下存放了 12 个月的销售报表(2025-01.xlsx 至 2025-12.xlsx),各表列结构完全一致,目标是将它们合并为一张年度明细表,并追加一张按大区汇总的销售额统计表。首先实现数据收集环节:from pathlib import Pathfrom openpyxl import load_workbookdef collect_rows(folder: Path) -> list: """遍历文件夹内全部月度报表,收集表头之外的所有数据行。 Args: folder: 报表所在目录,文件名形如 2025-01.xlsx。 Returns: 各报表数据行组成的列表,行内顺序与源表一致。 """ all_rows: list = [] # sorted() 按文件名排序,从而保证合并顺序与时间顺序一致 for file in sorted(folder.glob("2025-*.xlsx")): wb = load_workbook(file, read_only=True, data_only=True) ws = wb.active for row in ws.iter_rows(min_row=2, values_only=True): if row is None:# 报表末尾常有多余空行,遇空则跳过 continue all_rows.append(row) wb.close() return all_rows这里有一个值得固化的工程习惯:将"读数据"与"写数据"拆分为独立函数。收集环节只负责读取,输出环节只负责写入,统计逻辑单独成函数。模块边界一旦清晰,后续任何单一环节的调整都不会牵连全局。汇总逻辑同样保持简洁:from collections import defaultdictdef summarize_by_region(rows: list) -> dict: """按大区汇总销售额。列结构约定:(月份, 大区, 产品, 数量, 单价)。 Args: rows: collect_rows 返回的原始数据行。 Returns: 大区名到销售额的映射。 """ region_sum: dict = defaultdict(float) for _, region, _, qty, price in rows: # qty / price 可能为空值,统一按 0 兜底,避免 None 参与乘法运算 amount = float(qty or 0) * float(price or 0) region_sum += amount return dict(region_sum)值得一提的是,真实业务中「表」并非唯一的数据入口。很多需要定期维护的 Excel 报表,其上游其实是网页数据——例如竞品价格、行业指数、公开榜单。这类数据往往需要定时抓取、清洗后再落盘为 Excel。而一旦采集达到一定规模,单 IP 高频请求极易触发目标站点的频率限制与反爬策略。此时稳定可靠的代理资源就成了刚需——实践中我通常搭配亿牛云代理这类成熟的代理服务,通过其动态轮换的住宅代理或隧道代理来分散请求压力,保证采集链路的长效稳定。这也意味着:掌握 openpyxl 只是打通了"最后一公里",上游数据管道同样值得用工程化手段去加固。5. 写出结果:数据与格式一次到位写入环节同样遵循固定模式:创建工作簿 → 操作工作表 → 保存文件。这里再进一步——将表头加粗、设置底色与列宽,使产出物达到可直接对外交付的标准:from openpyxl import Workbookfrom openpyxl.styles import Font, Alignment, PatternFilldef write_summary(rows: list, summary: dict, out_path: str) -> None: """将明细数据与汇总结果写入多工作表 Excel 文件。 Args: rows: 合并后的明细数据行。 summary: 按大区汇总的销售额映射。 out_path: 输出文件的路径。 """ wb = Workbook() # 工作表一:合并后的完整明细 detail = wb.active detail.title = "年度明细" detail.append(["月份", "大区", "产品", "数量", "单价"]) for row in rows: detail.append(row) # 工作表二:按大区聚合结果 region_sheet = wb.create_sheet("大区汇总") region_sheet.append(["大区", "销售额"]) for region, amount in summary.items(): region_sheet.append() # 统一样式:表头加粗 + 浅灰底 + 居中对齐 header_font = Font(bold=True) header_fill = PatternFill("solid", fgColor="D9D9D9") for sheet in (detail, region_sheet): for cell in sheet: cell.font = header_font cell.fill = header_fill cell.alignment = Alignment(horizontal="center") sheet.column_dimensions["A"].width = 12 wb.save(out_path)def main() -> None: rows = collect_rows(Path("./reports")) summary = summarize_by_region(rows) write_summary(rows, summary, "2025年度汇总.xlsx")if __name__ == "__main__": main()几个关键写法的取舍说明:
[*]append() 以整行追加方式写入,代码量远小于逐单元格赋值,且执行效率更高;
[*]Font、PatternFill 等样式对象先创建后复用。若在每个单元格上重复实例化,行数较大时性能差异会非常明显;
[*]金额统一在写入前执行 round(amount, 2),避免交付端出现一长串浮点尾数。
跑通之后,原本 40 分钟的手工流程被压缩为秒级执行,且彻底消除了人工复制粘贴导致的串行、漏行风险。6. 效果验证:如何确认脚本产出是正确的?自动化最危险的状态,不是脚本报错,而是它安静地算错。我习惯用"三重对照"来完成验收:
[*]小样本对照:先用 2~3 个文件试跑,并手工合并一次,逐行比对两侧结果;
[*]总量对照:脚本输出的年度总销售额,必须与各月小计再求和的结果完全一致,不允许分毫误差;
[*]行数对照:合并明细的行数应等于 12 张源表数据行数之和。若存在偏差,通常意味着源表中存在隐藏空行或重复表头。
这三步看似笨拙,却能在数据对外发出前拦截绝大多数问题。尤其是行数对照,已多次帮我定位到报表中隐藏的空行与冗余表头。7. 常见问题和踩坑提醒第一个坑:目标文件被占用。 若脚本执行时 Excel 正打开着目标文件,save() 将直接抛出 PermissionError。建议在脚本入口给出明确提示,或在异常处理中捕获该错误并输出可读的引导信息,而非任其裸奔中断。第二个坑:日期字段类型不稳定。 openpyxl 通常将日期单元格读取为 datetime 对象,但部分数据源返回的是 Excel 内部序列号(浮点数)。当数据来源不可控时,应在写入前做统一的类型归一化,而非假定单一类型。第三个坑:只修改未保存。 所有单元格操作都发生在内存中,未调用 wb.save() 则全部失效;而 save() 覆盖原文件的行为不可逆——处理重要报表前务必先行备份。我的默认策略是输出至新文件名,绝不直接覆盖源文件。第四个坑:大文件导致内存暴涨。 数十万行的表格若未开启 read_only=True,openpyxl 会将整个工作簿载入内存,办公电脑极易卡死;写入方向则应对应使用 write_only=True 追加模式。读写两端均有对应的优化开关——用对了是"让 Excel 飞起来",用错了则是"让电脑趴下去"。第五个坑:上游数据获取的不确定性。 如前文所述,报表数据若来自网页采集,单点 IP 的稳定性会直接影响整个管线的产出质量。请求被限流或封禁时,脚本本身并不会报错,只会带回残缺的数据——这种"静默失败"比显式异常更难排查。为采集任务配置可靠的代理资源(如亿牛云的动态代理池),并把"抓取成功率"纳入巡检指标,才是对产出负责的做法。8. 总结提升:从跑通一个脚本,到沉淀一套模板本文的核心结论可以浓缩为三点:
[*]读表三参数:load_workbook 中的 data_only、read_only、min_row,决定了读取的正确性与性能;
[*]职责拆分:收集、统计、输出各自成函数,脚本才具备可维护、可交接的工程属性;
[*]产出必验:小样本、总量、行数三重对照缺一不可,宁可慢一步,不可错一分。
但比结论更重要的,是模板意识。第一次编写合并报表脚本或许需要一小时;可一旦将其沉淀为带完整注释与类型标注的模板文件,下个月、下季度乃至换一批数据源时,往往只需改动数行配置即可复用。自动化的价值不在于第一次运行,而在于此后每一次运行所累积的时间复利。先跑通,再改造,再沉淀——把散落的报表交给脚本,把重复的采集交给可靠的管道,把省下来的时间留给自己。这才是"让 Excel 飞起来"的真正含义。
感谢分享
有截图+步骤+效果+小结更好{:6_264:}
页:
[1]