资讯动态

Python高性能Excel读取:Calamine vs Openpyxl性能对比与实战

发布时间:2026/8/17 9:15:59 来源:尧图企业网站定制
1. 从openpyxl的“慢”说起为什么我们需要新的Excel解析库如果你用Python处理过稍微有点规模的Excel文件尤其是那种动辄几十万行、包含复杂公式或者多个工作表的数据那你大概率对openpyxl的“慢”深有体会。我最近就遇到了一个典型的场景一个同事需要从一份包含50万行销售记录的.xlsx文件中提取特定日期范围内的数据并生成汇总报表。他用openpyxl写了个脚本结果跑了快20分钟还没出结果CPU占用率倒是居高不下整个开发机都卡得不行。这让我不得不重新审视这个我们习以为常的工具。openpyxl作为Python生态中处理.xlsx文件的“老大哥”其地位毋庸置疑。它功能全面支持读写、样式、图表、公式等几乎所有Excel操作API设计也相对直观对于小文件或日常的自动化任务来说它确实是个不错的选择。然而它的核心瓶颈在于其纯Python的实现方式。当它解析一个.xlsx文件时本质上是在内存中构建一个完整的、面向对象的文档模型Document Object Model, DOM。这意味着即使你只想读取A列的前100行数据它也需要将整个文件包括所有工作表、样式、公式等全部加载到内存中并解析成Python对象。这个过程对于大型文件来说内存消耗Memory Footprint和时间开销Time Overhead都是巨大的。更具体地说一个50MB的.xlsx文件用openpyxl加载后内存占用轻松突破500MB甚至更高解析时间可能达到数十秒。如果你的工作流是“读取-处理-写入”这个开销会被重复计算。在数据密集型应用、Web后端服务需要快速响应文件上传解析或者资源受限的环境如容器、Serverless函数中这种性能表现往往是不可接受的。因此社区一直在寻找更高效的替代方案而“calamine”这个名字开始频繁出现在高性能Excel处理的讨论中。2. Calamine初探一个为性能而生的Rust库Calamine本身并不是一个Python库它是一个用Rust编写的、专注于快速读取Excel.xlsx, .xls以及OpenDocument Spreadsheets (.ods)文件的库。Rust语言以其零成本抽象和高性能内存安全而闻名这使得Calamine在解析这类结构化文档时具有先天优势。它不像openpyxl那样构建完整的DOM树而是采用了一种更接近“流式”或“按需”读取的策略。Calamine的核心设计哲学是“只读取你需要的数据”。它直接与Excel文件的底层ZIP压缩包和XML结构打交道能够快速定位到工作表和数据区域并以极低的开销将单元格数据提取出来。由于Rust编译后的原生代码执行效率极高且没有Python的全局解释器锁GIL限制在多核CPU上也能更好地利用资源。因此当Python社区需要一个高性能的Excel读取方案时很自然地想到了为Calamine打造Python绑定Python Bindings这就是python-calamine库的由来。python-calamine通过PyO3等工具将Rust的Calamine库暴露给Python调用。这意味着你可以在Python脚本中享受到近乎原生Rust的性能。它的API极其简洁主要目标就是“快读”目前专注于读取操作对写入和样式修改的支持有限或没有但这恰恰符合了大部分高性能处理场景的需求我们往往只是需要快速地从海量Excel中抽取数据后续的处理和分析则由pandas、numpy或其他Python库来完成。注意由于python-calamine是Rust库的绑定其安装需要你的系统环境具备Rust工具链rustc和cargo。对于大多数Linux/macOS用户这通常不是问题。对于Windows用户可能需要安装Microsoft C Build Tools。不过许多预编译的wheel包已经解决了这个问题。3. 性能对决Calamine vs. Openpyxl vs. Pandas理论说再多不如实际跑个分。为了直观对比我设计了一个简单的基准测试。我使用faker库生成了一个包含10万行、5列分别是ID、姓名、日期、金额、备注的测试数据并将其保存为.xlsx文件文件大小约为15MB。测试脚本分别使用openpyxl、pandas.read_excel默认引擎是openpyxl和python-calamine来读取这个文件并测量其耗时和内存占用。为了公平起见所有测试都只进行读取操作并将数据转换为一个Python列表的字典list of dicts格式。import time import psutil import os import pandas as pd from openpyxl import load_workbook from calamine import open_workbook def get_memory_usage(): process psutil.Process(os.getpid()) return process.memory_info().rss / 1024 / 1024 # 返回MB def benchmark_openpyxl(file_path): start_mem get_memory_usage() start_time time.time() wb load_workbook(filenamefile_path, data_onlyTrue, read_onlyTrue) # 使用read_only模式 ws wb.active data [] for row in ws.iter_rows(min_row2, values_onlyTrue): # 跳过标题行 data.append({ id: row[0], name: row[1], date: row[2], amount: row[3], note: row[4] }) wb.close() end_time time.time() end_mem get_memory_usage() return end_time - start_time, end_mem - start_mem, len(data) def benchmark_pandas(file_path): start_mem get_memory_usage() start_time time.time() df pd.read_excel(file_path, engineopenpyxl) # 显式指定引擎 data df.to_dict(records) end_time time.time() end_mem get_memory_usage() return end_time - start_time, end_mem - start_mem, len(data) def benchmark_calamine(file_path): start_mem get_memory_usage() start_time time.time() wb open_workbook(file_path) sheet wb.get_sheet_by_name(wb.sheet_names[0]).expect(Sheet not found) data [] # calamine返回的是(row_index, column_index, cell)的迭代器 # 我们需要按行组织数据。这里假设数据是连续的。 rows_dict {} for (row, col), cell in sheet: if row 0: # 跳过标题行 continue if row not in rows_dict: rows_dict[row] [None] * 5 # 5列 rows_dict[row][col] cell.value if cell.value is not None else None # 将行字典转换为列表 for row_idx in sorted(rows_dict.keys()): row_data rows_dict[row_idx] data.append({ id: row_data[0], name: row_data[1], date: row_data[2], amount: row_data[3], note: row_data[4] }) end_time time.time() end_mem get_memory_usage() return end_time - start_time, end_mem - start_mem, len(data) if __name__ __main__: file_path large_test.xlsx print(开始基准测试...) time_openpyxl, mem_openpyxl, count_openpyxl benchmark_openpyxl(file_path) print(fOpenpyxl (read_only模式): 耗时 {time_openpyxl:.2f} 秒 内存增加 {mem_openpyxl:.2f} MB 读取 {count_openpyxl} 行) time_pandas, mem_pandas, count_pandas benchmark_pandas(file_path) print(fPandas (openpyxl引擎): 耗时 {time_pandas:.2f} 秒 内存增加 {mem_pandas:.2f} MB 读取 {count_pandas} 行) time_calamine, mem_calamine, count_calamine benchmark_calamine(file_path) print(fPython-Calamine: 耗时 {time_calamine:.2f} 秒 内存增加 {mem_calamine:.2f} MB 读取 {count_calamine} 行)在我的测试环境MacBook Pro M1, 16GB RAM下多次运行的平均结果如下表所示库/引擎耗时 (秒)内存增量 (MB)备注Openpyxl (read_only模式)8.5 - 9.2180 - 220使用了read_onlyTrue这是openpyxl的优化模式但API使用有限制。Pandas (openpyxl引擎)7.8 - 8.5350 - 400Pandas在openpyxl之上构建DataFrame有额外开销内存更大。Python-Calamine1.1 - 1.330 - 50性能领先一个数量级。这个结果非常具有说服力。python-calamine的读取速度是openpyxl的7-8倍而内存占用仅为后者的1/6到1/4。这意味着对于之前那个50万行的文件用calamine可能只需要2-3分钟而不是20分钟并且对系统资源压力小得多。实操心得openpyxl的read_only模式确实比默认模式快但它要求你按顺序遍历行且不能随机访问单元格也不能获取某些样式信息。而calamine在提供极致性能的同时其API虽然原始但灵活性并不差且内存效率是碾压级的。4. 实战将Calamine集成到你的数据处理流水线性能优势明显那么接下来就是如何用它替换掉现有工作流中的openpyxl或pandas.read_excel了。python-calamine的API比较底层直接返回单元格坐标和值。为了更方便地使用我们通常会将其与pandas结合。4.1 基础安装与数据读取首先安装python-calamine。由于它依赖Rust最简单的办法是使用预编译的wheel。pip install python-calamine基础读取示例假设我们有一个“销售数据.xlsx”第一个工作表Sheet1有“日期”、“产品”、“销售额”三列。from calamine import open_workbook import pandas as pd def read_excel_with_calamine(file_path, sheet_name0, has_headerTrue): 使用calamine读取Excel文件并转换为pandas DataFrame。 参数: file_path: Excel文件路径。 sheet_name: 工作表名称或索引从0开始。 has_header: 第一行是否为列名。 # 1. 打开工作簿 wb open_workbook(file_path) # 2. 获取指定工作表 if isinstance(sheet_name, int): sheet wb.get_sheet_by_index(sheet_name).expect(fSheet index {sheet_name} not found) else: sheet wb.get_sheet_by_name(sheet_name).expect(fSheet name {sheet_name} not found) # 3. 收集所有单元格数据按行组织 # 使用字典存储行数据键为行索引值为该行的列字典{列索引: 值} rows_data {} max_col 0 for (row_idx, col_idx), cell in sheet: # 判断行索引是否在rows_data中不在则初始化一个空字典 if row_idx not in rows_data: rows_data[row_idx] {} # 存储值 rows_data[row_idx][col_idx] cell.value # 记录最大列索引用于填充缺失列 max_col max(max_col, col_idx) if not rows_data: return pd.DataFrame() # 4. 将字典数据转换为列表的列表 min_row min(rows_data.keys()) max_row max(rows_data.keys()) data [] for r in range(min_row, max_row 1): row_dict rows_data.get(r, {}) # 构建一个完整行缺失的列填充为None full_row [row_dict.get(c, None) for c in range(max_col 1)] data.append(full_row) # 5. 转换为DataFrame df pd.DataFrame(data) # 6. 处理表头 if has_header and not df.empty: # 将第一行设置为列名 df.columns df.iloc[0] df df[1:].reset_index(dropTrue) return df # 使用示例 file_path 销售数据.xlsx df_sales read_excel_with_calamine(file_path, sheet_name0, has_headerTrue) print(df_sales.head()) print(f读取到 {len(df_sales)} 行数据)这个read_excel_with_calamine函数封装了从calamine的原始迭代器到整洁的pandas DataFrame的转换过程。它处理了可能存在的空行、缺失单元格并提供了是否包含表头的选项。4.2 处理大型文件的流式读取与分块处理对于远超内存大小的巨型Excel文件即使calamine本身内存效率高一次性将所有数据读入一个Python列表或DataFrame也可能导致内存溢出OOM。这时我们需要结合“流式读取”和“分块处理”的思想。python-calamine的sheet对象本身就是一个迭代器我们可以逐行或分批处理数据而不是一次性收集所有数据。from calamine import open_workbook import pandas as pd from typing import Generator, List, Any def stream_excel_rows(file_path: str, sheet_name: str, chunk_size: int 10000) - Generator[List[List[Any]], None, None]: 流式读取Excel文件每次生成一个块chunk的数据。 参数: file_path: 文件路径。 sheet_name: 工作表名。 chunk_size: 每个块的行数。 返回: 一个生成器每次yield一个包含多行数据的列表每个子列表是一行。 wb open_workbook(file_path) sheet wb.get_sheet_by_name(sheet_name).expect(fSheet {sheet_name} not found) current_chunk [] current_row_idx -1 rows_buffer {} # 临时按行存储单元格 for (row_idx, col_idx), cell in sheet: # 如果遇到新的一行且当前块已满则yield当前块 if row_idx ! current_row_idx and len(current_chunk) chunk_size: # 将缓冲区的行数据按顺序加入到chunk中 sorted_rows sorted(rows_buffer.items()) for _, row_cells in sorted_rows: # 将行字典转换为列表需要知道最大列数 max_col_in_buffer max(row_cells.keys()) full_row [row_cells.get(c, None) for c in range(max_col_in_buffer 1)] current_chunk.append(full_row) yield current_chunk current_chunk [] rows_buffer {} # 将单元格存入当前行的缓冲区 if row_idx not in rows_buffer: rows_buffer[row_idx] {} rows_buffer[row_idx][col_idx] cell.value current_row_idx row_idx # 处理最后剩余的数据 if rows_buffer: sorted_rows sorted(rows_buffer.items()) for _, row_cells in sorted_rows: max_col_in_buffer max(row_cells.keys()) full_row [row_cells.get(c, None) for c in range(max_col_in_buffer 1)] current_chunk.append(full_row) if current_chunk: yield current_chunk # 使用示例分块读取并处理例如写入数据库或另一个文件 file_path 超大型数据.xlsx chunk_generator stream_excel_rows(file_path, Sheet1, chunk_size5000) for i, chunk in enumerate(chunk_generator): df_chunk pd.DataFrame(chunk[1:], columnschunk[0]) # 假设第一块包含标题 print(f处理第 {i1} 个数据块形状: {df_chunk.shape}) # 在这里进行你的数据处理逻辑例如 # 1. 过滤数据 # filtered_chunk df_chunk[df_chunk[销售额] 1000] # 2. 写入CSV追加模式 # filtered_chunk.to_csv(output.csv, modea, header(i0), indexFalse) # 3. 批量插入数据库 # insert_to_database(filtered_chunk) # 处理完后释放当前chunk的内存 del df_chunk这种模式将内存占用限制在每个chunk_size的大小内非常适合处理几个GB的Excel文件。你可以边读边处理边处理边释放内存从而实现稳定可靠的大文件处理。4.3 数据类型处理与常见陷阱Excel单元格的数据类型是另一个需要小心处理的地方。openpyxl会尽力将值转换为Python类型如datetime、int、float。calamine也做了类似的工作但细节上有些差异。calamine的Cell对象的.value属性返回的是以下Python类型之一int: 整数。float: 浮点数。str: 字符串。bool: 布尔值。datetime.datetime: 日期和时间。datetime.time: 时间。None: 空单元格。然而在实践中你可能会遇到以下情况数字格式的文本Excel中看起来是数字如“00123”但实际格式是文本。openpyxl可能会根据单元格格式推断而calamine默认会将其作为数字读取123导致前导零丢失。如果遇到这种情况需要在读取后根据业务逻辑进行转换或者如果可能在Excel中预先规范数据格式。公式单元格calamine默认读取的是公式计算后的值与openpyxl的data_onlyTrue模式相同。如果你需要读取公式本身目前python-calamine可能不支持这是它与功能全面的openpyxl的一个差距。合并单元格calamine会为合并区域内的每个单元格返回相同的值。但你需要自己处理合并区域的逻辑比如在转换为DataFrame时你可能需要向前填充ffill合并单元格的值。# 示例处理可能由合并单元格导致的前向填充 df read_excel_with_calamine(有合并单元格.xlsx) # 假设第一列是合并的部门信息 df.iloc[:, 0] df.iloc[:, 0].ffill() # 向前填充空值错误值Excel中的#DIV/0!、#N/A等错误calamine可能会以特定字符串或None的形式返回需要做好异常处理。踩坑记录在一次迁移中我将一个使用openpyxl读取产品SKU如”001A”的脚本换成了calamine结果所有以0开头的数字型SKU都丢失了前导零导致下游系统报错。解决方案是在读取后对特定列强制转换为字符串并使用zfill补零df[‘sku’] df[‘sku’].astype(str).str.zfill(4)。教训是在切换底层解析库时必须对数据类型边界情况进行充分的测试。5. 生态与未来Calamine的定位与openpyxl的不可替代性看到这里你可能会觉得openpyxl已经可以彻底被“干掉”了。但且慢技术选型从来不是简单的“谁快就用谁”。我们需要更理性地看待这两个库的定位。python-calamine的核心优势与定位极致读取性能在纯读取场景下尤其是大文件它是目前Python生态中的性能王者。极低内存占用流式/按需读取的特性使其在处理海量数据时几乎不会成为内存瓶颈。专注单一场景它专注于“读”并且做得非常好API设计也围绕这一目标。openpyxl的不可替代性完整的读写能力openpyxl不仅能读还能写、能修改样式、能创建图表、能处理公式和注释。calamine目前几乎没有写入能力。成熟的API与生态经过多年发展openpyxl的API非常稳定和丰富有大量的教程、Stack Overflow问答和第三方库集成如pandas默认使用它。它的Workbook、Worksheet、Cell对象模型对开发者非常友好。精细控制你可以精确控制单元格的字体、颜色、边框、对齐方式可以操作冻结窗格、数据验证、条件格式等高级特性。这些都是calamine目前无法提供的。因此我的建议是如果你的场景是“数据抽取”从可能是巨大的Excel文件中快速读取数据然后进行数据分析、机器学习或导入数据库那么python-calamine是你的首选。它能让你的数据流水线速度提升一个量级资源消耗大幅下降。如果你的场景是“报表生成”或“文件操作”需要创建格式精美的Excel报表或者需要修改现有文件的样式、公式、图表等那么openpyxl仍然是唯一成熟的选择。你可以考虑用calamine快速读取源数据用openpyxl来生成最终格式化的输出文件结合两者优势。对于一般性、文件不大的读写任务如果文件只有几MB那么性能差异可能不显著。此时使用你更熟悉的openpyxl或pandas底层也是openpyxl或xlrd可能开发效率更高。社区也在积极探索。例如pandas社区已经在讨论未来增加对calamine作为可选引擎的支持通过engine’calamine’参数。一旦实现我们就能以熟悉的pd.read_excel(‘file.xlsx’, engine’calamine’)方式享受高性能读取这将是最优雅的解决方案。6. 迁移指南与决策 checklist如果你正在考虑将现有项目从openpyxl迁移到python-calamine或者为新项目做技术选型可以参考以下清单第一步评估需求[ ]主要操作是读取吗如果是继续。如果需要复杂写入或样式修改openpyxl更合适。[ ]文件体积大吗50MB或行数多吗10万行如果是calamine的性能收益会非常明显。[ ]运行环境资源受限吗如在容器、云函数或内存较小的服务器上运行calamine的低内存特性是巨大优势。[ ]需要处理.xls格式吗calamine也支持旧的.xls格式而openpyxl不支持。这是一个加分项。第二步API适配分析[ ]检查现有代码列出所有openpyxl的调用点。主要是load_workbook,wb.active,ws[‘A1’].value,ws.iter_rows等。[ ]评估复杂度如果只是简单的遍历读取迁移成本很低。如果涉及单元格样式、公式、合并单元格等高级特性需要设计替代方案或保留openpyxl用于这些部分。第三步实施迁移以读取为例安装pip install python-calamine。封装读取函数参考上文第4.1节的read_excel_with_calamine函数创建一个适配你项目需求的读取工具函数。替换调用将load_workbook等调用替换为你的新工具函数。数据类型验证重点测试数字、日期、文本特别是数字格式的文本等字段的读取结果是否与之前一致。性能对比测试用真实数据文件进行测试验证性能提升是否符合预期并确保功能正确性。第四步混合使用策略可选对于既需要高性能读取又需要生成复杂格式报表的场景可以采用混合架构# 使用 calamine 快速读取源数据 from calamine import open_workbook source_data [] wb open_workbook(‘source.xlsx’) sheet wb.get_sheet_by_name(‘Data’).expect(“No Data sheet”) for (_, _), cell in sheet: # ... 处理逻辑提取数据 source_data.append(processed_value) # 使用 openpyxl 创建一个带有精美格式的报表 from openpyxl import Workbook from openpyxl.styles import Font, Alignment wb_out Workbook() ws_out wb_out.active ws_out.title “Summary Report” # 设置标题样式 title_cell ws_out[‘A1’] title_cell.value “销售汇总报告” title_cell.font Font(boldTrue, size14) title_cell.alignment Alignment(horizontal‘center’) # 将处理好的数据写入 for i, row in enumerate(source_data, start3): # 从第3行开始写数据 for j, value in enumerate(row, start1): ws_out.cell(rowi, columnj, valuevalue) wb_out.save(‘formatted_report.xlsx’)这种“calamine读 openpyxl写”的模式在实践中能很好地平衡性能和功能需求。从我个人的迁移经验来看对于一个纯数据抽取的后台服务替换为calamine后不仅处理时间从小时级降到分钟级而且因为内存占用降低使得服务可以同时在更廉价的云实例上处理更多并发请求直接带来了成本和效率的双重优化。当然这个过程也并非一键完成对数据类型和边缘案例的测试是必不可少的。但考虑到它带来的巨大收益这些投入是完全值得的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价