
1. 为什么还要聊 xlrd 读取 Excel 这件事如果你最近两年才开始写 Python 数据处理大概率一上来就被推荐用 pandas 或者 openpyxlxlrd 这个名字听起来像是上个时代的产物。但实际情况是大量存量的数据脚本、老项目的依赖链、以及一些特定格式的历史表格文件仍然需要 xlrd 来兜底。我自己手头就有几个跑了五六年的对账脚本核心读取逻辑一直是 xlrd迁移成本高而且它确实稳定没出过什么幺蛾子。xlrd 是一个专门用来读取 Excel 文件的 Python 库注意它的定位非常明确——只读不写。它最擅长处理的是老版本的.xls格式也就是 Excel 97-2003 那种二进制格式。后来 xlrd 2.0 版本做了一个重大决定直接砍掉了对.xlsx的支持只保留.xls。这个变化让很多人踩了坑明明昨天还能跑的代码升级了一下依赖就报错。所以搞清楚 xlrd 的能力边界比会调几个 API 重要得多。这篇文章适合几类人看一是手里有老.xls文件需要批量处理的数据从业者二是维护老项目、不敢随便升级依赖的工程师三是想搞清楚 Excel 读取底层逻辑、不想只会调 pandas 黑盒的学习者。我会从设计思路、核心 API、实操流程、常见报错排查几个角度把 xlrd 读取 Excel 这件事讲透代码都是可以直接复制去跑的。2. xlrd 的整体设计与选型逻辑拆解2.1 为什么 xlrd 只读不写这个设计到底图什么很多人第一次用 xlrd 会疑惑为什么它不能写文件。这其实是一个刻意的架构取舍。读取和写入 Excel 的复杂度完全不对称。读取的时候你面对的是一个已经确定结构的二进制流只需要按格式规范解析就行而写入要考虑样式、公式、合并单元格、图表、兼容性等一大堆东西复杂度高出一个量级。xlrd 的作者选择把读取这一件事做到极致写入交给 xlwt针对.xls或者 openpyxl针对.xlsx。这个设计带来的直接好处是库的体积小、依赖少、解析速度快。我实测过一个 8MB 左右、大概三万行的.xls文件xlrd 全量读取到内存大概两三秒比某些通用库快不少。因为通用库往往要同时兼容多种格式内部做了大量判断和抽象而 xlrd 针对.xls是专门优化的。从选型角度讲如果你的场景是纯读取.xlsxlrd 是最省心的选择。如果你需要读写.xlsx那就别用 xlrd 了直接上 openpyxl。如果你要处理的是超大文件、需要做数据分析pandas 配合合适的引擎更合适。选型的核心判断依据就是文件格式和是否需要写入这两点想清楚工具就定了。2.2 xlrd 2.0 砍掉 xlsx 支持的前因后果xlrd 2.0 这个版本是个分水岭。在 1.2.0 及之前xlrd 是能读.xlsx的虽然读得不算特别好但能用。到了 2.0官方直接把.xlsx的支持移除了只留.xls。官方给出的理由是.xlsx本质是个 zip 包加 XML和.xls的二进制格式完全是两码事硬塞在一个库里维护成本太高不如让专门的库去做。这个改动导致的最典型问题就是你写了一段读取.xlsx的代码本地跑得好好的因为本地装的是老版本 xlrd结果部署到新环境pip 自动装了最新版代码直接报xlrd.biffh.XLRDError: Excel xlsx file; not supported。这个报错我见过太多次了新手往往一脸懵明明文件就在那怎么就 not supported 了。解决办法有两个方向。一是如果文件确实是.xlsx换用 openpyxl 或者 pandas 的read_excel配合engineopenpyxl。二是如果必须用 xlrd那就把文件另存为.xls格式或者降级 xlrd 到 1.2.0。但降级不是长久之计因为老版本可能有别的隐患而且新环境不一定让你锁版本。我的建议是新项目一律不要依赖 xlrd 读.xlsx把它当成纯.xls读取工具来用心态就对了。2.3 xlrd 和 pandas、openpyxl 的定位对比为了让你一眼看清这几个库的关系我整理了一张对比表。这张表是我自己在选型时反复参考的基本上覆盖了日常决策需要的所有维度。维度xlrdopenpyxlpandas.read_excel支持格式仅.xls2.0.xlsx/.xlsm取决于引擎是否可写否是否读取为主依赖体积极小中等大读取速度快针对 xls中等中等偏慢底层控制粒度细可到单元格细粗直接给 DataFrame适合场景老格式批量读取新格式读写数据分析从表里能看出来xlrd 的生态位非常清晰老格式、纯读取、要细粒度控制。pandas 适合你不想管底层、直接要结构化数据的场景。openpyxl 适合新格式且需要写入的场景。三者不是替代关系而是互补关系。我自己的习惯是做数据探索用 pandas做生产环境的稳定读取脚本用 xlrd 或 openpyxl看格式定。3. xlrd 核心 API 与关键细节解析3.1 打开工作簿的两种方式与各自适用场景xlrd 打开文件的核心函数是xlrd.open_workbook。它有两种典型用法一种传文件路径一种传文件内容。传路径最简单import xlrd workbook xlrd.open_workbook(data.xls)这种方式适合本地文件、路径固定的场景。但如果你是从网络下载、从数据库读取、或者从内存流里拿到的文件就得用file_contents参数with open(data.xls, rb) as f: content f.read() workbook xlrd.open_workbook(file_contentscontent)这里有个细节要注意file_contents接收的是 bytes不是字符串。如果你不小心传了字符串会报类型错误。另外open_workbook还有几个常用参数值得了解。on_demand设为 True 时工作表只在被访问时才加载处理大文件能省内存。formatting_info设为 True 时会解析单元格的格式信息比如字体、颜色、边框但会显著增加内存占用和解析时间。如果你只关心数据不关心样式千万别开这个参数我见过有人无脑开着结果读一个几万行的文件内存直接爆了。还有一个ragged_rows参数默认 False意思是每行都按最大列数补齐。如果你确定数据是规整的保持默认就行如果数据稀疏、列数差异大设成 True 能省点内存。3.2 工作表、行、列、单元格的层级关系xlrd 的对象模型是清晰的四层结构工作簿workbook→ 工作表sheet→ 行/列 → 单元格cell。理解这个层级后面所有操作都是顺藤摸瓜。拿到 workbook 之后获取工作表有几种方式# 按索引获取从 0 开始 sheet workbook.sheet_by_index(0) # 按名称获取 sheet workbook.sheet_by_name(Sheet1) # 获取所有工作表名称 names workbook.sheet_names() # 获取所有工作表对象 sheets workbook.sheets()按索引获取是最稳的因为名称可能被用户改过索引不会变。但索引也有坑如果工作表顺序被调整了索引对应的内容就变了。我的经验是如果脚本要长期跑最好先打印一遍sheet_names()确认顺序或者用名称加异常兜底的方式。拿到 sheet 之后几个核心属性要记住sheet.nrows是行数sheet.ncols是列数。这两个值决定了你遍历的范围。注意nrows和ncols是有效数据的边界不是 Excel 里显示的最大行列。比如你只填了 A1 到 C5那 nrows 就是 5ncols 就是 3哪怕你在 Excel 里看到的是几百列。3.3 单元格读取的三种方法及性能差异读取单元格内容是 xlrd 最核心的操作主要有三种方法性能差异明显选错了在大数据量下会很痛苦。第一种是sheet.cell(row, col)返回一个 Cell 对象包含值和类型cell sheet.cell(0, 0) print(cell.value, cell.ctype)第二种是sheet.cell_value(row, col)直接返回值不返回类型value sheet.cell_value(0, 0)第三种是sheet.row_values(row)和sheet.col_values(col)一次性取一整行或一整列row_data sheet.row_values(0) col_data sheet.col_values(0)性能上row_values和col_values最快因为它们内部做了批量处理。cell_value次之cell最慢因为要构造 Cell 对象。我做过一个粗略测试读取一个一万行十列的文件用row_values逐行读大概 0.8 秒用cell_value双重循环读大概 2.5 秒差距三倍左右。所以如果你只是要数据优先用row_values。但cell方法也不是没用当你需要判断单元格类型时它返回的 ctype 就派上用场了。ctype 的取值有几种0 是空1 是文本2 是数字3 是日期4 是布尔5 是错误。日期类型特别要注意xlrd 读出来的日期是个浮点数需要转换才能变成可读的日期。3.4 日期和时间的处理这是最容易踩的坑Excel 里的日期本质上是个数字从 1900 年 1 月 1 日开始算的天数。xlrd 读出来也是这个数字比如你看到 45000 这样的值别慌那是日期。转换需要用xlrd.xldate_as_tuple或者xlrd.xldate_as_datetimeimport xlrd from datetime import datetime cell sheet.cell(0, 0) if cell.ctype xlrd.XL_CELL_DATE: date_tuple xlrd.xldate_as_tuple(cell.value, workbook.datemode) dt datetime(*date_tuple) print(dt)这里有个关键点xldate_as_tuple的第二个参数是workbook.datemode不是随便传的。这个值决定了日期系统的基准不同来源的文件可能不一样。如果你传错了日期会差好几天甚至更多。我踩过一次坑读出来的日期总是差一天查了半天才发现是 datemode 没传对。还有一个更隐蔽的坑有些单元格明明是日期但 ctype 返回的是数字 2 而不是 3。这种情况通常是因为单元格格式没被正确识别或者文件本身格式不规范。解决办法是结合业务判断如果某列应该是日期就手动用xldate_as_tuple转换不要完全依赖 ctype。4. 完整实操流程与代码实现4.1 环境准备与版本锁定第一步永远是环境。安装 xlrd 很简单pip install xlrd但这里有个版本陷阱。如果你要读.xlsx必须装 1.2.0 或更早pip install xlrd1.2.0如果你只读.xls装最新版就行。我强烈建议在项目里用requirements.txt锁死版本写清楚xlrd2.0.1或者xlrd1.2.0避免不同环境装出不同版本导致行为不一致。这个教训是用血换来的曾经有个脚本在测试环境跑得好好的上线就崩查了两小时发现是测试环境锁了老版本生产环境自动装了新版。4.2 一个可直接复用的读取模板下面这个模板是我自己常用的涵盖了打开文件、遍历工作表、逐行读取、日期处理、异常兜底你可以直接拿去改import xlrd from datetime import datetime def read_xls(file_path): result {} try: workbook xlrd.open_workbook(file_path, on_demandTrue) except Exception as e: print(f打开文件失败: {e}) return result for sheet_name in workbook.sheet_names(): sheet workbook.sheet_by_name(sheet_name) rows_data [] for row_idx in range(sheet.nrows): row_values sheet.row_values(row_idx) # 处理日期列假设第 3 列是日期 for col_idx, cell in enumerate(sheet.row(row_idx)): if cell.ctype xlrd.XL_CELL_DATE: date_tuple xlrd.xldate_as_tuple(cell.value, workbook.datemode) row_values[col_idx] datetime(*date_tuple) rows_data.append(row_values) result[sheet_name] rows_data workbook.release_resources() return result if __name__ __main__: data read_xls(example.xls) for sheet, rows in data.items(): print(f工作表: {sheet}, 行数: {len(rows)})这个模板有几个设计考虑。用on_demandTrue延迟加载处理多工作表大文件时省内存。用sheet.row(row_idx)而不是row_values是为了拿到 ctype 做日期判断如果你确定没有日期列直接用row_values更快。最后调release_resources()释放资源虽然 Python 有垃圾回收但显式释放在大批量处理时更稳妥。4.3 批量处理多个文件的实操方案实际工作中很少只读一个文件通常是读一个目录下的一批文件。这时候要注意几个点。第一是文件过滤只处理.xls结尾的别把临时文件~$xxx.xls也读进来那种文件会报错。第二是异常隔离一个文件读失败不能影响其他文件。第三是结果汇总通常要把多个文件的数据合并成一个大表。import os import xlrd def batch_read(folder_path): all_data [] for filename in os.listdir(folder_path): if not filename.endswith(.xls) or filename.startswith(~$): continue file_path os.path.join(folder_path, filename) try: workbook xlrd.open_workbook(file_path) sheet workbook.sheet_by_index(0) for row_idx in range(1, sheet.nrows): # 跳过表头 row sheet.row_values(row_idx) row.append(filename) # 标记来源文件 all_data.append(row) workbook.release_resources() except Exception as e: print(f处理 {filename} 失败: {e}) continue return all_data这里跳过表头是从第 1 行开始因为第 0 行通常是列名。给每行加上来源文件名是个好习惯后续排查数据问题时能快速定位是哪个文件出的问题。异常捕获里用continue而不是break保证一个文件失败不影响后续文件。4.4 大文件读取的内存优化技巧.xls格式本身有个硬限制单表最多 65536 行、256 列。所以严格来说.xls不会特别大但如果你一次读几十个文件或者开了formatting_info内存还是会吃紧。优化手段有几个。一是用on_demandTrue前面提过。二是不开formatting_info除非你真的需要样式。三是读完一个工作表就释放不要把所有工作表都留在内存里。四是如果只需要部分列用col_values按需取不要全量读。还有一个技巧是用生成器逐行 yield而不是一次性构建大列表。这样内存占用是常数级的适合做流式处理def iter_rows(file_path, sheet_index0): workbook xlrd.open_workbook(file_path, on_demandTrue) sheet workbook.sheet_by_index(sheet_index) for row_idx in range(sheet.nrows): yield sheet.row_values(row_idx) workbook.release_resources()用的时候for row in iter_rows(big.xls):就行内存友好。5. 常见报错与排查技巧实录5.1 报错速查表我把这些年遇到的 xlrd 报错整理成了一张表基本覆盖了九成以上的问题报错信息原因解决办法XLRDError: Excel xlsx file; not supported用 2.0 读 xlsx换 openpyxl 或降级到 1.2.0FileNotFoundError路径错误检查路径用绝对路径XLRDError: Unsupported format文件不是真正的 xls确认文件格式可能是改后缀的IndexError: list index out of range行列索引越界用 nrows/ncols 做边界检查TypeError: expected bytesfile_contents 传了字符串用 rb 模式读文件日期读出来是数字没做日期转换用 xldate_as_tuple 转换中文乱码编码问题一般 xlrd 自动处理检查文件本身5.2 几个典型问题的深入排查XLRDError: Excel xlsx file; not supported这个报错排第一因为它太常见了。排查思路很简单先确认文件真实格式。有时候文件后缀是.xls但内容其实是.xlsx这种情况用文本编辑器打开文件头看看.xls的开头是D0 CF 11 E0.xlsx的开头是PK因为它是 zip。如果发现是伪装成 xls 的 xlsx那就得换库或者用 Excel 另存为真正的 xls。日期读出来是数字这个问题排查时要先确认 ctype。如果 ctype 是 3直接转换就行。如果 ctype 是 2 但你知道它是日期那可能是单元格格式问题手动转换。还有一种情况是日期读出来差几天那基本就是 datemode 传错了检查workbook.datemode的值。中文乱码相对少见因为 xlrd 对中文支持不错。如果真遇到了通常是文件本身编码有问题或者是从别的系统导出的非标准 xls。这种情况可以尝试用encoding_override参数指定编码但成功率不高更靠谱的办法是用 Excel 重新另存为标准格式。5.3 独家避坑经验第一个经验是关于nrows和ncols的。这两个值有时候会比你预期的大因为 Excel 里可能有你看不到的格式残留比如某一行曾经填过数据后来删了但格式还在xlrd 可能把它算进去。所以遍历时不要盲目相信 nrows最好加个空行判断遇到连续空行就跳过或停止。第二个经验是关于合并单元格的。xlrd 读合并单元格时只有左上角那个单元格有值其他都是空的。如果你需要把合并单元格的值填充到每个子单元格得自己写逻辑处理。这个在读取报表类文件时特别常见不处理的话数据会缺一大片。第三个经验是关于性能的。如果你要反复读取同一个文件的多个工作表不要每次都open_workbook打开一次拿到 workbook 对象然后反复取 sheet。open_workbook的开销主要在解析文件头重复调用很浪费。第四个经验是关于文件句柄的。虽然 xlrd 读的是文件内容但如果你用open()读文件再传file_contents记得用with语句确保文件关闭。我见过有人忘了关处理几千个文件后报 too many open files。6. 写在最后的一点个人体会xlrd 这个库说它老也好说它局限也好但在.xls读取这个细分场景里它依然是那个最趁手的工具。我这些年用它处理过各种奇奇怪怪的历史表格从财务报表到生产记录稳定性没得说。关键是要认清它的边界只读.xls不碰.xlsx不开不必要的格式化解析做好日期转换和异常兜底。如果你现在还在维护用 xlrd 的老脚本我的建议是别急着迁移先把版本锁死把异常处理补全让它稳稳地跑着。如果是要写新脚本先看清楚文件格式.xls就用 xlrd.xlsx就老老实实上 openpyxl。工具没有高低合适最重要。踩过的坑多了你就会发现很多问题不是库的问题而是没搞清楚它到底能干什么、不能干什么。