
做数据导入时我拿到一份接近 10 万行的订单明细 Excel一开始没多想直接用 pandas 的read_excel一把梭。结果等待时间比预想长很多内存占用也跟着涨。为了搞清楚“到底该用哪个库”我把 Python 生态里读取 Excel 的高频库都拉出来做了一次横向耗时测试用 10 万行真实结构的数据文件统一验证。这篇文章会完整记录这次测试的过程测试数据怎么造、每种库的读取代码怎么写、结果如何对比以及不同库之间为什么会有那么大的耗时差距。对于经常处理 Excel 导入导出、报表数据清洗的同学这份笔记可以直接作为选型参考。1. 为什么要专门测试 Excel 读取耗时1.1 10 万行 Excel 在真实业务中非常普遍很多人觉得 10 万行 Excel 已经算“大数据”其实这个量级在业务系统里很常见。比如财务系统导出的年度流水明细。ERP 里的物料库存快照。运营每月整理的订单明细表。第三方系统导出的对账数据。这类文件行数通常在几万到几十万之间列数从十几列到几十列不等。它们不会大到让数据库崩溃但用 Python 读取时如果工具选得不对等待时间可能从“秒级”直接变成“分钟级”体验差别非常大。1.2 Python 读取 Excel 的常见库有哪些目前主流方案有这几个库全称/说明适合格式pandas数据分析核心库封装了底层解析器xlsx / xlsopenpyxl读写 xlsx 的专业库xlsxxlrd老牌读取库2.0 以后只支持 xlsxlsxlwt老牌写入库配合 xlrd 使用xlspyxlsb读取二进制 Excel 格式xlsbcsv 标准库文本表格格式不算 Excel 但常作为对比基线csv一个很容易踩坑的认知是pandas本身并没有实现 Excel 解析它调用的是openpyxl或xlrd等底层引擎。所以“pandas 一定比 openpyxl 快”这个说法并不成立甚至因为 pandas 需要额外构建 DataFrame反而会更慢一些。1.3 本文测试的目标本文要回答几个问题同样一份 10 万行 Excel不同库读取耗时差多少。pandas 的read_excel在不同引擎下表现如何。openpyxl 普通模式和只读模式差距大不大。结合使用场景应该怎么选。测试重点是“读取整张表并遍历数据”的耗时不包含后续逻辑处理。2. 环境准备与测试方案设计2.1 运行环境性能测试结果和机器配置强相关不同 CPU、硬盘、内存都会影响最终数字。这里说明一下测试环境方便你做横向参考操作系统Windows 11 / Ubuntu 22.04 均可Python3.10依赖库pandas、openpyxl、xlrd、xlwt、pyxlsb测试文件同一份数据生成的 10 万行 xlsx / xls / csv建议使用虚拟环境避免污染全局环境python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate2.2 安装依赖创建一个requirements.txtpandas2.2.2 openpyxl3.1.5 xlrd2.0.1 xlwt1.3.0 pyxlsb1.0.10然后安装pip install -r requirements.txt注意事项pandas 2.x读取.xlsx需要openpyxl。pandas读取.xls需要xlrd。xlrd 2.x已经不支持.xlsx这是很多人最容易踩的坑。pyxlsb仅用于读取.xlsb并且这个库更新较慢运行时建议按官方示例调整 API 细节。版本号会随时间更新本文示例中以这些常用稳定版本为主。如果你本机版本更高通常兼容性没有大问题但xlrd对.xlsx的限制从 2.0 开始是硬性的无法绕过。2.3 测试数据设计为了尽量接近真实业务文件我设计了 10 列数据包含文本、数值、日期字符串内容如下字段示例值类型id100001数值name同事100001文本dept部门1文本position岗位1文本salary12345数值score78.5小数entry_date2023-01-01字符串city城市1文本status在职文本remark无文本日期统一用字符串2023-01-01这种格式写入不依赖 Excel 原生日期类型。这样做是为了避免不同库对日期类型转换逻辑不一致引入额外耗时。如果测试原生日期类型结果会有波动后面可以单独说明。2.4 如何控制测试误差Excel 文件读取耗时受很多因素影响比如磁盘缓存、系统当前负载、杀毒软件实时扫描等。为了减少误差建议每个读取函数先预热一次再开始计时。每个函数重复执行 2 到 3 次取平均值或中位数。测试前关闭无关程序避免 CPU 争抢。读取函数内部只做“完整读取 遍历行数”不做额外复杂逻辑。计时统一使用time.perf_counter()比time.time()精度更高适合短时间过程测量。3. 构造 10 万行测试文件3.1 项目目录结构先创建项目目录excel_read_benchmark/ ├── data/ │ ├── data_100k.csv │ ├── data_100k.xlsx │ ├── data_100k.xls │ └── data_100k.xlsb ├── generate_data.py ├── benchmark_libs.py └── requirements.txt其中generate_data.py负责生成测试文件。benchmark_libs.py负责执行各库读取测试。data目录存放生成的测试文件提交 Git 时通常排除大文件。3.2 生成 CSV 文件CSV 不是 Excel 原生格式但它是很多数据流程的通用中间格式读取速度也快非常适合作为对比基线。生成代码如下# generate_data.py import csv import random from datetime import date, timedelta TOTAL_ROWS 100000 HEADER [ id, name, dept, position, salary, score, entry_date, city, status, remark ] CITY_LIST [f城市{i} for i in range(1, 21)] POSITION_LIST [f岗位{i} for i in range(1, 21)] DEPT_LIST [f部门{i} for i in range(1, 21)] def build_row(index): name f同事{100001 index} dept DEPT_LIST[index % len(DEPT_LIST)] position POSITION_LIST[index % len(POSITION_LIST)] salary 5000 (index * 37) % 20000 score round(60 (index * 7) % 4000 / 100, 2) entry_date (date(2020, 1, 1) timedelta(daysindex)).isoformat() city CITY_LIST[index % len(CITY_LIST)] status 在职 if index % 10 ! 3 else 离职 remark 无 return [100001 index, name, dept, position, salary, score, entry_date, city, status, remark] def generate_csv(file_path): with open(file_path, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow(HEADER) for i in range(TOTAL_ROWS): writer.writerow(build_row(i)) print(fcsv 生成完成: {file_path}) if __name__ __main__: generate_csv(data/data_100k.csv)这里把日期做成字符串再写入后续读取时不需要额外解析时间能更纯粹地比较库本身的解析性能。使用utf-8-sig编码可以让 Excel 直接打开 CSV 时中文不乱码。3.3 用 openpyxl 生成 xlsx 文件生成 xlsx 时不要用普通模式一行一行append否则 10 万行写入会非常慢。更好的是write_onlyTrue这种模式下 openpyxl 不会在内存中保留所有单元格对象而是边写边落盘。# generate_data.py 追加函数 from openpyxl import Workbook def generate_xlsx(file_path): workbook Workbook(write_onlyTrue) sheet workbook.create_sheet(data) sheet.append(HEADER) for i in range(TOTAL_ROWS): sheet.append(build_row(i)) workbook.save(file_path) print(fxlsx 生成完成: {file_path})需要提醒的是xlsx 单张表最多支持 1048576 行10 万行写入没有任何问题。生成时间取决于机器磁盘和 CPU通常在十几秒到一分钟之间属于正常现象。3.4 用 xlwt 生成 xls 文件.xls是 Excel 97-2003 的老格式存在一个硬性限制单个工作表最多 65536 行。所以无法直接生成一个包含 10 万行的.xls单表文件。解决方案是拆成两个 sheet每个 sheet 放 50000 行这样整个工作簿总行数约 10 万行。# generate_data.py 追加函数 import xlwt def generate_xls(file_path): workbook xlwt.Workbook(encodingutf-8) rows_per_sheet 50000 sheet_count (TOTAL_ROWS rows_per_sheet - 1) // rows_per_sheet for sheet_idx in range(sheet_count): sheet workbook.add_sheet(fdata_{sheet_idx}) for col_idx, col_name in enumerate(HEADER): sheet.write(0, col_idx, col_name) start sheet_idx * rows_per_sheet end min(start rows_per_sheet, TOTAL_ROWS) row_in_sheet 1 for i in range(start, end): row build_row(i) for col_idx, value in enumerate(row): sheet.write(row_in_sheet, col_idx, value) row_in_sheet 1 workbook.save(file_path) print(fxls 生成完成: {file_path})注意xlwt写入每一行都要逐个单元格write所以 10 万行写入会比较慢建议有点耐心。如果只是做读取测试也可以找一个现成的真实.xls文件文件结构不需要和 xlsx 完全一致。3.5 xlsb 文件处理说明.xlsb是 Excel 的二进制格式读取速度通常比.xlsx快但 Python 生态里支持写入的库非常少最常用的pyxlsb只能读取、不能写入。所以本文不写自动生成.xlsb的代码若你机器上有 Microsoft Excel 或 LibreOffice可以手动把data_100k.xlsx另存为data_100k.xlsb再测试。如果你并不关心.xlsb可以直接跳过这一项不影响整体结论。3.6 生成文件时先预览结构生成完文件后建议先用办公软件打开预览或写一段只读前几行的代码确认数据没有乱码、列没有错位。这里提供一个快速验证方法import pandas as pd df pd.read_excel(data/data_100k.xlsx, nrows5) print(df.head())nrows5表示只读前 5 行不会加载完整文件适合快速检查。4. 各库读取耗时测试代码4.1 测试框架封装在benchmark_libs.py中先定义统一的计时函数。为了让测试可重复允许设置重复运行次数默认每次读取一次避免把测试时间拉得太长。# benchmark_libs.py import csv import time TOTAL_ROWS 100000 def measure(name, func, *args, repeat1): print(f {name} ) costs [] last_rows 0 for i in range(repeat): start time.perf_counter() last_rows func(*args) cost time.perf_counter() - start costs.append(cost) print(frun {i 1}: {cost:.4f}s, rows{last_rows}) if repeat 1: avg_cost sum(costs) / len(costs) print(favg: {avg_cost:.4f}s) return last_rows后续每个读取函数都返回读取到的行数这样既能验证数据是否真的被完整读取也可以排除“解析没执行完”的假象。4.2 pandas openpyxl 读取 xlsx这是最常用的操作import pandas as pd def read_pandas_xlsx(path): df pd.read_excel(path, engineopenpyxl) return df.shape[0]df.shape[0]表示 DataFrame 的行数。pandas 会自动把第一行作为表头所以这行代码实际读取了 99999 条业务数据。严格来说和其它函数相差一行但 10 万行里差一行对耗时影响可以忽略不计。如果想手动指定列类型可以减少后续类型推断的开销def read_pandas_xlsx_dtype(path): df pd.read_excel( path, engineopenpyxl, dtype{id: int, salary: float}, ) return df.shape[0]不过类型推断本身也是读取耗时的一部分这一步会影响测试结果。后面统一按默认方式测试即可。4.3 pandas xlrd 读取 xls读取老式.xls文件时指定enginexlrddef read_pandas_xls(path): df pd.read_excel(path, enginexlrd) return df.shape[0]这种方法在语法上很简单但要注意xlrd 2.x不支持.xlsx所以这里只能传入.xls文件。4.4 openpyxl 普通模式读取 xlsx先写出最朴素的 openpyxl 读取方式from openpyxl import load_workbook def read_openpyxl_normal(path): workbook load_workbook(path, data_onlyTrue) sheet workbook[data] row_count 0 for row in sheet.iter_rows(values_onlyTrue): row_count 1 workbook.close() return row_count这里data_onlyTrue表示读取单元格缓存值而不是公式本身。普通模式下load_workbook会把整个工作表的样式、列宽、合并单元格、数据类型等元数据都加载到内存所以加载阶段比较耗时遍历时反而很快。4.5 openpyxl 只读模式读取 xlsxopenpyxl 提供了只读模式适合处理大文件def read_openpyxl_readonly(path): workbook load_workbook(path, read_onlyTrue, data_onlyTrue) sheet workbook[data] row_count 0 for row in sheet.iter_rows(values_onlyTrue): row_count 1 workbook.close() return row_count两种模式的代码差异很小只是多了一个read_onlyTrue但内部解析方式完全不同。只读模式不会一次把所有单元格对象加载到内存而是按需迭代所以内存占用更低读取时间通常也更短。4.6 xlrd 直接读取 xls不经过 pandas直接用 xlrd 读取import xlrd def read_xlrd(path): workbook xlrd.open_workbook(path) row_count 0 for sheet in workbook.sheets(): for row_idx in range(sheet.nrows): _row sheet.row_values(row_idx) row_count 1 return row_count因为.xls单表限制为 65536 行而生成文件时用了两个 sheet所以这里要遍历workbook.sheets()。有了这层循环读取的总行数就是两个 sheet 的行数之和。sheet.row_values(row_idx)返回一行所有单元格的列表这里只负责访问不额外处理数据。4.7 pyxlsb 读取 xlsb可选如果生成了.xlsb文件可以使用pyxlsb读取try: from pyxlsb import open_workbook except ImportError: open_workbook None def read_pyxlsb(path): if open_workbook is None: print(pyxlsb 未安装跳过 xlsb 测试) return 0 row_count 0 with open_workbook(path) as workbook: # 如果文件名不是 data请按实际 sheet 名调整 sheet workbook.get_sheet(data) for row in sheet.rows(): row_count 1 return row_countpyxlsb的 API 在不同版本中变化较大如果你的版本不同建议先查看官方文档确认get_sheet的参数是索引还是名称。这个测试在实际项目中不是必须项如果安装或文件转换太麻烦可以跳过。4.8 csv 标准库读取 csvCSV 是纯文本格式没有工作表、样式、公式等元数据读取速度通常会快很多作为参考基线def read_csv_by_stdlib(path): row_count 0 with open(path, encodingutf-8-sig, newline) as f: reader csv.reader(f) header next(reader, None) # 跳过表头 for row in reader: row_count 1 return row_count这里把表头跳过了。但要注意前面各个函数在统计时对表头的处理不完全一致比如 pandas 会自动去掉表头openpyxl 普通模式会把表头也算进row_count里。这个差异对结果的影响很小但如果想做到绝对严谨可以统一在所有函数里都跳过表头再统计。为了减少纠结我建议测试目的是“一次完整读取文件的耗时”而不是“业务数据行的精确数量”。表头多一行在 10 万行数据面前基本可以忽略。4.9 主测试入口把所有函数组织到主入口依次执行def main(): data_dir data print(开始读取测试等待结果...) print(- * 50) measure(pandas openpyxl 读取 xlsx, read_pandas_xlsx, f{data_dir}/data_100k.xlsx) measure(pandas xlrd 读取 xls, read_pandas_xls, f{data_dir}/data_100k.xls) measure(openpyxl 普通模式读取 xlsx, read_openpyxl_normal, f{data_dir}/data_100k.xlsx) measure(openpyxl 只读模式读取 xlsx, read_openpyxl_readonly, f{data_dir}/data_100k.xlsx) measure(xlrd 直接读取 xls, read_xlrd, f{data_dir}/data_100k.xls) measure(csv 标准库读取 csv, read_csv_by_stdlib, f{data_dir}/data_100k.csv) if open_workflow is not None: measure(pyxlsb 读取 xlsb, read_pyxlsb, f{data_dir}/data_100k.xlsb) print(- * 50) print(测试结束) if __name__ __main__: main()如果你的机器上只有部分文件比如没有生成.xlsb可以注释掉对应调用不会影响其它项目。5. 测试结果与直观对比5.1 结果怎么看运行下面的命令开始测试python benchmark_libs.py输出格式大致如下开始读取测试等待结果... -------------------------------------------------- pandas openpyxl 读取 xlsx run 1: 11.2084s, rows99999 -------------------------------------------------- pandas xlrd 读取 xls run 1: 3.2561s, rows99999 ...不同机器上绝对数字会差很多不要照搬到自己的项目报告里。如果把测试放在机械硬盘、虚拟机、云服务器上结果差异会更大。5.2 相对快慢的一般规律虽然没法给出对所有机器都成立的绝对秒数但读取同样大小、同样结构的 10 万行数据时相对快慢通常会有以下趋势读取方式数据格式相对耗时说明csv 标准库csv最低作为基线纯文本无格式元数据xlrd 直接读取xls较低xls 解析相对直接pandas xlrdxls中等xlrd 快但 pandas 构建 DataFrame 有额外开销openpyxl 只读模式xlsx中等避免加载大量单元格对象和样式openpyxl 普通模式xlsx较高加载完整工作簿元数据pandas openpyxlxlsx较高openpyxl 慢加上 DataFrame 构建更慢简单说在 10 万行这个量级CSV 最快xlrd 读 xls 通常也不错openpyxl 读 xlsx 会明显感觉到慢而 pandas 本身并不会让读取变快它是在底层解析器之上加了一层 DataFrame 转换成本。如果你发现自己的测试结果排序不是这样优先检查几个地方是否用错文件格式。openpyxl 是否开了只读模式。机器内存是否充足有没有触发大量换页。是否在测试前没有预热第一次读取把模块初始化时间也算进去了。5.3 只看时间还不够读取耗时只能反映“把数据从磁盘解析到内存”的成本。实际开发里我们还要关注内存占用openpyxl 普通模式可能吃掉几个 GB只读模式则小很多。转换成本pandas 后续处理方便但先期构建 DataFrame 成本高。代码维护成本直接 openpyxl 循环代码往往比 pandas 长容易出错。所以下一节从原理上解释为什么有这么大差距。6. 耗时差异背后的原理6.1 xlsx 本质上是 zip 压缩包里的 XML很多人以为 xlsx 是一个“大表格文件”实际上用压缩软件打开 xlsx 文件会发现里面是一个目录结构data_100k.xlsx ├── [Content_Types].xml ├── _rels/.rels ├── docProps/app.xml ├── docProps/core.xml ├── xl/ │ ├── workbook.xml │ ├── _rels/workbook.xml.rels │ ├── styles.xml │ ├── theme/theme1.xml │ ├── worksheets/ │ │ └── sheet1.xml │ └── sharedStrings.xml真实数据主要存放在sheet1.xml和sharedStrings.xml中。解析 xlsx 的过程大约是解压或按需读取 zip 内容。解析多个 XML 文件。把 Excel 行列坐标映射成单元格对象。处理共享字符串、样式、数据类型、公式等。这一步比解析普通文本或二进制格式要重得多。openpyxl 普通模式还会把样式、列宽、合并单元格等全部组织成内存对象所以更慢。read_onlyTrue模式则想办法减少这些对象的创建尽量迭代读取。6.2 xls 是旧版二进制格式结构相对紧凑.xls格式是 Excel 97-2003 使用的 OLE2 复合文档二进制格式。数据以二进制流的方式存储在文件中不需要解析大量 XML 文本。xlrd解析这种二进制