这标题看着简单,实际上手写一阵子就能发现,openpyxl这库的坑全藏在细节里。我从最早用xlrd读Excel、再用xlwt写、中间还得靠xlutils来回倒腾,到后来换成openpyxl一把梭,中间经历了不少折腾。这篇就把我日常用得最多的读写场景完整梳理一遍,代码都是可以直接抄走改改就用的。
1. 为什么偏偏是openpyxl:Excel处理库的选型逻辑
1.1 主流Excel处理库的横向对比
Python处理Excel的库不少,但各有各的脾气。如果你没经历过选错库然后返工的过程,我帮你把常见的几个按实际体验排个序。
xlrd:老牌读库,但注意,新版本已经不支持xlsx了,只读旧格式的xls。很多人网上搜教程还在用xlrd读xlsx,直接报错“格式不支持”,这是新手最容易踩的第一个坑。读取性能不错,但功能边界很死,只能读不能写。
xlwt:专门写xls格式,生成的还是老式二进制格式,不支持xlsx。最难受的是它默认65536行上限,超过直接抛异常,现在随便一个数据表就破万行了,根本不够用。
xlutils:本来是给xlrd和xlwt搭桥用的,把读到的文件复制一份再修改,流程绕来绕去,而且对格式的保留效果很差,处理完经常出现样式错乱。
pandas:如果你要处理的是“表格数据”而不是“Excel文件本身”,用pandas的read_excel/to_excel确实快,但它内部还是调用了openpyxl或xlrd做底层解析。pandas擅长的是数据计算和聚合,但要做单元格合并、设置样式、调整行高列宽、冻结窗格这些精细操作,pandas根本使不上劲。它用openpyxl做引擎后又包了一层,等于中间隔了层纱。
openpyxl:直接读写xlsx格式,读写一体,支持样式、公式、图表、图片,操作模型直观,文档也全。xlsx本身就是zip压缩包,内部是xml文件,openpyxl本质就是在操作这些xml结构,但封装得足够舒服,你不用关心底层的混乱格式。
我现在的工作流基本就一个原则:单纯算数据用pandas,但最终落盘成Excel一定用openpyxl来做样式和布局。这两者不是替代关系,是协作关系。pandas算完导出的裸表格太丑,拿openpyxl重新包装一遍,才能交出去。
1.2 个人项目中的选型依据
之前做过一个数据报表自动化的需求,每天要从接口拉一批数据,生成Excel发给业务方看。最初用pandas一把梭,to_excel写完就交了。结果业务方提了一堆要求:标题要合并单元格、表头要带底色、数字要千分位格式、列宽要自适应、还要冻结首行。
pandas这些全都做不了或者做得很别扭。后来全部改成openpyxl:pandas算数据,openpyxl排版。比如你可以一行代码把DataFrame数据导入工作表:
from openpyxl import load_workbook from openpyxl.utils.dataframe import dataframe_to_rows wb = load_workbook("template.xlsx") ws = wb.active # 把DataFrame逐行写入已有工作表 for row in dataframe_to_rows(df, index=False, header=True): ws.append(row) wb.save("report.xlsx")这一下就把数据处理和表格美化两个问题彻底分开了。选库的时候,先想清楚你的核心需求是什么:如果只是把结构化数据存成Excel给机器读,pandas够了;但只要是给人看的表格,最终都躲不开openpyxl。人看表格就讲究格式,格式就是openpyxl的强项。
2. 环境准备与工作簿加载:先让文件“打开”在内存里
2.1 安装与版本选择
安装很简单,pip一把梭:
pip install openpyxl但要注意几点。Python版本最好在3.7以上,openpyxl现在新版对老版本Python的兼容性已经在逐步收紧了。版本号我建议别用最新尝鲜,稳定版就行,比如3.1.x系列就非常稳,生产环境跑很久没出过问题。
如果是在公司内网环境装不了,可以选择下载wheel文件离线安装。还有个常见场景:你机器上同时有pandas和openpyxl,pandas的ExcelWriter引擎版本如果和openpyxl不匹配,写入时可能报错,这时候把两个库一起升级到兼容版本就好。
2.2 工作簿、工作表、单元格三层模型
很多人一开始会懵,openpyxl的对象关系其实特别清晰,就三层:
Workbook(工作簿)对应一个Excel文件,你可以理解成一本书的封面和目录。代码里的变量名习惯用wb。
Worksheet(工作表)对应文件中底部的sheet标签,类似书里的每一章。变量名常用ws。
Cell(单元格)对应表格里的每个格子,通过行列坐标定位。比如A1就是第一列第一行的交叉点。
这三层是一路向下的:wb打开文件,wb.sheetnames拿到所有工作表名字,wb["Sheet1"]选工作表,ws["A1"]取单元格。我一般会把这句记在脑子里:一切操作都是“工作簿→工作表→单元格”的逐级下钻,从不下钻就是最常犯的错误。
2.3 打开文件的三种姿势
先导入库,然后根据场景选打开方式:
from openpyxl import Workbook, load_workbook # 场景一:新建空白工作簿 wb = Workbook() # 场景二:读取已有xlsx文件(最常用) wb = load_workbook("data.xlsx") # 场景三:读取已有xlsx但忽略样式和格式,加了data_only参数 wb = load_workbook("data.xlsx", data_only=True)场景三里这个data_only=True是个大坑,后面会单独说。现在先记住:如果你只关心单元格里的计算值,不关心公式本身,就加data_only=True;如果需要修改公式,就别加。
打开之后,操作工作表的常用方式:
# 获取所有工作表名字 print(wb.sheetnames) # ['Sheet1', 'Sheet2'] # 获取当前活动工作表 ws = wb.active # 按名字取工作表 ws = wb["Sheet1"] # 新建工作表 ws_new = wb.create_sheet("新表", position=0) # position=0插到最前面 # 删除工作表 del wb["要删除的表"] # 重命名 ws.title = "改名了"我之前接手过一个乱糟糟的报表程序,里面每次运行都新建一个“Sheet1、Sheet1_1、Sheet1_2”这样的工作表,就是因为对Workbook对象不熟悉,反复调用wb = Workbook()而不是在同一个wb上create_sheet。搞清楚这三层关系之后,再复杂的文件结构都能理清楚。
3. 写入操作:从空白工作簿到一张能看的报表
3.1 单元格写入的四种方式
写入单元格是我用得最多的操作,但大多数教程只教一种写法,实际工作里四种都得掌握。
from openpyxl import Workbook wb = Workbook() ws = wb.active # 方式一:按坐标直接赋值,最简单 ws["A1"] = "姓名" # 方式二:用cell方法指定行列,适合循环 ws.cell(row=1, column=2, value="年龄") # 方式三:append追加写入,适合从列表批量写入 row_data = ["张三", 25, "北京"] ws.append(row_data) # 自动追加到第一行空行 # 方式四:一次性写入多行数据 multi_rows = [ ["李四", 30, "上海"], ["王五", 28, "广州"], ] ws.append(multi_rows) # 注意:append不支持直接传二维列表等等,方式四直接传二维列表会报错的,正确做法是循环append:
for row_data in multi_rows: ws.append(row_data)方式三和方式四里的append是我最推荐的批量写入方式。它的特点是永远从第一行无数据的行开始写,你不用自己维护行号递增,省掉手写row_index变量到处+1的烦恼。以前我用cell方式循环写入,一旦中间插入一行,后面全乱套,换append再也没出过这种问题。
3.2 三种写入场景及其适用场景对比
我把写入场景归纳成三类,下面这个表可以帮你快速判断用哪种:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 固定位置的表头、标题 | 直接坐标赋值 | 位置确定,语义清晰,A1就是标题 |
| 循环写入大量结构化数据 | append批量追加 | 自动定位空行,代码简洁,不容易错行 |
| 从DataFrame转到Excel | dataframe_to_rows再append | 处理数据用pandas,排版用openpyxl,各取所长 |
实践里最忌讳的就是拿着append写所有数据,结果想往中间某个位置插一个单元格都费劲。写入之前先画一遍表格布局草图,标清楚哪些是固定位置、哪些是流式数据,再决定写法和循环结构。
3.3 样式设置:让报表“能看”的基本功
数据写进去了,但光秃秃的报表交出去会被业务方吐槽。openpyxl的样式系统我承认刚接触时有点繁琐,但符合“每个属性都要从模块导入”的设计。下面是我封装的常用样式函数:
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter def style_header(ws, row_idx, columns): """设置表头样式:加粗、背景色、居中""" header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF") header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") center = Alignment(horizontal="center", vertical="center") for col_idx in range(1, columns + 1): cell = ws.cell(row=row_idx, column=col_idx) cell.font = header_font cell.fill = header_fill cell.alignment = center def auto_width(ws, min_width=10, max_width=40): """根据单元格内容自动调整列宽""" for col_cells in ws.columns: col_letter = get_column_letter(col_cells[0].column) max_len = min_width for cell in col_cells: if cell.value: # 中文按2个字符宽度估算 text_length = sum(2 if ord(ch) > 127 else 1 for ch in str(cell.value)) max_len = max(max_len, text_length + 2) ws.column_dimensions[col_letter].width = min(max_len, max_width) def freeze_and_filter(ws, freeze_cell="A2", ref_range=None): """冻结首行并添加筛选器""" ws.freeze_panes = freeze_cell if ref_range: ws.auto_filter.ref = ref_range用的时候就这样:
# 写入表头 headers = ["姓名", "年龄", "城市"] ws.append(headers) # 写入数据 for item in data_list: ws.append(item) # 美化 style_header(ws, 1, len(headers)) auto_width(ws) freeze_and_filter(ws, "A2", f"A1:C{ws.max_row}") # 设置行高(标题行高一点) ws.row_dimensions[1].height = 25有几个容易被忽略但又特别实用的设置:合并单元格用ws.merge_cells("A1:C1"),取消用unmerge_cells,合并后只保留左上角的值;边框对象Side配合Border四边设置,注意我国表格的审美偏好比较偏向左边框、细边框,你全设成粗框就会显得很“学习资料”;数字格式用cell.number_format = "#,##0.00",这会直接显示千分位,不用动原始数据。样式调整是个细活,每次改完记得save重新打开看效果,别靠脑补。
4. 读取操作:把Excel里的数据准确“捞”出来
4.1 读取单元格与区域的几种姿势
读取相比写入稍微简单点,我不直接给代码,先讲清楚你面对一个未知表格时怎么问问题。你用openpyxl打开一个别人发的Excel,第一件事永远是搞清楚这张表长什么样:
from openpyxl import load_workbook wb = load_workbook("别人发的表.xlsx") ws = wb.active # 1. 看表头在不在第一行,最大行列是多少 print(ws.max_row, ws.max_column) # 2. 读单个单元格 value = ws["A1"].value # 或者用行列坐标 value = ws.cell(row=1, column=1).value # 3. 读整个区域的值(返回二维数组) all_data = ws.values # 这是个生成器,循环打印 for row in all_data: print(row) # 4. 读指定区域的切片 for row in ws.iter_rows(min_row=2, max_row=5, min_col=1, max_col=3, values_only=True): print(row)values_only=True这个参数相当实用,如果不加,iter_rows返回的是Cell对象,你还得再取.value;加了你拿到的直接就是纯值。有人觉得直接用ws.values就完了,但那个是全表扫描,如果你只要中间几列,性能浪费很大。
4.2 遍历数据的惯用手法
掌握了上面这些基础访问之后,遍历就顺理成章了。我给你一个比较标准的遍历模式:
# 从第2行开始,逐行读取,跳过表头 for row in ws.iter_rows(min_row=2, values_only=True): name, age, city = row # 假设表头是姓名、年龄、城市 if name is None: continue # 跳过空行 print(f"{name} - {age} - {city}")这里有个容易踩的坑:ws.max_row获取的是“曾经被编辑过的最大行”,如果表中间有一行被删掉过,行号会出现空洞。你先测一下它的值和实际行数对不对得上,如果对不上,要么手动用iter_rows限定范围,要么读取时做None判断跳过空行。
按列读取的场景我也常遇到:
# 单独读某一列 col_letter = "B" col_values = [cell.value for cell in ws[col_letter] if cell.value is not None] # 一次性读所有列(返回的是每列的元素列表) for col_cells in ws.columns: col_name = col_cells[0].value # 第一个单元格当作列名 col_data = [cell.value for cell in col_cells[1:] if cell.value is not None]读取最需要注意的是数据类型保留。Excel在openpyxl眼里是有类型的:字符串、数字、日期、布尔值。你读出来是什么就是什么,比如Excel里的日期是datetime.datetime对象,不是字符串。很多新手拿到日期直接str()转字符串,结果格式变成“2024-01-15 00:00:00”。我的习惯是读的时候判断一下类型再做格式化:
from datetime import datetime cell_value = ws["B2"].value if isinstance(cell_value, datetime): cell_value = cell_value.strftime("%Y-%m-%d")4.3 带格式数据的读取边界
这里必须提醒一个openpyxl的根本限制:它读不到你Excel里肉眼看到的“显示值”。你看到的表格里可能是“1,234.56”,但单元格里的原始值其实是1234.56;你看到的是“2024/1/15”,但底层值可能是日期对象。数据本身没丢,只是“给人看”和“给程序读”是两套东西。如果你的下游程序需要的是显示格式的字符串,那得自己写代码模拟Excel的显示逻辑,比如根据number_format判断再加千分位。千万不要指望openpyxl能替你完成这一步。
遇到这种显示值需求的场景,我一般分两条路走:如果数据量不大,直接用openpyxl读原始值,然后用Python的字符串格式化自己处理;如果数据量大且显示格式非常复杂(比如条件格式、自定义格式),我会考虑用Excel本身打开另存为CSV,或者用其他专业库,绕开format的盲区。
5. 实战中绕不开的坑与性能调优
5.1 公式存读与data_only参数
公式这块,我估计是初学者遇到最多的困惑,因为光是data_only=True和data_only=False,很多人就搞不清楚自己该用哪个。
openpyxl不干“计算”这件事,它只负责把公式字符串存进单元格。比如你让ws["C1"] = "=SUM(A1:B1)",它真就把这个公式写进文件了。Excel打开文件时会自动计算并显示结果。但是——如果你用openpyxl读这个文件,不加data_only的时候,你拿到的值是公式字符串“=SUM(A1:B1)”,不是算好的结果。加了data_only=True,你拿到的才是这个单元格的“缓存值”,也就是Excel上次保存时算出来的结果。
问题来了:如果一个文件生成后从没被Excel打开保存过,那么它的缓存值就不存在,data_only=True读出来是个None。这个坑特别隐蔽,我见过很多同事调了半天代码发现读出来全是None,然后怀疑人生。解决办法很简单:要么生成文件后用Excel或其他能计算并重新保存的工具过一遍,要么自己用Python公式库计算一遍再写进去。
我的建议很简单:凡是你要读公式计算结果,且文件是别人维护的Excel,data_only=True没问题;凡是你的脚本自己生成的文件又立刻读,别用data_only=True,直接用False读公式字符串,或者干脆绕开公式,写入前先把算术结果算好存成值。
5.2 大文件性能问题与读写模式
我遇到过一份10MB左右的xlsx,用普通模式load_workbook等了近30秒,内存直接飙了几百MB,操作起来还卡。问题就出在openpyxl为了支持随机访问所有单元格,默认把所有工作表的全部内容都塞进了内存。
解决方法是openpyxl提供的两种内存优化模式:
# 只读模式:加载时只保留数据,不加载样式对象 wb_r = load_workbook("big.xlsx", read_only=True) # 只写模式:只能写入,不能读,但写入时内存占用极低 wb_w = Workbook(write_only=True)只读模式的用法是迭代,不能用随机访问:
ws_r = wb_r["Sheet1"] for row in ws_r.iter_rows(values_only=True): # 这里处理每一行数据 pass只写模式则是只有append,不能改已有单元格:
ws_w = wb_w.create_sheet() for row_data in big_data_list: ws_w.append(row_data) wb_w.save("output_big.xlsx")之前的报表需求,数据20万行,用默认的普通模式生成直接卡死,换write_only模式后十几秒就完事了。这个模式跑完后打开文件看,样式、合并、列宽一个不少,非常能打。
但注意:write_only模式下你不能用ws["A1"]这种方式去改单元格,也不能合并单元格或设置样式。所以如果写入的内容需要复杂格式排版,只能在数据量没那么大的时候用普通模式做精细操作;超大文件且排版要求又不高时,优先write_only。
5.3 合并单元格与读取的相爱相杀
合并单元格是表格美化常用的,但读取时一定出幺蛾子。比如A1:A3合并了,A1有值,A2和A3用openpyxl读出来是None,不是和A1相同的值。这是符合Excel底层逻辑的——合并后只有左上角那个格真实存在值。
处理办法是在读取时反查合并区域:
def get_merged_value(ws, row, col): """读取单元格,如果它在合并区域里,返回合并区域左上角的值""" for merged_range in ws.merged_cells.ranges: if row in range(merged_range.min_row, merged_range.max_row + 1) and \ col in range(merged_range.min_col, merged_range.max_col + 1): return ws.cell(row=merged_range.min_row, column=merged_range.min_col).value return ws.cell(row=row, column=col).value写入时的合并也要注意顺序:**先写入数据到左上角,再合并。**如果先合并再写,经常会把值写到被合并掉的隐藏格子上,导致你这个值丢了。我一开始就犯过这个错,写完合并且同一个文件再打开,标题不见了,查半天发现是写到被合并的犄角旮旯里了。
5.4 日常用下来最顺手的一批小技巧
对已有文件做模板化复用:把表头、样式、列宽全部手工设置好存成一个模板文件,程序里每次load进来然后追加数据,这样样式代码几乎为零,运行也快。这是我最推荐的方案,比程序里反复设置样式省太多事。
单元格写入时避免自动转类型:Excel里输入“00123”会被自动转成123,但openpyxl不会,你写入字符串"00123"存出来就是00123。这是好事还是坏事取决于场景,但你要知道这一点,就不会因为订单号前导0丢失而抓狂。如果是从其他来源读数据再写入,注意源数据先转成字符串。
保存前检查覆盖风险:wb.save("同名文件.xlsx")会直接覆盖旧文件。如果有重要历史文件,先做备份或者用时间戳命名:
f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"。附一个完整的“洗数据”小工具,我做数据处理前经常先用它探路:
def inspect_sheet(ws, max_preview=5): """快速查看工作表的基本结构和前几行内容""" print(f"工作表名:{ws.title}") print(f"最大行列:{ws.max_row} x {ws.max_column}") print(f"合并区域:{list(ws.merged_cells.ranges)}") print("前几行数据:") for i, row in enumerate(ws.iter_rows(min_row=1, max_row=max_preview, values_only=True), start=1): print(f" Row{i}: {row}")拿到任何一张陌生表,先跑一遍这个函数,文件结构一目了然,再决定后面怎么处理。
6. 常用的一个完整读写范例
把上面的内容串起来,给你一个最小可用的报表示例。这个示例涵盖:新建文件→写入数据→设置样式→保存;重新打开→读取数据→打印结果。你把它当成模板,改改就能用。
from openpyxl import Workbook, load_workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter # ---------- 写入部分 ---------- wb = Workbook() ws = wb.active ws.title = "销售报表" headers = ["月份", "销售额", "利润"] ws.append(headers) data = [ ["1月", 12000, 3000], ["2月", 15000, 4000], ["3月", 13000, 3500], ] for row in data: ws.append(row) # 总和行 ws.append(["合计", sum(r[1] for r in data), sum(r[2] for r in data)]) # 表头样式 for cell in ws[1]: cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") cell.alignment = Alignment(horizontal="center") # 列宽 for col in range(1, 4): ws.column_dimensions[get_column_letter(col)].width = 18 # 冻结首行 ws.freeze_panes = "A2" wb.save("sales_report.xlsx") # ---------- 读取部分 ---------- wb2 = load_workbook("sales_report.xlsx") ws2 = wb2["销售报表"] print(f"数据范围:{ws2.max_row}行 x {ws2.max_column}列") for row in ws2.iter_rows(values_only=True): print(row)这个流程跑通之后,你已经能应付日常90%的Excel读写需求。剩下那些更高级的场景,比如图表、图片、数据透视表、条件格式,都是在这个基础上加料而已,主体框架不变。
我在实际项目中最大的体会是:**openpyxl最突出的优点是把“能用代码操作Excel”这件事的门槛压到了极低,但代价是它并不能替代你理解Excel本身的数据模型——合并单元格会带来空值,公式和缓存值是两套系统,显示格式与原始值并不一致。**先把这些绕开,你的读写代码就会稳得多。