pandas.read_excel在Python 3.12中变慢是因PIGIL启用而openpyxl等引擎未适配,导致锁争用;推荐改用calamine引擎、预分配chunk、指定dtype、按replace→fillna→astype→dropna顺序清洗,并用ProcessPoolExecutor多进程处理。

为什么 pandas.read_excel 在 Python 3.12 里变慢了?
Python 3.12 默认启用 per-interpreter GIL(PIGIL),但 openpyxl 和 xlrd 等底层引擎未完全适配,导致 pandas.read_excel 在多线程/多进程批量读取时反而出现锁争用或初始化延迟。这不是你代码写错了,而是引擎层兼容性滞后。
- 验证方法:在单文件读取时加
timeit,对比 Python 3.11 和 3.12 的read_excel(engine="openpyxl")耗时 - 临时解法:显式指定
engine="calamine"(需安装calamine-python)——它基于 Rust,不依赖 CPython GIL,且支持 Excel 2007+(.xlsx) - 注意:
calamine不支持公式计算、样式、图表,仅适合清洗场景(你本来也不需要这些)
用 pd.concat + chunking 替代循环 read_excel
逐个 read_excel 再 pd.concat 是常见写法,但在 Python 3.12 下容易触发内存碎片和 GC 频繁回收。更稳的路径是预分配 chunk size + 复用 DataFrame 结构。
- 避免
df = pd.concat([df, new_df])这种原地拼接——每次调用都复制整个 DataFrame - 改用列表收集:
dfs = [pd.read_excel(f, engine="calamine") for f in files],最后一次性pd.concat(dfs, ignore_index=True) - 若内存吃紧,用
chunksize=5000参数(仅对calamine和odf引擎有效),配合pd.concat流式合并 -
dtype务必提前指定,尤其对 ID 列(别让 pandas 自动推断成float64再转str)
replace / astype / dropna 的顺序影响清洗结果
顺序错会导致空值扩散或类型坍塌。比如先 astype("int") 再 dropna(),会把 np.nan 强转成 -2147483648(取决于平台);而先 replace("", np.nan) 再 dropna() 才安全。
Excel/WPS 公式大师。用大白话描述需求,直接给出可用的公式、函数组合和数据处理方案,并解释每一段的作用。当用户提到"Excel 公式"、"表格函数"、"VLOOKUP 怎么用"、"XLOOKUP"、"LET"、"LAMBDA"、"动态数组"、"这个公式报错了"、"#N/A"、"#REF!"、"
- 清洗链推荐顺序:
replace→fillna→astype→dropna(按需) -
replace优先用字典:df.replace({"A": {"N/A": None, "NULL": None}}),比正则快且可控 - 对数值列做
astype("Int64")(大写 I),它能保留pd.NA,比int64+np.nan更健壮 - 避免
df.dropna(how="all")后再reset_index(drop=True)——直接用df.dropna(how="all").reset_index(drop=True)减少中间对象
用 concurrent.futures.ProcessPoolExecutor 跑清洗任务
Python 3.12 的 PIGIL 让多线程收益下降,但多进程依然有效。关键是避免 pickle 开销过大——不要传整个 DataFrame 进子进程,而是传文件路径 + 清洗函数名。
立即学习“Python免费学习笔记(深入)”;
- 清洗函数必须定义在模块顶层(不能是 class method 或嵌套函数),否则
ProcessPoolExecutor无法序列化 - 示例结构:
def clean_one_file(filepath):
df = pd.read_excel(filepath, engine="calamine")
df = df.replace(...).astype(...).dropna(...)
return df
<h1>主流程</h1><p>with ProcessPoolExecutor(max_workers=4) as executor:
dfs = list(executor.map(clean_one_file, file_list))
result = pd.concat(dfs, ignore_index=True)if __name__ == "__main__": 下,否则 spawn 失败ThreadPoolExecutor 做 CPU 密集型清洗——在 3.12 下几乎没提速真正卡住的往往不是某一行代码,而是引擎选择与数据类型声明的组合。比如用 calamine 读取后没设 dtype,后续 astype("category") 会触发全量扫描;或者 dropna 前忘了 replace 空字符串。这些细节在小数据上无感,一到万行以上就暴露。

















