做数据导入时,我拿到一份接近 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 / xls |
| openpyxl | 读写 xlsx 的专业库 | xlsx |
| xlrd | 老牌读取库,2.0 以后只支持 xls | xls |
| xlwt | 老牌写入库,配合 xlrd 使用 | xls |
| pyxlsb | 读取二进制 Excel 格式 | xlsb |
| csv 标准库 | 文本表格格式,不算 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 均可
- Python:3.10+
- 依赖库:pandas、openpyxl、xlrd、xlwt、pyxlsb
- 测试文件:同一份数据生成的 10 万行 xlsx / xls / csv
建议使用虚拟环境,避免污染全局环境:
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate2.2 安装依赖
创建一个requirements.txt:
pandas==2.2.2 openpyxl==3.1.5 xlrd==2.0.1 xlwt==1.3.0 pyxlsb==1.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 列数据,包含文本、数值、日期字符串,内容如下:
| 字段 | 示例值 | 类型 |
|---|---|---|
| id | 100001 | 数值 |
| name | 同事100001 | 文本 |
| dept | 部门1 | 文本 |
| position | 岗位1 | 文本 |
| salary | 12345 | 数值 |
| score | 78.5 | 小数 |
| entry_date | 2023-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(days=index)).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", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(HEADER) for i in range(TOTAL_ROWS): writer.writerow(build_row(i)) print(f"csv 生成完成: {file_path}") if __name__ == "__main__": generate_csv("data/data_100k.csv")这里把日期做成字符串再写入,后续读取时不需要额外解析时间,能更纯粹地比较库本身的解析性能。使用utf-8-sig编码,可以让 Excel 直接打开 CSV 时中文不乱码。
3.3 用 openpyxl 生成 xlsx 文件
生成 xlsx 时不要用普通模式一行一行append,否则 10 万行写入会非常慢。更好的是write_only=True,这种模式下 openpyxl 不会在内存中保留所有单元格对象,而是边写边落盘。
# generate_data.py 追加函数 from openpyxl import Workbook def generate_xlsx(file_path): workbook = Workbook(write_only=True) sheet = workbook.create_sheet("data") sheet.append(HEADER) for i in range(TOTAL_ROWS): sheet.append(build_row(i)) workbook.save(file_path) print(f"xlsx 生成完成: {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(encoding="utf-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(f"data_{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(f"xls 生成完成: {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", nrows=5) print(df.head())nrows=5表示只读前 5 行,不会加载完整文件,适合快速检查。
4. 各库读取耗时测试代码
4.1 测试框架封装
在benchmark_libs.py中,先定义统一的计时函数。为了让测试可重复,允许设置重复运行次数,默认每次读取一次,避免把测试时间拉得太长。
# benchmark_libs.py import csv import time TOTAL_ROWS = 100000 def measure(name, func, *args, repeat=1): 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(f"run {i + 1}: {cost:.4f}s, rows={last_rows}") if repeat > 1: avg_cost = sum(costs) / len(costs) print(f"avg: {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, engine="openpyxl") return df.shape[0]df.shape[0]表示 DataFrame 的行数。pandas 会自动把第一行作为表头,所以这行代码实际读取了 99999 条业务数据。严格来说和其它函数相差一行,但 10 万行里差一行对耗时影响可以忽略不计。
如果想手动指定列类型,可以减少后续类型推断的开销:
def read_pandas_xlsx_dtype(path): df = pd.read_excel( path, engine="openpyxl", dtype={"id": int, "salary": float}, ) return df.shape[0]不过类型推断本身也是读取耗时的一部分,这一步会影响测试结果。后面统一按默认方式测试即可。
4.3 pandas + xlrd 读取 xls
读取老式.xls文件时,指定engine="xlrd":
def read_pandas_xls(path): df = pd.read_excel(path, engine="xlrd") 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_only=True) sheet = workbook["data"] row_count = 0 for row in sheet.iter_rows(values_only=True): row_count += 1 workbook.close() return row_count这里data_only=True表示读取单元格缓存值而不是公式本身。普通模式下,load_workbook会把整个工作表的样式、列宽、合并单元格、数据类型等元数据都加载到内存,所以加载阶段比较耗时,遍历时反而很快。
4.5 openpyxl 只读模式读取 xlsx
openpyxl 提供了只读模式,适合处理大文件:
def read_openpyxl_readonly(path): workbook = load_workbook(path, read_only=True, data_only=True) sheet = workbook["data"] row_count = 0 for row in sheet.iter_rows(values_only=True): row_count += 1 workbook.close() return row_count两种模式的代码差异很小,只是多了一个read_only=True,但内部解析方式完全不同。只读模式不会一次把所有单元格对象加载到内存,而是按需迭代,所以内存占用更低,读取时间通常也更短。
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 标准库读取 csv
CSV 是纯文本格式,没有工作表、样式、公式等元数据,读取速度通常会快很多,作为参考基线:
def read_csv_by_stdlib(path): row_count = 0 with open(path, encoding="utf-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, rows=99999 -------------------------------------------------- == pandas + xlrd 读取 xls == run 1: 3.2561s, rows=99999 ...不同机器上绝对数字会差很多,不要照搬到自己的项目报告里。如果把测试放在机械硬盘、虚拟机、云服务器上,结果差异会更大。
5.2 相对快慢的一般规律
虽然没法给出对所有机器都成立的绝对秒数,但读取同样大小、同样结构的 10 万行数据时,相对快慢通常会有以下趋势:
| 读取方式 | 数据格式 | 相对耗时 | 说明 |
|---|---|---|---|
| csv 标准库 | csv | 最低,作为基线 | 纯文本,无格式元数据 |
| xlrd 直接读取 | xls | 较低 | xls 解析相对直接 |
| pandas + xlrd | xls | 中等 | xlrd 快,但 pandas 构建 DataFrame 有额外开销 |
| openpyxl 只读模式 | xlsx | 中等 | 避免加载大量单元格对象和样式 |
| openpyxl 普通模式 | xlsx | 较高 | 加载完整工作簿元数据 |
| pandas + openpyxl | xlsx | 较高 | 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_only=True模式则想办法减少这些对象的创建,尽量迭代读取。
6.2 xls 是旧版二进制格式,结构相对紧凑
.xls格式是 Excel 97-2003 使用的 OLE2 复合文档二进制格式。数据以二进制流的方式存储在文件中,不需要解析大量 XML 文本。xlrd解析这种二进制