资讯详情

Python实现Excel读改写:xlrd/xlwt/xlutils三件套实战

📅 2026/10/12 8:46:20 | 华诺云谱 👁 阅读
Python实现Excel读改写:xlrd/xlwt/xlutils三件套实战
用Python改Excel的时候最烦人的就是“想改的表格文件已经存在”。很多人一开始只用xlwt写完发现它压根儿不能打开已有的xls文件只能新建。好不容易换xlrd把数据读出来了又不知道怎么把改动写回去。后来找到xlutils但搜到的教程大多只给一段复制粘贴的代码压根儿没解释为什么要这么写出了问题也不知道怎么排查。这篇东西主要聊聊xlwt、xlrd、xlutils这三件套是怎么配合起来实现“对着同一个Excel文件读数据、改数据、再存回去”的完整流程。我会把底层原理、代码步骤、常见坑一次性说清楚适合正在做办公自动化、报表批量处理、数据台账更新的开发者和数据处理同学参考。读完你不仅能跑通代码还能明白每一步背后的逻辑——下次遇到格式丢了、数据覆盖了这类问题自己就有能力排查。1. 为什么“同一个Excel读写”会成为项目里的老大难先说个很多人踩过的场景。你手上有一张历史报表里面是过去几个月的业务数据格式已经设计好了有标题、有配色、有合并单元格。现在每个月都要把新数据追加到这张表里或者要批量修正某些单元格的值。用pandas读出来再另存格式必丢不现实。用openpyxl读xlsx还行但如果是老旧的xls文件openpyxl直接不支持。这时候你就明白为什么这三件套会同时出现xlrd负责把旧数据捞出来xlutils负责“复制”一份旧文件的内存镜像xlwt负责往这个镜像里写入新内容最后整体覆盖保存回同一个文件。整个过程听起来像“读原件、做副本、改副本、替换原件”而不是真的对同一个文件进行并发写入。这也就解释了为什么标题里叫“同一个Excel”而不是“同一个文件流”——它并不是像数据库那样支持随机写入而是通过“复制副本再覆盖保存”来达到“最终看起来就像改的是同一个文件”的效果。1.1 三个库各管一段谁也不能缺先明确分工。xlrd 2.x之前的老版本负责读取xls文件的结构。注意xlrd从2.0.0开始删除了对xlsx的支持只保留xls格式的读取能力。这一点很多人不知道装上最新版后一读xlsx就报错还以为是代码问题。xlwt 负责从零创建xls文件也能往已有Workbook对象里写入单元格。但它本身没有“打开已有文件”的能力。说白了它不管文件在磁盘上是什么状态只负责往内存里的Book对象填数据。xlutils 是个胶水层核心是copy函数。它能把xlrd打开的Book对象转换成xlwt可以操作的Workbook对象这中间会尽量保留原有的格式信息包括单元格样式、合并单元格、行高列宽等。三库的版本兼容是个大坑。目前稳妥的组合是xlrd1.2.0、xlwt1.3.0、xlutils2.0.0。我实测过很多次xlrd装2.x、xlutils还是老版本copy时直接报错。1.2 同样叫“读写”为什么不用openpyxl不是抬杠是场景不同。openpyxl只支持xlsx格式如果你处理的文件是当年Office 2003留下的xls后缀openpyxl连打开都做不到。而xlwt这套三件套是为老格式设计的兼容性更稳。如果你的文件是xlsx那直接用openpyxl就行不需要看这篇文章。xlsx本质上是压缩包内的XML文件集合openpyxl基于lxml解析性能也够用。但xls是二进制格式结构完全不同只能靠xlrd/xlwt这套原生生态。所以先看清楚你的文件后缀再决定用什么方案别上来就套代码。2. 核心流程拆解读、复制、改、存四步走完整流程可以用这么一句话概括用xlrd打开旧文件用xlutils.copy把旧文件转成xlwt能操作的副本用xlwt的接口往副本里写数据最后用workbook.save覆盖保存原文件。每一步都有一个关键细节值得展开。2.1 第一步xlrd打开旧文件注意formatting_info参数很多人读Excel只用了两个参数filename和sheet_by_index。但你要做“保留格式的复制”就必须额外加上formatting_infoTrue。不传这个参数xlutils在copy阶段就会丢失所有样式信息存出来的文件格式变成“空白模板”连列宽都保不住。import xlrd old_book xlrd.open_workbook(台账.xls, formatting_infoTrue)formatting_info是xlrd里的一个开关默认False。开启后xlrd会把单元格的字体、边框、背景色、数字格式、合并单元格等元数据全部加载到内存里。代价是读取速度变慢、内存占用变大但这是后续copy能保留样式的唯一前提。如果你只是临时读数据做统计不打算写回那就别开formatting_info白耗性能。2.2 第二步xlutils.copy复制出可写副本这一步没有任何多余参数就是一行代码。from xlutils.copy import copy write_book copy(old_book)但底层发生的事值得了解。xlutils的copy并不是把xlrd的Book对象直接当xlwt的Workbook用而是遍历原Book里的每一个sheet、每一行、每一列把数据、样式对象一一“搬到”新Workbook里。这个过程会调用xlrd的cell_type、cell_value以及xf_index等信息再映射到xlwt的write接口。这解释了为什么有些教程里提示“不支持合并单元格操作”——copy只是搬运不改变结构。如果你想在copy之后新增合并单元格需要用write_book的write_merge方法但这方法在已有sheet上执行会有坑后面细说。2.3 第三步通过xlwt接口写入新内容拿到write_book之后你操作的逻辑就跟xlwt新建文件完全一样了。先get_sheet拿sheet对象然后write(row, col, value)往指定位置写。write_book.get_sheet(0).write(10, 3, 新数据)注意xlwt的write默认会覆盖原单元格的值。如果你只是想追加新行要先用xlrd读一下原表的最大行数从那一行开始写。old_sheet old_book.sheet_by_index(0) start_row old_sheet.nrows # 从已有数据的下一行开始追加 for i, val in enumerate([A001, 张三, 88.5]): write_book.get_sheet(0).write(start_row, i, val)我犯过的一个低级错误是没看nrows直接硬编码从第5行开始写结果原表只有4行数据新数据从第5行开始没问题等原表哪天多了两行数据就重叠了。所以一定要动态获取行数。2.4 第四步save覆盖保存到原文件最后一步是write_book.save(台账.xls)。这个save会直接把完整的新内容写回原文件路径等于覆盖。但它不是“增量写”而是把整个Workbook序列化落盘。所以原文件越复杂合并单元格多、样式多、sheet多save耗时越长。这在逻辑上是必然的你要保留原始格式就得把带着格式信息的内容整体重写一遍。有个细节容易忽略save之前最好确保原文件没有被别的程序打开。Excel如果正在编辑这个文件Windows会锁定文件句柄save会直接报PermissionError。这个坑几乎每个人都踩过。3. 实操案例一个每月追加数据并修改指定单元格的完整脚本为了方便直接“抄作业”我给一个能直接跑通的小项目。假设有一张“销售台账.xls”里面有11行历史数据第0行是表头每个月要追加本月订单同时要把上月某个单元格的错误值修正。3.1 完整代码与关键行解析import xlrd from xlutils.copy import copy from datetime import datetime def update_sales_data(file_path, new_rows, fix_cells): file_path: xls文件路径 new_rows: 需要追加的新行列表套列表 [[...], [...]] fix_cells: 需要修正的单元格格式为 [(row, col, new_value), ...] # 1. 以保留格式的方式打开 old_book xlrd.open_workbook(file_path, formatting_infoTrue) old_sheet old_book.sheet_by_index(0) # 2. 复制成可写对象 write_book copy(old_book) write_sheet write_book.get_sheet(0) # 3. 追加新行 start_row old_sheet.nrows for offset, row_data in enumerate(new_rows): for col, value in enumerate(row_data): # 日期值统一转成字符串写进去避免格式错乱 if isinstance(value, datetime): value value.strftime(%Y-%m-%d) write_sheet.write(start_row offset, col, value) # 4. 修正指定单元格 for row, col, new_value in fix_cells: write_sheet.write(row, col, new_value) # 5. 覆盖保存 write_book.save(file_path) print(f更新完成共追加{len(new_rows)}行修正{len(fix_cells)}个单元格) # 调用示例追加两行修正第2行第3列的值 update_sales_data( 销售台账.xls, new_rows[ [2024-05-01, 华北区, 张三, 12000], [2024-05-02, 华东区, 李四, 9800], ], fix_cells[(2, 3, 15000)], )这里有几个地方值得单独说明。日期字符串处理。xlwt写datetime对象不是不行但可能涉及单元格格式匹配问题。老的xls模板里单元格可能已经是文本格式你写入datetime对象xlwt会把类型写成date类型显示就变成了数字序列。所以我在代码里主动把datetime转成字符串最省心。追加行的起点。我用old_sheet.nrows来定位但注意如果原表最后几行有隐藏的空行或者样式残留nrows可能不太准确。稳妥做法是先用old_sheet.cell_type(row, 0)判断是否为空再决定真实末尾在哪里。3.2 批量更新多个sheet实际项目里台账往往不止一个sheet。如果要对每个sheet都追加对应数据需要循环处理。for sheet_idx in range(old_book.nsheets): old_s old_book.sheet_by_index(sheet_idx) write_s write_book.get_sheet(sheet_idx) # 根据不同sheet的旧行数追加不同数据 if sheet_idx 0: start_row old_s.nrows write_s.write(start_row, 0, Sheet1新数据) elif sheet_idx 1: start_row old_s.nrows write_s.write(start_row, 0, Sheet2新数据)这个写法在sheet数量少时够用。如果sheet特别多、逻辑复杂建议先整理成分层字典代码会干净得多。3.3 性能实测文件变大后慢到什么程度我拿一个400KB左右的xls做过测试里面大约3000行、8列有表头样式和几个合并单元格。从打开到save完整跑一次耗时大概1.2秒。如果文件里面图片、图表比较多时间可能翻倍。这个速度对于月度/周度报表更新来说完全够用。除非你要批量处理上千个文件否则不需要额外优化。如果确实有大批量处理的场景可以考虑用threading并发处理不同文件但同一文件的读写仍然不建议并行因为copy后保存会打乱顺序。4. 区别于“整表替换”用xlrd读、用别的工具写的混合方案三件套的方案适合“保留旧格式、修改局部”的场景。但有时候你会发现要改的单元格太多逻辑复杂到用xlwt一笔一笔写很痛苦。这时候可以考虑另一个混合路线先用xlrd把全部数据读成DataFrame或二维列表用pandas做各种计算和筛选再用xlutils.copy保留原文件样式最后用xlwt把处理后的数据写进新的sheet或覆盖原数据。4.1 混合方案代码示例import xlrd import xlwt from xlutils.copy import copy import pandas as pd # 用xlrd读数据并转成DataFrame old_book xlrd.open_workbook(数据.xls, formatting_infoTrue) old_sheet old_book.sheet_by_index(0) data [old_sheet.row_values(r) for r in range(old_sheet.nrows)] df pd.DataFrame(data[1:], columnsdata[0]) # 用pandas做数据处理比如筛选金额大于1000的行 filtered_df df[df[金额] 1000] # 复制原文件保留格式 write_book copy(old_book) write_sheet write_book.get_sheet(0) # 在原数据下方追加处理结果 start_row old_sheet.nrows for idx, row in filtered_df.iterrows(): for col_idx, value in enumerate(row): write_sheet.write(start_row idx, col_idx, value) write_book.save(数据.xls)这个方案的优点是可以借用pandas的向量化操作处理逻辑复杂的数据时效率高、代码短。缺点是pandas读出来的值可能带numpy类型直接写入xlwt有时会报类型错误需要做一次转string或float。另外pandas会自动把空值变成NaN你要决定NaN是写空字符串还是跳过。4.2 这种方案什么时候值得用当你的核心需求是“改数据、算数据”而不是“调格式”时这个混合方案的优势很明显。比如你有一份库存表要根据新的入库单批量增加库存数量还要把低于安全库存的行标红。先用pandas算好每个商品的最新库存再回到xlwt去逐行写代码会更好维护。但如果你的需求只是“追加一行、改两个单元格”没必要上pandas杀鸡用牛刀反而增加依赖和转换成本。5. 常见问题与排查技巧实录这节内容全部来自实操中真实遇到的问题每一个我都亲自踩过。整理成表方便你对照排查。问题现象根本原因解决办法xlrd读xlsx报错xlrd 2.0移除了xlsx支持降级安装xlrd1.2.0或换openpyxlcopy后保存原格式全丢打开时没传formatting_infoTrue补上formatting_infoTrue写日期显示成数字xlwt写入datetime类型与单元格格式不匹配预先转成字符串或用xlwt.easyxf设置日期格式save报PermissionError原文件被Excel或WPS占用关闭文件再运行或先复制一份再保存追加行覆盖了原有数据起始行算错硬编码行号用nrows动态定位并检查末尾空行合并单元格区域写入报错copy后样式对象映射不完整避免直接写合并区域中间位置先解除合并再写写入中文乱码xlwt默认编码不是UTF-8创建Workbook时加encodingutf-8save后文件损坏文件本身是xlsx后缀但内容是xls先确认真实格式再选择工具库5.1 最常见的坑xlrd版本陷阱我见过最多的问题是“明明照着教程写一运行就报错Invalid file signature”。排查下来十个里有八个是xlrd装了2.0.0以上版本这个版本号称增强了安全性但代价是放弃xlsx读取。老教程写的时候xlrd还是1.2.0能读xls也能读xlsx所以代码没兼容问题。解决办法很简单pip install xlrd1.2.0。但要注意如果你同时在用pandaspandas会把xlrd作为xlsx的引擎。一旦你降级pandas读xlsx也会报错。这时候可以在pandas里指定engineopenpyxl互不干扰。5.2 合并单元格写入问题xlutils.copy在复制含合并单元格的文件时合并状态是保留的。但如果你往合并区域中间的某个单元格写新值xlwt并不会自动把值合并进去而是可能出现只显示一个值或者错位的情况。我的经验是要修改合并单元格里的内容直接写到合并区域的左上角单元格其他位置保持空白让合并效果自然呈现。如果你要动态调整合并范围光靠xlutils.copy做不到需要在copy之后手动操作xlwt的write_merge方法。但这个方法的参数r1, r2, c1, c2, label, style会把原有区域覆盖如果你没先把旧区域解除合并可能出现重叠样式异常。所以一般场景下我建议别动合并只改内容。5.3 文件被占用问题Windows环境下Excel打开文件后文件句柄会被锁定。如果你在PyCharm里运行脚本save时报PermissionError先想想是不是刚才手动打开过这个文件。这个问题没有完美的代码解法只能在运行前确保文件关闭。批量处理时我一般会在脚本开头加一个提示让操作者确认没有打开相关文件。Linux环境下这个问题少见但如果是通过网络共享目录访问Windows文件也可能因为SMB锁导致相同报错。5.4 大数据量下的内存与速度xlutils.copy的原理是把整个Book对象全部复制到内存。如果一个xls文件特别大比如几十MB、几万行copy过程可能消耗数百MB内存save时也可能卡顿。我测试过3000行的文件没问题但到了5万行样式多的时候明显变慢。这时候要考虑是否换方案保守做法是放弃xlutils.copy改用xlwt新建文件然后手动复制关键样式激进做法是改用openpyxl处理xlsx格式。实际项目中一个xls文件超过5万行的场景本来就少见老格式承载不了那么大容量。所以这个性能问题不需要过度担心。5.5 单元格样式丢失的深层原因还有一种半丢格式的情况copy之后保存大部分样式还在但某些单元格的背景色变了、字体重了。这多半是xlrd的formatting_info对某些特殊样式解析不完整比如条件格式、数据验证。这些高级功能在xls二进制格式里存储方式特殊xlrd没有完整暴露copy时自然丢。如果你依赖条件格式做报表预警建议在save之后再用openpyxl补一次操作不过前提是文件变成xlsx或者先转格式。这个限制属于库的能力边界不是配置问题。理解这一点你就不会再花几个小时纠结“为什么我的VBA条件格式没了”。6. 一些工程化的经验建议三件套本身很轻量但放在项目里还是有一些工程上的细节值得注意。这套经验来自我把几个Excel自动更新脚本做成定时任务后踩出来的。6.1 写操作前备份原文件这是最重要的一条经验。xlwt的save是整体覆盖一旦你代码里把某个单元格写错了原文件的旧数据直接没了。特别是运行在无人值守的定时任务里错误一旦发生你可能过几天才发现。我的做法是每次操作前先把原文件复制一份带时间戳的备份文件放在backup目录里。如果执行失败或数据异常手动回滚即可。import shutil import time timestamp time.strftime(%Y%m%d_%H%M%S) shutil.copy(file_path, fbackup/{timestamp}_{file_path})6.2 用日志记录每次更新的细节脚本在后台跑没办法实时看到输出。建议用logging模块记录打开了哪个文件、读到了多少行、追加了多少行、修正了哪些单元格、save成功还是失败。这个习惯救过我几次因为用户投诉“数据不对”的时候我可以直接翻日志定位是哪一行写错了不用对着最后的文件瞎猜。6.3 处理空表和不需要的旧数据有时候原Excel虽然有几十行历史数据但其中有部分行是你这次更新不需要保留的。比如用户要求把上个月的临时数据删掉只保留本月数据。用xlwt没法直接删除整行只能清空单元格内容。更常用的做法是判断某列标志位如果是“临时”就把整行数据覆盖成空字符串。for row_idx in range(1, old_sheet.nrows): flag old_sheet.cell_value(row_idx, 0) if flag 临时: for col_idx in range(old_sheet.ncols): write_sheet.write(row_idx, col_idx, )这么做是物理删除行还在但内容没了。打印或统计时会看到空行。如果你想要真正的“行消失”只能重建表把需要保留的行逐行写入新的sheet。这种方法不常用但需要知道。6.4 定时任务中的并发冲突如果你把这个脚本挂到定时任务里同时用户还可能手动打开Excel查看就会冲突。要么在脚本开头检测文件是否被锁定要么在固定时间窗口内运行避开办公时段。我自己的项目是放在凌晨1点执行配合备份和日志基本不会和用户操作撞车。7. 备选方案速览什么时候不要用三件套虽然这篇文章讲的是xlrd/xlwt/xlutils但作为一个实在的经验分享我还是要把边界划清楚。不同场景选不同工具效率差别很大。场景推荐工具原因旧xls追加数据、保留格式xlrdxlwtxlutils唯一能处理xls的三件套新xlsx且格式复杂openpyxl原生支持xlsx样式控制更精细大量数据计算pandasxlsxwriter计算和批量写入效率高几十万行数据pandasCSV或数据库xls/xlsx行数容量有限需要图表和公式win32com调Excel应用能驱动Excel完整能力win32com是个特殊选项它可以直接调Excel的程序接口等于让Excel自己干活。如果你要在已有xls里做VBA级别的操作比如刷新透视表、触发宏三件套做不了win32com可以。但它依赖Windows系统和已安装的Office可移植性差。作为补充方案了解一下就好。我个人的体会是工具选型永远看文件格式和操作粒度。遇到xls老文件、需要保留样式、改动量不大三件套就是最稳妥的选择。别盲目追求“最新最强”能稳定跑一年多不翻车的方案才是好方案。最后再分享一个小技巧当你把整个流程跑通之后可以封装一个update_excel_cell函数把“打开文件→写入→保存”压缩成两行调用后续接新的报表需求几乎不用改底层实现。这个库组合虽然老但稳定、可靠办公自动化里依然是值得掌握的实用工具。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑