为什么我想写这一篇?
做办公自动化这几年,我见过太多这样的场景:每天早上的固定流程,是把十几个业务群发来的 Excel 下载下来,手动复制、粘贴、对齐格式、核对数字,最后拼成一份日报。整件事耗时约 40 分钟,其中真正需要"思考"的环节可能只有 5 分钟,其余 35 分钟都是在做低价值的机械搬运。
这类工作,恰恰是 Python 最擅长接管的部分。
《超简单:用 Python 让 Excel 飞起来》这本书给我的最大启发,不是某个具体函数的用法,而是一套判断标准:凡是"有规律、可重复、需批量"的 Excel 操作,都值得停下来问一句------它是不是本来就该由脚本完成?
这一篇,我想聚焦其中最实用也最典型的一个场景:用 openpyxl 批量读取 Excel、规整数据、再写回新表。这是几乎所有 Excel 自动化的起点。掌握了它,合并报表、数据汇总、工作表拆分等需求,都可以顺着同一套模式扩展出去。

2. openpyxl 到底解决了什么问题?
先澄清一个技术前提:.xlsx 文件本质是一个 ZIP 压缩包,内部封装了一组 XML 描述文件。人通过 Excel 软件来操作它,而程序可以直接解析它的内容。openpyxl 正是 Python 生态中专门承担这一职责的库------它不依赖 Excel 软件本身,甚至无需安装 Office,即可完成对 .xlsx 的读写。
一个常见疑问是:pandas 不也能处理 Excel 吗?能,但两者的定位差异非常明显:
- pandas 长于计算------读取后即转换为 DataFrame,适合统计、筛选、建模等数据分析链路;
- openpyxl 长于控制------精确操作文件本身,单元格取值、样式、行高列宽、多工作表结构,皆在掌控范围之内。
因此实践中的分工通常是:pandas 负责"把数据算出来",openpyxl 负责"把表整理成可以直接交付的样子"。两者各司其职、配合使用,才是完整的工程方案。
3. 上手第一步:读取一个 Excel 文件
安装只需一条命令:
bash
pip install openpyxl
读取文件的核心流程分三步:加载工作簿 → 定位工作表 → 遍历单元格。看一个最小可运行的例子:
python
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),各表列结构完全一致,目标是将它们合并为一张年度明细表,并追加一张按大区汇总的销售额统计表。
首先实现数据收集环节:
python
from pathlib import Path
from openpyxl import load_workbook
def collect_rows(folder: Path) -> list[tuple]:
"""遍历文件夹内全部月度报表,收集表头之外的所有数据行。
Args:
folder: 报表所在目录,文件名形如 2025-01.xlsx。
Returns:
各报表数据行组成的列表,行内顺序与源表一致。
"""
all_rows: list[tuple] = []
# 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[0] is None: # 报表末尾常有多余空行,遇空则跳过
continue
all_rows.append(row)
wb.close()
return all_rows
这里有一个值得固化的工程习惯:将"读数据"与"写数据"拆分为独立函数。收集环节只负责读取,输出环节只负责写入,统计逻辑单独成函数。模块边界一旦清晰,后续任何单一环节的调整都不会牵连全局。
汇总逻辑同样保持简洁:
python
from collections import defaultdict
def summarize_by_region(rows: list[tuple]) -> dict[str, float]:
"""按大区汇总销售额。列结构约定:(月份, 大区, 产品, 数量, 单价)。
Args:
rows: collect_rows 返回的原始数据行。
Returns:
大区名到销售额的映射。
"""
region_sum: dict[str, float] = defaultdict(float)
for _, region, _, qty, price in rows:
# qty / price 可能为空值,统一按 0 兜底,避免 None 参与乘法运算
amount = float(qty or 0) * float(price or 0)
region_sum[region] += amount
return dict(region_sum)
值得一提的是,真实业务中「表」并非唯一的数据入口。很多需要定期维护的 Excel 报表,其上游其实是网页数据------例如竞品价格、行业指数、公开榜单。这类数据往往需要定时抓取、清洗后再落盘为 Excel。而一旦采集达到一定规模,单 IP 高频请求极易触发目标站点的频率限制与反爬策略。此时稳定可靠的代理资源就成了刚需------实践中我通常搭配亿牛云代理 这类成熟的代理服务,通过其动态轮换的住宅代理或隧道代理来分散请求压力,保证采集链路的长效稳定。这也意味着:掌握 openpyxl 只是打通了"最后一公里",上游数据管道同样值得用工程化手段去加固。
5. 写出结果:数据与格式一次到位
写入环节同样遵循固定模式:创建工作簿 → 操作工作表 → 保存文件。这里再进一步------将表头加粗、设置底色与列宽,使产出物达到可直接对外交付的标准:
python
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill
def write_summary(rows: list[tuple], summary: dict[str, float], 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([region, round(amount, 2)])
# 统一样式:表头加粗 + 浅灰底 + 居中对齐
header_font = Font(bold=True)
header_fill = PatternFill("solid", fgColor="D9D9D9")
for sheet in (detail, region_sheet):
for cell in sheet[1]:
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 飞起来"的真正含义。