300MB 的 Excel 打不开?用 Python 把它拆成 N 个小文件(附完整代码)
- 2026-09-22 08:39:21
一个真实场景:同事甩来一个 300MB 的 Excel,双击之后电脑风扇起飞,十分钟后你选择了重启。
一、先说痛点
做数据的朋友大概都遇到过这种时刻:
业务方发来一个 几十万行、几百 MB 的 Excel,要求你"按部门拆成 20 个文件";
你想用 pandas 读一下,
pd.read_excel()跑了五分钟,内存直接爆掉;用 Excel 自带的"筛选 → 复制 → 粘贴"手工拆,拆到第三个文件就想辞职。
今天这篇,就用 100 行左右的 Python 代码,把这件事彻底解决掉。
而且我会给你两个版本:
按行数拆:每 5 万行一个文件,适合"文件太大要分片"的场景;
按列的值拆:按"部门""地区""供应商"等字段拆成多个文件,适合"业务方要分表"的场景。
二、为什么不能用 pandas?
很多人第一反应是:
import pandas as pddf = pd.read_excel("big.xlsx")
这个思路在大文件上会直接翻车。
原因很简单:.xlsx 本质上是一个 zip 压缩包,里面是一堆 XML。pandas 读取时会:
把整个 zip 解压;
把全部 XML 解析成 Python 对象;
再构建成 DataFrame。
一个 300MB 的 xlsx,解压后的 XML 可能有 3~5 GB,再经过 Python 对象膨胀,内存占用轻松上到 10GB+。
所以正确的姿势是两个字:流式。
读一行、写一行、丢掉一行。内存占用只跟"一个文件"有关,跟"总数据量"无关。
Python 里干这件事的最佳工具是 openpyxl 的两个模式:
| 模式 | 参数 | 作用 |
|---|---|---|
| 只读模式 | read_only=True | 不把整表加载进内存,逐行迭代 |
| 只写模式 | Workbook(write_only=True) | 边写边落盘,不在内存里攒数据 |
三、完整代码
新建一个文件 split_excel.py,直接复制下面全部代码:
# -*- coding: utf-8 -*-"""大 Excel 文件拆分工具依赖:pip install openpyxl用法:# 按行数拆,每 5 万行一个文件python split_excel.py big.xlsx -o out rows -n 50000# 按"部门"这一列的值拆python split_excel.py big.xlsx -o out cols -c 部门"""import osimport reimport timeimport argparsefrom openpyxl import load_workbook, Workbook# ---------------------------------------------------------------# 公共工具# ---------------------------------------------------------------def _new_writer(header):"""创建一个只写模式的工作簿,并写入表头"""wb = Workbook(write_only=True)ws = wb.create_sheet("Sheet1")ws.append(header)return wb, wsdef _safe_filename(name):"""把不能做文件名的字符替换掉"""name = str(name).strip()name = re.sub(r'[\\/:*?"<>|\[\]\r\n\t]', "_", name)name = name.strip(". ") or "空白"return name[:80] # 防止文件名过长# ---------------------------------------------------------------# 方案一:按行数拆分# ---------------------------------------------------------------def split_by_rows(src_file, out_dir, rows_per_file=50000, prefix="part"):os.makedirs(out_dir, exist_ok=True)# 只读模式打开,内存占用极低src_wb = load_workbook(src_file, read_only=True, data_only=True)src_ws = src_wb.activerow_iter = src_ws.iter_rows(values_only=True)try:header = next(row_iter)except StopIteration:src_wb.close()print("[!] 源文件是空的")return []out_files = []part_no = 1count = 0has_data = Falseout_wb, out_ws = _new_writer(header)for row in row_iter:out_ws.append(row)count += 1has_data = True# 攒够一个文件就落盘if count % rows_per_file == 0:path = os.path.join(out_dir, f"{prefix}_{part_no}.xlsx")out_wb.save(path)out_files.append(path)print(f" ✔ {os.path.basename(path):<20} 累计 {count} 行")part_no += 1out_wb, out_ws = _new_writer(header)has_data = False# 处理最后一批(避免正好整除时多生成一个空文件)if has_data:path = os.path.join(out_dir, f"{prefix}_{part_no}.xlsx")out_wb.save(path)out_files.append(path)print(f" ✔ {os.path.basename(path):<20} 累计 {count} 行")src_wb.close()return out_files# ---------------------------------------------------------------# 方案二:按某一列的值拆分# ---------------------------------------------------------------def split_by_column(src_file, out_dir, key_col, sheet_name=None):os.makedirs(out_dir, exist_ok=True)src_wb = load_workbook(src_file, read_only=True, data_only=True)src_ws = src_wb[sheet_name] if sheet_name else src_wb.activerow_iter = src_ws.iter_rows(values_only=True)try:header = next(row_iter)except StopIteration:src_wb.close()print("[!] 源文件是空的")return []# 定位分组列if isinstance(key_col, int):key_idx = key_colelse:if key_col not in header:src_wb.close()raise ValueError(f"找不到列 [{key_col}],实际列名:{header}")key_idx = header.index(key_col)writers = {} # 分组值 -> (workbook, worksheet)counts = {} # 分组值 -> 行数for row in row_iter:raw = row[key_idx]key = "空白" if raw is None or str(raw).strip() == "" else str(raw).strip()if key not in writers:wb, ws = _new_writer(header)writers[key] = (wb, ws)counts[key] = 0writers[key][1].append(row)counts[key] += 1src_wb.close()# 统一落盘out_files = []used_names = set()for key, (wb, _) in writers.items():base = _safe_filename(key)name = basei = 1while name in used_names: # 防止清洗后重名i += 1name = f"{base}_{i}"used_names.add(name)path = os.path.join(out_dir, f"{name}.xlsx")wb.save(path)out_files.append(path)print(f" ✔ {name + '.xlsx':<24}{counts[key]:>8} 行")return out_files# ---------------------------------------------------------------# 命令行入口# ---------------------------------------------------------------def main():parser = argparse.ArgumentParser(description="大 Excel 文件拆分工具")parser.add_argument("src", help="源 Excel 文件路径")parser.add_argument("-o", "--out", default="output", help="输出目录(默认 output)")sub = parser.add_subparsers(dest="mode", required=True)p1 = sub.add_parser("rows", help="按行数拆分")p1.add_argument("-n", "--rows", type=int, default=50000, help="每个文件的行数")p1.add_argument("-p", "--prefix", default="part", help="文件名前缀")p2 = sub.add_parser("cols", help="按某列的值拆分")p2.add_argument("-c", "--col", required=True, help="用于分组的列名或列序号")p2.add_argument("-s", "--sheet", default=None, help="工作表名(默认第一个)")args = parser.parse_args()t0 = time.time()print(f"[*] 开始处理:{args.src}")if args.mode == "rows":files = split_by_rows(args.src, args.out, args.rows, args.prefix)else:files = split_by_column(args.src, args.out, args.col, args.sheet)print(f"\n[√] 完成!共生成 {len(files)} 个文件,"f"耗时 {time.time() - t0:.1f} 秒,输出目录:{args.out}")if __name__ == "__main__":main()
四、怎么用
场景 1:文件太大,按行切
# 每 5 万行切一个文件python split_excel.py 订单明细.xlsx -o out_rows rows -n 50000
[*] 开始处理:订单明细.xlsx✔ part_1.xlsx 累计 50000 行✔ part_2.xlsx 累计 100000 行✔ part_3.xlsx 累计 150000 行✔ part_4.xlsx 累计 187342 行[√] 完成!共生成 4 个文件,耗时 96.3 秒,输出目录:out_rows
场景 2:按部门拆表
python split_excel.py 员工花名册.xlsx -o out_dept cols -c 部门✔ 销售部.xlsx 23841 行✔ 技术部.xlsx 10233 行✔ 财务部.xlsx 3120 行✔ 人力资源部.xlsx 1842 行✔ 空白.xlsx 126 行
顺手就把"空值"单独拆出来了,数据清洗的活也干了。
五、几个关键点拆解
1. read_only=True 是灵魂
src_wb = load_workbook(src_file, read_only=True, data_only=True)read_only=True:openpyxl 不再构建完整的单元格对象树,而是边解析 XML 边吐数据,内存占用从 GB 级降到几十 MB;data_only=True:只取值,不取公式。
代价:只读模式下不能随机访问 ws["A1"],只能从头到尾迭代;ws.max_row 也可能不准。对拆分场景来说完全够用。
2. write_only=True 同样重要
wb = Workbook(write_only=True)ws = wb.create_sheet("Sheet1")ws.append(header) # 先写表头...ws.append(row) # 逐行追加
只写模式下 openpyxl 用 lxml 增量写临时文件,不会在内存里攒一个大列表。
注意:只写模式下,第一个动作必须是 append,而且不能用 ws["A1"] = xxx 这种写法。
3. 为什么用 iter_rows(values_only=True)
row_iter = src_ws.iter_rows(values_only=True)values_only=True 直接返回元组 (v1, v2, v3),而不是 Cell 对象。省掉了大量对象创建开销,实测能快 30% 以上。
4. 最后一批数据的"空文件"陷阱
如果总行数正好是 50000 的整数倍,用朴素的 count % n == 0 判断会在最后多生成一个只有表头的空文件。代码里用 has_data 标志位解决了这个问题。
六、踩坑提醒
① 公式列读出来是 None?
因为 data_only=True 读的是 Excel 缓存的公式计算结果。如果这个 xlsx 是程序生成的、从来没被 Excel/WPS 打开保存过,缓存值就是空的。
解决办法:用 Excel 打开一次并保存,或者去掉 data_only=True(但那样拿到的是公式字符串)。
② 按列拆分时分组太多怎么办?
如果按"订单号"这种高基数列拆,可能会生成几万个文件。代码里所有分组的工作簿是同时保持打开、最后统一保存的,几千个还行,上万个就可能耗尽文件句柄。
这种情况建议先做分组统计,只拆你需要的那几个值。
③ 想保留格式、公式、图表?
这套方案只搬运值,格式会丢失。如果需要保留格式,得用 openpyxl 的普通模式逐个单元格复制,速度会慢很多,而且大文件下内存顶不住。这种需求更适合用 VBA 或者专业 ETL 工具。
④ 输出文件也别忘了行数上限
xlsx 单表上限是 1,048,576 行。如果你的源文件超过这个数,-n 参数要设得比它小。
七、性能参考
我用一份 80 万行 × 15 列、约 210MB 的订单表做了个粗略测试(普通笔记本,SSD):
| 方案 | 内存峰值 | 耗时 |
|---|---|---|
pandas read_excel | 直接 MemoryError | — |
| openpyxl 普通模式 | 约 9 GB | 极慢 |
| 本文流式方案 | 约 260 MB | 约 100 秒 |
具体数字跟机器和数据有关,但量级差距是确定的:内存从 GB 级降到百 MB 级。
八、最后
这套代码的核心思想其实就一句话:
永远不要把整个文件读进内存,读一行、写一行、丢掉一行。
这个思路不只适用于 Excel,处理大 CSV、大 JSON、大日志文件时都是一样的道理。
代码你可以直接拿去改:
想加个"按日期分月"?把
key_col换成日期列,再截取年月就行;想拆完自动发邮件?在
split_by_column后面接个smtplib就完事;想做成网页工具?套个 FastAPI + 前端上传,就是个小内部系统。
觉得有用的话,点个「在看」和「赞」,我会继续更新 Python 数据处理实战系列。