刚接手的旧项目里有一个报表脚本,代码里清一色xlrd、xlwt、xlutils三个库。我第一反应是:现在处理Excel,不都用openpyxl或者pandas吗,怎么还有人把这套老组合搬出来?后来仔细读代码才明白,业务方要求处理的是一批历史遗留的.xls文件,既要读、要写,还要在不破坏原有格式的前提下做局部修改。当时若硬换openpyxl,反而要写一堆兼容逻辑。别看xlrd、xlwt、xlutils这名字像上个时代的产物,它们把“读、写、改”三件事拆得很干净,彼此配合起来恰好覆盖了老格式Excel的完整操作链路。
这篇教程就围绕这三件套展开,讲清楚它们各自负责什么、为什么选用它们、怎么组合使用,以及我在实际项目中踩过的坑和对应的绕坑办法。如果你正在维护旧系统、需要批量处理历史xls文件,或者只是想搞懂这三个库之间的协作关系,那这篇内容可以帮你直接把操作链路跑通。
1. 三库分工与项目思路拆解
1.1 为什么现在还在用这套库
先说一个很多人容易忽略的背景:xlrd、xlwt、xlutils都不是官方库,而是开源社区分别维护的第三方库。它们在老一代Python项目里出现频率极高,尤其是那些从Python 2时代传下来的报表模块,几乎清一色用它们处理Excel。
xlrd的定位是读取Excel文件,xlwt的定位是从零生成Excel文件,xlutils更像是工具集,核心价值是提供copy工具,把xlrd读进来的数据转换成xlwt可操作的对象,从而实现对已有文件的修改。三个库正好形成一个闭环:读、写、改。
为什么现在还有项目坚持用它们?关键是历史文件的现状决定的。很多公司上线十年以上的系统,存档文件、下载模板、导出报表都是.xls格式。这些文件里可能带公式、固定表头、手工调过的行高列宽、合并单元格。用openpyxl直接打开旧格式虽然也能做,但一旦文件里混着老旧的公式或自定义样式,解析出来的效果可能和原先在Excel里看到的不一致。而xlrd、xlwt这套组合是同时代产物,对这种文件兼容性更稳定,改动后样式变化可控。
另外还有一点:这类库的内存占用和接口风格也很符合旧脚本的调性。xlrd是只读解析,xlwt负责顺序写入,生命周期短、释放干净。尤其在批处理任务里,几百个xls文件逐份处理,很少会碰到内存打满的问题。
1.2 三库之间的关系到底怎么理解
把这三个库的关系讲直白点:xlrd负责把Excel文件从磁盘上解析成Python对象,并在读取时保留单元格坐标、格式信息、数据类型等元数据。xlwt负责把内存里的数据重新组织成Excel二进制结构,最终写回.xls文件。xlutils的copy模块则像一座桥,它拿到xlrd解析出来的工作簿对象,深拷贝成xlwt风格的工作簿对象,这样你就能在xlwt的接口下继续操作原有数据,比如修改某个单元格的值、追加一行数据、整体另存为新文件。
用一张表格概括更清楚:
| 库 | 职责 | 典型入口函数 | 操作对象 |
|---|---|---|---|
| xlrd | 读取xls | xlrd.open_workbook | xlrd Book对象 |
| xlwt | 写入xls | xlwt.Workbook | xlwt Workbook对象 |
| xlutils | 复制、修改桥梁 | xlutils.copy.copy | 从xlrd Book转为xlwt Workbook |
实际开发里最常见的组合套路是:先用xlrd打开原文件并开启formatting_info参数,再用xlutils.copy复制一份,接着用xlwt风格的接口在副本上改数据,最后另存为新文件。这个流程避免了“先读全部内容再手工重建一张新表”的做法,效率提升非常明显,也让整体代码结构更简单。
1.3 这套方案的适用边界
任何技术方案都有边界,这套库组合也不是万能的。首先,它只针对.xls老格式,不支持.xlsx。如果你手上的文件是新格式,第一步就应该考虑换工具。其次,xlwt写入的行数上限是65536行、列数上限是256列,一旦数据规模超过这个范围,直接会触发写入异常,根本没有商量余地。另外,xlrd在2.0版本以后也主动移除了对.xlsx的解析能力,如果你遇到“文件打不开”的错误,大概率就是版本和格式不匹配导致的。
所以,选择这套组合之前先做一个判断:文件后缀是不是.xls,行数是否在可接受范围内,是否需要保留原文件里的复杂样式。只要这三条都满足,用xlrd加xlwt加xlutils就是非常顺手的组合。
2. 环境准备与最基础的读写能力
2.1 依赖安装与版本搭配
安装这三个库非常简单,直接pip安装就行。但版本搭配是个容易踩坑的点,单纯执行”pip install xlrd“会默认安装最新版,而xlrd最新版只支持.xls,并不支持.xlsx。如果你想读取旧版.xlsx文件,可以考虑固定版本到1.2.0。
我自己在项目里采用过的稳妥组合是:
pip install xlrd==1.2.0 pip install xlwt pip install xlutils为什么不建议全部装最新版?因为xlutils本身依赖于xlrd和xlwt的接口,新版xlrd改动了一些内部结构,某些旧版本xlutils会报错。固定xlrd到1.2.0,是目前测试下来兼容性最好的选择。如果只需要处理xls,也可以不固定版本,但最后读到xlsx时肯定会报错,到时候再用pip降级也行。
装完后可以在交互环境里快速验证:
import xlrd import xlwt import xlutils print(xlrd.__version__)能正常打印版本号,说明环境基本没问题。
2.2 用xlrd读取xls文件
读取是整套流程的入口。xlrd读取.xls文件后返回一个Book对象,这个Book对象里包含多个Sheet,每个Sheet又包含行列数据。最基础的读取代码如下:
import xlrd workbook = xlrd.open_workbook('sample.xls') sheet = workbook.sheet_by_index(0) print('总行数:', sheet.nrows) print('总列数:', sheet.ncols) print('第一行第一列的值:', sheet.cell_value(0, 0))这个代码能跑通的前提是文件路径正确,且文件是真正的.xls格式。我们写代码时经常只关注sheet_by_index,但如果文件有多个Sheet,指定名称会更好:
sheet = workbook.sheet_by_name('成绩表')在所有读取接口里,比较常用的是cell_value,它接收行号和列号,返回对应位置的数值或字符串。如果需要判断单元格类型,可以用cell_type方法,返回0表示空,1表示文本,2表示数字,3表示日期,4表示布尔值。这在处理混合类型数据时非常有用。
如果单元格是日期格式,从cell_value拿到的其实是一个浮点数,需要借助xlrd自己的转换函数:
from xlrd import xldate_as_datetime dt = xldate_as_datetime(sheet.cell_value(1, 3), workbook.datemode)这里第二个参数是datemode,用来区分Excel日期体系是1900还是1904。别忘了传workbook.datemode,否则日期会算错。我自己在这上面栽过跟头,当时有个Mac生成的xls文件,日期差异直接导致统计结果偏移。
2.3 用xlwt从零生成表格
如果只是创建一个全新的.xls文件,xlwt完全可以胜任。核心思路是先创建Workbook对象,再添加Sheet,最后用write方法按行列写入数据。
import xlwt workbook = xlwt.Workbook(encoding='utf-8') sheet = workbook.add_sheet('学生名单') sheet.write(0, 0, '姓名') sheet.write(0, 1, '年龄') sheet.write(1, 0, '张三') sheet.write(1, 1, 18) workbook.save('student.xls')注意一个关键点:Workbook创建时如果不指定encoding,默认是ascii。一旦里面出现中文,写入保存时就会报UnicodeEncodeError。所以只要文件可能包含中文,我都会在创建Workbook对象时明确写上encoding='utf-8'。
对于单元格样式,xlwt也提供了接口。比如设置字体、对齐方式、边框,用XFStyle组合各种属性:
style = xlwt.XFStyle() font = xlwt.Font() font.name = 'SimSun' font.height = 220 # 单位是1/20磅,220表示11号字 style.font = font alignment = xlwt.Alignment() alignment.horz = xlwt.Alignment.HORZ_CENTER alignment.vert = xlwt.Alignment.VERT_CENTER style.alignment = alignment sheet.write(0, 0, '表头', style)这里的XFStyle就像给你的写入动作套上一层格式外壳。每次写入时,可以把同一个style对象传给多个单元格,不用为每个单元格新建样式,性能会好很多。xlwt内部对重复使用样式对象做了缓存,如果每格新建,反而浪费内存。
3. 核心实操:用xlutils改造旧表
3.1 复制工作簿时保留格式的关键
前面读和写都相对独立,但实际业务里最头疼的需求是“修改已有文件”。比如一份已经做好的模板,里面有公司logo、固定表头、合并单元格、预设的行高列宽,我们只想在某个位置填入数据,然后另存为新文件。如果用xlwt从零重建,等于把所有样式重新实现一遍,费时费力还容易出错。
xlutils.copy就是为这个场景设计的。它的作用是接受一个xlrd的Book对象,复制出xlwt可以直接操作的Workbook对象,然后我们在这个副本上做修改。最核心的使用前提是:打开原文件时必须加formatting_info=True,否则xlutils复制出来的副本会丢失字体、边框、合并单元格等信息。
import xlrd from xlutils.copy import copy rb = xlrd.open_workbook('template.xls', formatting_info=True) wb = copy(rb) ws = wb.get_sheet(0) ws.write(2, 1, '填充内容') wb.save('template_filled.xls')这段代码的作用是:把template.xls的内容连同样式一起复制出来,在副本的第3行第2列写入一个值,然后另存为新文件。原文件template.xls不会被动过,因为copy返回的是新对象。
我建议在工程项目里,修改前先备份原文件。虽然这个操作理论上不会破坏原文件,但万一保存时忘记改文件名,直接保存回原路径,就会覆盖掉原表。稳妥的做法是:输出文件名加时间戳,或者先copy到临时目录,确认无误后再替换。
3.2 在副本中写入内容并控制样式
用xlutils.copy拿到的是xlwt的Workbook对象,所以后续写入API和xlwt一样,都是get_sheet然后write。表面上看起来很简单,但有几个细节值得注意。
首先,通过get_sheet拿到的Sheet对象,和xlrd里直接读取的Sheet不是同一个类型。xlutils.copy已经把数据从只读模式转成了可写模式,所以不要再试图通过这个对象调用xlrd的读方法。如果想要读取单元格原始值,建议在复制之前先从xlrd的Book对象里拿一遍。
比如我想先看第5行第3列原来的值,再决定写入什么:
old_value = rb.sheet_by_index(0).cell_value(4, 2) print('原值:', old_value) ws = wb.get_sheet(0) if old_value == '待填充': ws.write(4, 2, '已填充') else: ws.write(4, 2, '无需处理')其次,复制出来的副本会保留原有单元格样式,但新增单元格默认是空白样式。如果往一个原本空白的区域写数据,它不一定继承整列或整行的样式,尤其不会自动带上边框、底纹。想要让新内容和原表风格统一,需要显式地指定样式。
一个实际做法是:先从原表中找一个同区域的单元格,读取它已有的样式信息,再把同样的样式设置到新单元格上。不过xlwt的样式提取不像openpyxl那么直接,通常需要结合XFStyle手动重建。简单点说,批量填充时先和需求方确认新增区域的样式标准,再在代码里统一设置,避免最后生成的表格看起来一边有边框、一边光秃秃。
同时对大量单元格连续写入时,尽量把write操作放在同一循环里,不要穿插读取操作。xlwt内部维护了工作表状态,穿插读取并不会导致崩溃,但会让代码结构变乱、可维护性变差。我习惯把读取逻辑和写入逻辑分成两个阶段:先收集数据,再批量落盘。
3.3 完整案例:批量给成绩表加等级列
结合三个库,我做一个完整的案例:有一份学生成绩单template.xls,里面有三列,分别是学号、姓名、总成绩。现在要新增一列“等级”,根据总成绩自动填写A、B、C、D,并且希望原表头格式和所有已有内容都保持不变。
原表结构示意:
| 学号 | 姓名 | 总成绩 |
|---|---|---|
| 1001 | 张三 | 88 |
| 1002 | 李四 | 72 |
| 1003 | 王五 | 59 |
处理逻辑:
import xlrd from xlutils.copy import copy rb = xlrd.open_workbook('template.xls', formatting_info=True) sheet = rb.sheet_by_index(0) wb = copy(rb) ws = wb.get_sheet(0) for row in range(1, sheet.nrows): score = sheet.cell_value(row, 2) if score >= 85: grade = 'A' elif score >= 70: grade = 'B' elif score >= 60: grade = 'C' else: grade = 'D' ws.write(row, 3, grade) wb.save('score_with_grade.xls')这里有几个关键点。原表头在row=0,数据从row=1开始,所以写入的也是从row=1开始。文件保存后,前三列的内容和样式完全保留,第四列新增等级。如果第四列原本有数据,这种写法会覆盖掉原值。因此动手前,确认新列是否原本就为空非常重要。
实际项目中,总成绩可能是字符串,比如原始数据里带着百分号“88%”。这时直接用score >= 85比较会出问题,需要先做数据类型转换。读取到字符串后,应先把百分号去掉,再转成浮点数。这种清洗逻辑看似简单,却是报表脚本里最容易出错的地方。
4. 常见问题与排查技巧实录
4.1 修改后格式全丢
这是xlutils使用里最常见的问题,打开原文件时漏了formatting_info=True。xlrd默认为了提升读取性能,不会加载格式信息。如果复制时没有这些格式数据,xlwt只能拿到光秃秃的值,自然存不出原有样式。
排查很简单,打开原文件时把参数加上:
rb = xlrd.open_workbook('source.xls', formatting_info=True)如果你用的是old_workbook方式读取且没有这个参数,保存后文件内容还在,但表头颜色、列宽、边框都会消失。这个问题在我早期的代码里出现过一次,当时现象很奇怪,数据都正常,就是表格像被脱了衣服一样。后来检查才发现是参数缺失。
另一个容易忽略的点是:如果原文件本身就是没有样式信息的纯数据表,加了这个参数也不会产生额外效果。所以别指望它能凭空变出样式,它只是把原文件里已有的东西搬运过来。
4.2 读取xlsx版本混乱
经常有人在网上提问:为什么用xlrd打开xlsx文件会报“Excel xlsx file; not supported”。这是因为xlrd 2.0以后明确不再支持xlsx格式,专注于xls老格式。如果确实需要读取xlsx,要么把xlrd版本固定到1.2.0,要么改用openpyxl。
我给项目定的原则是:xls文件一律走xlrd加xlwt加xlutils的组合,xlsx文件一律走openpyxl。两边不混用,因为格式处理逻辑不同,硬混会让代码里全是if分支,很难维护。
如果把xlrd版本固定到1.2.0,读取xlsx是可以的。但1.2.0在解析某些新特性单元格时,偶尔会出现警告或解析异常,并不是最佳体验。所以只在必须兼容旧代码时才这么干。
4.3 中文乱码与编码错误
中文乱码在xlwt场景里几乎都和编码设置有关。创建Workbook时没有指定encoding='utf-8',写入中文后保存时可能直接抛UnicodeEncodeError,即使保存成功,用Excel打开也可能显示乱码。
解决方案就一句话,创建Workbook时固定写:
workbook = xlwt.Workbook(encoding='utf-8')还有一种情况是读取旧文件时中文列名变成乱码。这可能不是库的问题,而是源文件本身的编码就不是常规编码。常见的旧xls文件可能用GBK或GB2312编码。xlrd解析后会返回Unicode字符串,如果打印到控制台正常,但写入新文件乱码,那检查一下新文件的Workbook编码是否设置正确。
我用过的最笨但有效的方法:先分别用utf-8和gbk尝试解码单元格内容,看看哪种能还原成可读中文,再在代码里手动处理。虽然不优雅,但足够解决历史文件带来的疑难杂症。
4.4 行数列数上限
xlwt写入.xls文件时,行数上限是65536行,列数上限是256列。如果数据超过这个量级,写入时会直接报错。我在处理一个十几万行的日志数据时就踩过这个坑,当时以为循环写入没问题,结果跑到中间抛异常,前面的数据全都没保存。
提前判断数据量非常必要。如果数据行数接近上限,可以考虑拆分成多个Sheet,或者干脆转换格式,改用openpyxl生成xlsx。另外,65536这个数字也是旧版Excel的格式限制,不是xlwt刻意卡你,而是底层二进制文件格式决定的。
4.5 文件被占用与写入崩溃
Windows下经常出现文件被Excel打开后,xlwt保存时报PermissionError。这是因为文件被另一个进程锁定,无法写入。排查思路很简单:先关闭所有打开该Excel文件的程序,再重新执行脚本。
有一种隐蔽情况:脚本本身以追加模式打开过同一个文件句柄,没有及时关闭,导致后面保存时锁住了自己。所以在处理多个文件时,要养成用with块管理文件对象的习惯,或者明确调用close方法释放资源。虽然xlrd和xlwt层面的文件句柄管理不太容易遇到问题,但在批量任务里,文件描述符堆积还是会造成写入异常。
定位方法非常直接:程序报错后立刻查看报错中的文件名,再检查代码里是否还有别的路径提前打开过它。也可以用系统的资源监视器确认进程占用,但一般问题都出在“忘了关”这条路上。
5. 个人实操的一些建议
最后聊一点个人经验。用xlrd、xlwt、xlutils这三件套处理Excel,最忌讳的就是一上来就堆代码。需求方说“改个报表”,一定要先弄清改的是哪一行、哪一列,原来的样式是从哪里来的,有没有必须保留的公式和合并单元格。很多时候,我们把技术实现想复杂了,结果把简单需求做成大工程。
我在实际开发里的习惯是:先用xlrd只读方式打开文件,输出所有Sheet名称、每个Sheet的行列数,再把目标区域的值扫描一遍。等数据结构完全清楚了,再去写复制和写入逻辑。这样改起来心里有底,也不会一遍遍地试错。
代码组织上,我倾向于把读写复制逻辑拆成独立函数。读取函数只负责返回数据,写入函数只负责落盘,复制和修改函数封装成业务动作。这样一旦遇到新需求,只需要在业务层面修改参数,不用动底层读写代码。
另外,千万别忽略备份。文件操作类的脚本,每一次覆盖保存都是有风险的。哪怕只是临时跑一下测试,也建议备份原文件。系好安全带再开车,总归不会错。
6. 后续还能往哪些方向扩展
这套库虽然老,但扩展思路并不封闭。比如在批量生成Excel报表时,可以结合模板文件,用xlutils.copy反复填充不同批次的数据,既能保留表头样式,又能快速产出文件。对运维场景来说,每天定时跑一次报表,输出带日期的文件名,完全够用。
还可以把读取Excel的逻辑做成数据校验工具。先用xlrd读出整表内容,再用数据校验规则逐行检查,把错误行和错误原因汇总成新表格。整个过程不需要数据库参与,非常适合快速处理临时数据。
如果项目逐渐往新格式迁移,可以考虑把xlwt的写入部分替换成openpyxl,保留xlrd的读取和xlutils的复制逻辑,做一个兼容层。这样旧代码不用推翻重写,又能支持xlsx新格式。我在实际项目里就是这样一步步过渡的,先让两种格式并存,再逐步切换默认输出格式,整个过程对线上业务的影响很小。
说到底,工具没有新旧之分,只有合不合适。在你需要操作xls文件、又要保留原始格式的时候,这三件套依然是务实的选择。