Python 读取 Excel 性能实测:10万行数据如何选库?
2026/9/5 22:27:09 网站建设 项目流程

做数据导入时,我拿到一份接近 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 以后只支持 xlsxls
xlwt老牌写入库,配合 xlrd 使用xls
pyxlsb读取二进制 Excel 格式xlsb
csv 标准库文本表格格式,不算 Excel 但常作为对比基线csv

一个很容易踩坑的认知是:pandas本身并没有实现 Excel 解析,它调用的是openpyxlxlrd等底层引擎。所以“pandas 一定比 openpyxl 快”这个说法并不成立,甚至因为 pandas 需要额外构建 DataFrame,反而会更慢一些。

1.3 本文测试的目标

本文要回答几个问题:

  1. 同样一份 10 万行 Excel,不同库读取耗时差多少。
  2. pandas 的read_excel在不同引擎下表现如何。
  3. openpyxl 普通模式和只读模式差距大不大。
  4. 结合使用场景,应该怎么选。

测试重点是“读取整张表并遍历数据”的耗时,不包含后续逻辑处理。

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\activate

2.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 列数据,包含文本、数值、日期字符串,内容如下:

字段示例值类型
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(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_count

pyxlsb的 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 + 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.xmlsharedStrings.xml中。解析 xlsx 的过程大约是:

  1. 解压或按需读取 zip 内容。
  2. 解析多个 XML 文件。
  3. 把 Excel 行列坐标映射成单元格对象。
  4. 处理共享字符串、样式、数据类型、公式等。

这一步比解析普通文本或二进制格式要重得多。openpyxl 普通模式还会把样式、列宽、合并单元格等全部组织成内存对象,所以更慢。read_only=True模式则想办法减少这些对象的创建,尽量迭代读取。

6.2 xls 是旧版二进制格式,结构相对紧凑

.xls格式是 Excel 97-2003 使用的 OLE2 复合文档二进制格式。数据以二进制流的方式存储在文件中,不需要解析大量 XML 文本。xlrd解析这种二进制

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询