Python读取10万行Excel:常见库性能实测与选型指南
2026/9/5 2:01:12 网站建设 项目流程

在办公自动化项目里,经常能看到这样一条提问:“用 Python 读取一个包含 10 万行的 Excel 文件,到底会耗时多久?”提问者通常已经写过一个简单脚本,却经常得到互相矛盾的答案:有人觉得 openpyxl 慢到无法接受,有人说 pandas 只需要几秒,还有人说自己用 xlrd 读取时遇到了五花八门的报错。这些结论并不一定是错的,只是因为 Excel 解析耗时和文件格式、列数、单元格内容、机器配置、读取接口以及最终要拿到的对象都有关系,脱离这些条件只谈“10 万行”很难得到稳定结论。

这篇文章围绕“测试 Python 常见库读取 10 万行 Excel 耗时”这个主题展开,先说明读取耗时的构成,再构造一份可控的 10 万行测试文件,分别用 openpyxl、xlrd、pandas 等常见方式计时,并结合代码解释慢在哪里、省内存在哪里、误区和排查路径在哪里。最终你会得到一个可在自己电脑上重复运行的测试方案,而不是一个无法考证的固定秒数。

1. 先拆开“读取耗时”的含义:文件解码、对象构建与业务遍历

很多人测试 Excel 读取耗时,只关心这一句:“从开始运行到拿到数据用了多少秒。”但“拿到数据”这个目标本身并不统一。同样是读一个文件,有的脚本只需要统计总行数,有的脚本需要把整张工作表塞进 DataFrame 做后续统计,有的脚本必须逐行判断某些字段再做业务处理。这三种需求对解析库提出的要求完全不同,耗时也不会相同。

如果把读取过程拆开看,通常包含三个阶段:从磁盘把文件读入内存并解压;把工作簿或工作表解析成程序能理解的对象;调用 API 逐行、逐单元格或整表取数。前两个阶段更接近“库本身的开销”,第三个阶段则取决于你的业务代码写得是否合适。

1.1 “10 万行”能说明情况,但不是完整指标

实际项目中,常见的 Excel 文件不会只有 1 列。同样 10 万行数据,5 列和 50 列对应的单元格数量完全不同。解析单元格是最耗时、最容易受数据内容影响的工作,所以只报告“行数”会让测试结果失真。

判断一个测试样本是否接近真实业务,至少还要看几个维度:

维度为什么会影响耗时
列数单元格总数近似为“行数 × 列数”,列数越多,解析和对象构建量越大
单元格类型纯文本、数字、日期、公式的空值混合,解析逻辑不同
文件格式.xlsx是压缩包加 XML,.xls是旧二进制,.csv是纯文本
单元格样式字体、颜色、合并单元格、数据验证等会增加文件复杂度
公式读取公式表达式和读取计算的缓存值,结果和耗时可能不同
工作簿结构多张工作表、冻结窗格、图表、图片可能带来额外解析
是否保留空白行和空白列某些接口会把空行也遍历出来,影响行数统计

所以,设计基准测试时,应该尽量复刻真实业务里的列结构、字段类型和文件生产方式。自己用 Excel 手动保存的文件,和用脚本生成的同尺寸文件,在格式细节上也可能不同,测试结果不一定能互相套用。

1.2 常见库在读取链路中的位置不同

Python 读 Excel 时,常被提到的库包括 openpyxl、xlrd、pandas、pyxlsb 等。它们并不是完全平行的关系。

主要兼容格式读取特点
openpyxl.xlsx底层解析 XML,支持样式和公式;默认会把工作表内容整理成一个较大的对象
xlrd.xls早期的.xls专用读取库,新版不再支持.xlsx
pandas.xlsx.xls.csv本身不是解析器,而是调用 openpyxl、xlrd 等引擎,再把数据包装成 DataFrame
pyxlsb.xlsb面向二进制 Excel 文件,二进制格式解析速度通常有优势

理解这一点很关键。例如很多人在pandas.read_excel里指定引擎为openpyxl,那么 pandas 和 openpyxl 做了同一份 XML 解析工作,只是在 pandas 一侧又多了 DataFrame 构建成本。如果测试目的是“读取后立即进行 DataFrame 运算”,那这些成本必须计入;如果测试目的是“只想把单元格数据尽量快地带出来”,那直接用 openpyxl 的只读模式会更接近问题本质。

1.3 按“读入后要得到什么”决定计时口径

在写测试代码之前,先明确接口目标,能把很多无意义比较排除掉。常见目标可以分为三类:

  • 目标 A:只需要统计行数、核对文件完整性,不关心每行内容。
  • 目标 B:需要逐行处理,但同一时间只保留一行或少数几行。
  • 目标 C:希望把整张表读入内存,例如转换成 DataFrame 后做批量分析。

如果目标是 C,单纯比较 openpyxl 和 pandas 没有意义,因为最终需要 DataFrame,pandas 多出来的构建过程不是浪费,而是主流程的一部分。如果目标是 B,那么read_only模式或流式读取可能更合适,因为整表载入内存会让内存峰值和读取耗时双双上升。计时代码要把目标写清楚,否则结果会误导后续选型。

2. 造一份可控的 10 万行测试文件并锁定运行环境

要比较多个库的读取速度,前提是它们读取同一个文件。文件必须固定,不能每次运行前临时生成,否则文件生成时间会被混进读取时间。建议先运行生成脚本,把测试文件落盘保存,之后重复读取同一份文件。

2.1 文件结构与目录设计

建议建立独立目录,避免和业务脚本混在一起。下面是一个简单结构:

excel-bench/ ├── requirements.txt ├── build_data.py ├── bench_read.py ├── files/ │ ├── bench_100k_10cols.csv │ └── bench_100k_10cols.xlsx └── outputs/ └── result_record.md

build_data.py负责生成测试文件;bench_read.py负责计时读取;outputs用来保存运行环境和结果记录。

依赖文件保持精简:

pandas==2.0.3 openpyxl==3.1.2 xlrd==2.0.1

安装依赖:

pip install -r requirements.txt

要注意,pandas 默认不会自动安装 openpyxl,所以即使只使用pandas.read_excel,也需要显式安装 openpyxl。这个依赖问题本身就是项目里常见的坑,后续排错部分还会再提到。

2.2 生成 10 万行、10 列的 xlsx 和 csv

测试文件不要只有一列,也不要全部是同一类型字符串。用 10 列混合数据更接近真实场景:其中有文本编号、数值、日期、城市名、数量和金额等字段。

下面是生成脚本build_data.py。生成之后就保存为固定文件,不再每次重复修改:

import random from datetime import datetime, timedelta import pandas as pd random.seed(42) row_count = 100_000 start_date = datetime(2023, 1, 1) city_list = ["北京", "上海", "广州", "深圳", "杭州", "成都"] category_list = ["数码", "服饰", "食品", "图书", "日用"] remark_list = ["", "正常", "退款", "换货"] # 先独立生成各列,避免在 DataFrame 中逐行 append order_ids = [f"TXN{202400000 + i}" for i in range(row_count)] customer_ids = [random.randint(10000, 99999) for _ in range(row_count)] order_dates = [ (start_date + timedelta(days=random.randint(0, 500))).strftime("%Y-%m-%d") for _ in range(row_count) ] cities = [random.choice(city_list) for _ in range(row_count)] categories = [random.choice(category_list) for _ in range(row_count)] quantities = [random.randint(1, 5) for _ in range(row_count)] prices = [round(random.uniform(10.0, 5000.0), 2) for _ in range(row_count)] amounts = [round(q * p, 2) for q, p in zip(quantities, prices)] remarks = [random.choice(remark_list) for _ in range(row_count)] df = pd.DataFrame( { "订单号": order_ids, "客户ID": customer_ids, "成交日期": order_dates, "城市": cities, "品类": categories, "件数": quantities, "单价": prices, "金额": amounts, "是否退款": [1 if r == "退款" else 0 for r in remarks], "备注": remarks, } ) df.to_excel("files/bench_100k_10cols.xlsx", index=False) df.to_csv("files/bench_100k_10cols.csv", index=False) print("实际形状:", df.shape)

生成之后确认文件是否存在,并查看体积:

ls -lh files/

对 10 万行 10 列的数据来说,.csv文件通常很小,.xlsx文件会大一些,两者体积差本身也是性能差异的来源之一。生成这一步不会计入读取时间,因为文件已经落盘,后续测试全部复用这份文件。

2.3 记录 Python 和库版本信息

版本信息对复现非常重要。同一个库的不同版本可能更换了解析引擎,耗时也会变化。记录环境时,至少保留 Python 版本、关键库版本和文件体积三部分。

python -c "import sys; print(sys.version)" python -c "import pandas, openpyxl; print('pandas', pandas.__version__); print('openpyxl', openpyxl.__version__)" python -c "import xlrd; print('xlrd', xlrd.__version__)"

输出内容建议直接保存到outputs/result_record.md中。后续所有耗时对比都基于同一份文件和同一套环境,否则数据之间没有可比性。

3. 写计时脚本:逐库、逐模式测量

现在编写核心测试脚本bench_read.py。核心原则是:相同入口文件,相同计时函数,相同返回值形态。只有读取方式不同,这样最后的差异才能归因到库或模式上。

计时使用time.perf_counter()而不是time.time()perf_counter()是单调时钟,适合测量短时间间隔,不会受到系统时间调整的影响。

3.1 openpyxl 默认模式:整表载入后逐行取值

第一段代码是 openpyxl 默认加载方式。它会先把工作表解析成内存对象,再通过iter_rows遍历每一行。

import time import openpyxl def read_openpyxl_default(path): start = time.perf_counter() wb = openpyxl.load_workbook(path, data_only=True) ws = wb.active row_count = 0 for row in ws.iter_rows(values_only=True): row_count += 1 elapsed = time.perf_counter() - start return row_count, elapsed

这里要注意data_only=True。如果文件中有公式,且公式没有缓存值,这个参数会影响拿到的内容是公式表达式还是计算结果。测试文件里没有公式,所以实际影响不大,但在真实项目中要根据业务判断。

默认加载模式适合比较“读取并处理整张表”的场景。它把数据载入内存后再让代码遍历,所以iter_rows很快,真正的开销主要在load_workbook阶段。

3.2 openpyxl read_only 模式:按行流式解析

第二种方式是 openpyxl 的只读模式。使用read_only=True后,load_workbook不会一次性构建所有单元格对象,而是在遍历时逐步读取。

def read_openpyxl_readonly(path): start = time.perf_counter() wb = openpyxl.load_workbook(path, read_only=True, data_only=True) ws = wb.active row_count = 0 for row in ws.iter_rows(values_only=True): row_count += 1 elapsed = time.perf_counter() - start wb.close() return row_count, elapsed

在只读模式下,工作表对象不能随意进行写操作,也没有完整的内存表。它的典型价值是逐行处理大文件,让内存峰值保持稳定。计时时把wb.close()放在计时结束之后,避免遗漏资源释放过程对结果的影响。

3.3 xlrd 读取旧格式时的路径

xlrd 2.0 之后不再支持.xlsx,只支持.xls。如果测试文件是.xls,可以用类似下面的方式读取:

import xlrd def read_xls_by_xlrd(path): start = time.perf_counter() wb = xlrd.open_workbook(path) sheet = wb.sheet_by_index(0) row_count = sheet.nrows # 如果需要逐行确认解析结果,可以再遍历一次 for idx in range(sheet.nrows): _ = sheet.row_values(idx) elapsed = time.perf_counter() - start return row_count, elapsed

这里没有生成.xls文件,原因是.xls需要通过旧版 Excel 或转换工具生成,不是所有项目都有这个需要。如果你主要处理旧业务系统导入的.xls文件,可以把这份代码加入测试脚本,并把目标文件换成同名.xls

如果尝试用 xlrd 读取.xlsx,通常会出现类似zipfile.BadZipFile或格式不支持的报错。这不是代码写错,而是库的兼容范围变了。

3.4 pandas 统一入口 read_excel

pandas 的read_excel会把整个工作表读成 DataFrame。对于 10 万行数据,这个操作通常会占用较多内存,但拿到的对象方便后续统计。

import pandas as pd def read_pandas_excel(path): start = time.perf_counter() df = pd.read_excel(path, engine="openpyxl") row_count, col_count = df.shape # 防止 DataFrame 长期占用内存,计时后主动释放 del df elapsed = time.perf_counter() - start return row_count, elapsed

之所以显式传engine="openpyxl",是为了保证它确实使用 openpyxl 作为后端,而不是让 pandas 自行猜测。猜测过程有时会依赖其他库,导致环境不同时结果不稳定。

3.5 csv 对照与 runner 汇总

CSV 是纯文本,没有 XML 结构和样式,通常比同规模 xlsx 读取更快。把它作为对照,能帮助我们判断“慢在 Excel 压缩包解析”还是“慢在 Python 对象构建”。

def read_csv_with_pandas(path): start = time.perf_counter() df = pd.read_csv(path) row_count, col_count = df.shape del df elapsed = time.perf_counter() - start return row_count, elapsed

runner 统一调用四个函数,并输出结果。为了避免某个函数抛异常导致整个脚本中断,可以在外面统一捕获并打印错误信息。

def main(): xlsx_path = "files/bench_100k_10cols.xlsx" csv_path = "files/bench_100k_10cols.csv" cases = [ ("openpyxl_default", read_openpyxl_default, xlsx_path), ("openpyxl_readonly", read_openpyxl_readonly, xlsx_path), ("pandas_read_excel", read_pandas_excel, xlsx_path), ("pandas_read_csv", read_csv_with_pandas, csv_path), ] for name, func, path in cases: try: row_count, elapsed = func(path) print(f"{name}: {row_count} 行, 耗时 {elapsed:.4f} 秒") except Exception as exc: print(f"{name} 执行失败: {type(exc).__name__}: {exc}") if __name__ == "__main__": main()

执行:

python bench_read.py

此时得到的耗时是单次结果,不能直接作为最终结论。下面一节说明为什么单次结果不可靠,以及应该如何解读。

4. 代码与结果解读:读得快或慢通常卡在哪一层

四段函数看起来逻辑接近,底层路径差异却很大。理解这些差异,才知道测试结果里的大头在哪个阶段。

4.1 openpyxl 默认模式下慢的两个来源

在默认情况下,load_workbook会解压.xlsx,读取全部 XML,并构建一个包含所有单元格信息的 Workbook 对象。对 10 万行 10 列的文件来说,这相当于先创建了大约 100 万个单元格对应的结构。有些单元格还要处理类型判断、日期转换、字符串解析,所以耗时并不只是“读文件”的时间。

第二个来源是公式和样式信息。如果文件由业务系统导出,通常会带有很多默认样式、列宽、格式等信息。openpyxl 在设计上偏向保留 Excel 内容,因此不会只保存“纯数据”的极简结构。这也是它比 CSV 慢的重要原因之一。

4.2 read_only 是省内存,而不是必然更快

read_only=True最大的优势是内存占用可控。它不会把整张表都摊开在内存里,而是在iter_rows迭代过程中逐行解出数据。这个设计对几十万行甚至几百万行的文件很有价值,但它不一定比默认模式快。

如果测试目标是“必须把 100 万行数据全部拿来做汇总”,那么 read_only 模式只是把数据读出来的方式改变,最终仍然需要把数据保存到列表或 DataFrame 中,内存总量不会因此减少。如果目标是“扫描到某个条件就提前停止”,read_only 才真正受益,因为它不需要等待整个文件全部解析完。

用 10 万行测试时,可能会发现默认模式和 read_only 模式差异没有想象中那么大,因为文件规模还没有大到让内存分配成为瓶颈。这是正常现象。测试结果不能简单翻译成“read_only 就是更快”,而要结合最终业务目标判断。

4.3 pandas 多出的 DataFrame 构建成本是否值得

pd.read_excel的读取链路通常是“openpyxl 解析 XML -> pandas 再从解析结果构建 DataFrame”。也就是说,在 openpyxl 原本就要处理的 Excel 结构之外,pandas 还要经历索引对齐、列类型推断、对象复制等步骤。

因此,如果只比较“从文件读到 Python 并逐行数一遍”,pandas 不一定最有优势。可一旦需要聚合、筛选、分组,OpenPyXL 默认返回的“单元格值 + 循环”方式在业务代码里反而会带来更多自定义逻辑。测试时要先定义“完成某业务结果的时间”,而不是只测“文件解析接口的时间”。

4.4 CSV 对照为什么不能直接代表 Excel

CSV 没有工作表结构,没有单元格格式,没有 ZIP 解压步骤,也没有严格的类型声明。读取时可能只是一行一行的字符串切分。得到 CSV 比 XLSX 快很多,不能简单推出“Excel 太慢所以不该用 Excel”,而应该意识到:如果业务只是高频读取大型表格,CSV 或 Parquet 在性能和内存上确实有优势,但如果必须保留 Excel 格式并和业务人员协同,就不能脱离 Excel 格式谈替换方案。

5. 用可复现的流程比较耗时和内存

得到几个单次秒数之后,要把它整理成可复现的记录,否则下次机器换一个环境,数字就会变化。下面介绍一个相对规范的记录流程。

5.1 多次运行,取中位数而不是第一次

第一次运行可能受到文件缓存、CPU 频率、后台任务影响。建议同一个用例连续运行多次,例如每个方法执行 5 次,去掉最大和最小后取中位数。

可以把脚本外层再套一个循环:

import statistics def run_repeat(func, path, repeat=5): elapsed_list = [] last_row_count = 0 for _ in range(repeat): row_count, elapsed = func(path) last_row_count = row_count elapsed_list.append(elapsed) median_elapsed = statistics.median(elapsed_list) return last_row_count, median_elapsed

如果运行次数太少,结果容易受系统临时负载干扰;过多又会拉长等待时间。普通基准测试可以先选 5 次,数据波动明显时再增加到 10 次。

5.2 记录环境信息和输出格式

如果要把测试结果发布或分享,除了耗时,还要附上三项信息:

  • Python 解释器版本和操作系统。
  • pandas、openpyxl、xlrd 等库版本。
  • 测试文件的行数、列数、文件大小和单元格类型说明。

建议把这些信息写入一个可复现记录文件:

Python: 3.11.4 pandas: 2.0.3 openpyxl: 3.1.2 xlrd: 2.0.1 文件: files/bench_100k_10cols.xlsx 形状: 100000 行 × 10 列 文件大小: 以 ls -lh 输出为准 运行次数: 每个用例 5 次,取中位数

这样以后任何人在自己的机器上重跑,都能对比条件和结果差异。

5.3 内存测量的基本原则

时间并不是唯一指标。读取大文件时,如果内存占用过高,可能直接导致进程被系统杀掉。openpyxl 默认加载 10 万行 10 列通常可控,但如果是 100 万行或 50 列以上,内存问题会先于耗时问题出现。

在 Linux 或 macOS 上,可以用resource模块读取当前进程的峰值内存。下面代码只做简单说明,Windows 环境下建议改用psutil或直接观察系统监控:

import resource def current_max_rss_mb(): # Linux/macOS 下 ru_maxrss 单位是 KB return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024

需要注意,内存测量最好让每个库在独立进程中运行,否则前一个库已经占用的模块和对象不会释放干净。可以在 shell 里分别执行python bench_read.py openpyxl_default,再记录对应进程的内存。

内存展示建议用下降沿或峰值数值,不要只看任务管理器里的当前值,否则容易误判。

6. 常见耗时测试误区与排查方法

在真实项目里,耗时测试跑出奇怪结果,往往不是库本身的问题,而是测试代码把其他开销算进去了。

6.1 现象:逐行打印 10 万行,误以为读取很慢

有人会在读取循环里加一行print(row),打印 10 万行内容到控制台。无论读取本身多快,控制台输出都会变成主要开销。尤其在有大量中文和数字时,终端 I/O 可能比解析 Excel 本身还要慢很多。

检查方式:观察运行过程中是否为“打印速度”后行还是明显卡在读取阶段。

处理建议:测试时不要打印单元格内容,最多打印行数、列数和耗时。业务代码若确实需要逐行输出,要把输出到文件的成本和读取成本分开测量。

6.2 现象:xlrd 读取 .xlsx 报 BadZipFile

xlrd 2.0 之后只支持.xls,不支持.xlsx。报错信息可能是zipfile.BadZipFile: File is not a zip file,原因是代码把.xlsx.xls解析,但.xlsx本质是 ZIP 容器。

检查方式:查看文件扩展名、文件头格式、xlrd 版本。

处理建议:.xlsx用 openpyxl 或 pandas + openpyxl;.xls才使用 xlrd。如果项目里同时存在两种格式,要在代码里按后缀分流。

6.3 现象:read_only=True 后感觉并没有变快

read_only 模式并不承诺“让读取更快”。它主要在省内存和避免一次性创建完整单元格对象上起作用。当文件本身只有几 MB 时,两种模式的差距可能不明显,甚至会因为额外检查逻辑出现轻微差异。

检查方式:把文件扩充到更多行列,或改用大内存统计工具查看峰值。

处理建议:如果目标就是整表分析,默认模式或 pandas 可能更适合;如果是超大文件流式过滤,再选 read_only,不要只根据某一次“是否更快”做结论。

6.4 现象:pandas 读取结果行数少了,或日期变成数字

常见原因有两种。一种是文件里有空行,pandas 会跳过全空的数据行,但读取前的“跳过空白行”还不影响总行数判断;另一种是日期型单元格没有被正确识别,被当成时间戳或文本。

检查方式:在读取后输出df.head()和前五行的数据类型。

处理建议:测试文件要固定生成方式,不要用人工删除或补录过的 Excel。真实业务里还需要单独做字段类型修正,比如pd.to_datetimeastype

6.5 现象:多次运行耗时变化大

同一份文件连续读几次,耗时可能忽高忽低,常见原因包括操作系统文件缓存、磁盘繁忙、CPU 降频或后台进程抢占。

检查方式:连续运行 5 次并记录全部数值,观察中位数是否稳定。

处理建议:跑基准前先让电脑空闲,关闭浏览器或编译任务等大负载程序;时间采集用time.perf_counter();报告中明确写清运行次数和取数口径。

7. 测试后的选型建议与工程化扩展

跑完一轮测试,不要急着宣布“某个库最快”,要把结论转化为适合业务场景的选型判断。选型时可以参考下面的清单。

需求推荐方向说明
手头是.xlsx,只需逐行扫描部分数据openpyxl read_only平衡内存和代码复杂度
手头是.xlsx,需要整表统计和清洗pandas + openpyxl读取成本换来后续分析效率
手头是.xls旧文件xlrd确认库版本支持.xls,不读.xlsx
文件是 CSV,且数据量大pandas + read_csv没有 Excel XML 额外开销
需要完整保留 Excel 样式openpyxlpandas 不擅长样式处理
数据量大并要求长期高速读取不要长期存 Excel考虑分块 CSV、Parquet 或数据库

生产环境还需要考虑配置文件抽离、运行日志、异常处理和文件来源。比如在实际工时统计或报表导入任务中,输入文件可能来自不同系统,有的列名是“客户ID”,有的是“CustomerID”,还有的是“客户编号”。如果只对一个手工生成的样本做了基准测试,没有设计通用列名映射和样本校验,那么再快的解析方案也无法直接上线。

项目落地时可以分三步走:

  1. 先在测试小文件上做正确性验证,确认列名、数据类型、空值策略。
  2. 再用 10 万行样本做性能验证,得出当前机器的可用耗时基线。
  3. 最后用文件大小、行数、读取耗时三项指标做监控告警,防止未来某个来源文件突然膨胀拖垮接口。

对新手来说,最有价值的实践不是背下某个库的最优方案,而是掌握“面向一个可复现样本做测量”的方法。一次测试结果只能代表一份文件、一种机器、一个版本下的读取路径。把生成脚本、计时脚本、环境记录都保留在项目里,以后再遇到更大量级的数据时,只要重新跑一遍脚本,就能迅速判断该换文件格式、换库还是换架构。

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

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

立即咨询