ARTICLE · 1079709
python-pandas拆分excel:从入门到生产级实践
在数据工程和日常办公自动化中,"把一个 Excel 按某一列的值拆成多个文件"几乎是最高频的需求之一。看似简单,但真正落地时会遇到一堆细节:编码、日期格式、公式、合并单元格、内存占用、文件命名冲突、性能瓶颈……
今天我们从实际使用场景出发,逐层递进地一起看看这个问题怎么使用python脚本解决,中间会给出可直接复用的代码。
一、常见的都有哪些场景?
不同场景实现的策略会有很大的变化。常见的场景:
| A. 小文件快速拆分 | ||
| B. 按列值分组,每组一个文件 | ||
| C. 拆成固定大小(行数) | ||
| D. 大批量/大文件 | ||
| E. 保留原格式 | ||
| F. 多列组合拆分 |
下面分别展开。
二、核心工具选择
pandas:负责数据读取、分组、写出。快、灵活。
openpyxl:pandas 写
.xlsx的底层引擎,也能做样式保留。xlsxwriter:写
.xlsx性能好、格式控制强,但只能写不能读。xlrd:老版本读
.xls(已不推荐,新版本 pandas 已弃用)。
结论:读用 pandas + openpyxl,写普通需求用 pandas,写带样式的大批量文件用 xlsxwriter。
安装:
pip install pandas openpyxl xlsxwriter三、最典型场景:按某一列的值拆成多个 Excel
假设有 sales.xlsx,含列:订单号, 地区, 销售额, 日期,要按"地区"拆成多个文件。
3.1 基础版
import pandas as pdfrom pathlib import Pathdf = pd.read_excel("sales.xlsx", sheet_name="Sheet1")out_dir = Path("output")out_dir.mkdir(exist_ok=True)for region, group in df.groupby("地区"):group.to_excel(out_dir / f"{region}.xlsx", index=False)
四行核心逻辑,已经能满足简单的基础要求。但实际中,往往要考虑更多。
3.2 生产版:考虑空值、非法字符、命名冲突
import reimport pandas as pdfrom pathlib import Pathdef safe_filename(name:str)->str:"""去掉 Windows/Linux 文件名非法字符"""name = str(name).strip()name = re.sub(r'[\\/:*?"<>|]',"_", name)name = name or "空值" return name[:100]# 防止文件名过长df = pd.read_excel("sales.xlsx", sheet_name="Sheet1")# 关键:把分组列的 NaN 单独归为一类,否则 groupby 默认丢弃df["地区"] = df["地区"].fillna("未知地区")out_dir = Path("output")out_dir.mkdir(exist_ok=True)for region, group in df.groupby("地区", dropna=False):fname = safe_filename(region)path = out_dir / f"{fname}.xlsx"# 防止重名覆盖i = 1while path.exists():path = out_dir / f"{fname}_{i}.xlsx"i += 1group.to_excel(path, index=False)print(f"共拆分 {df['地区'].nunique(dropna=False)} 个文件")
这里的坑点:
groupby默认丢弃 NaN:如果"地区"有空值,这些行会凭空消失。要加dropna=False,或提前 fillna。文件名非法字符:Windows 不允许
\ / : * ? " < > |,不处理会直接报错。数字型分组键:如果地区是数字,
groupby出来的 key 是int,文件名拼接时没问题,但要注意前导零丢失(如001变1)。建议读入时用dtype=str。重名覆盖:上面用 while 兜底,更稳妥。
四、场景 B:一次写多个 Sheet 到一个文件(反向需求)
有时不是拆成多文件,而是"每个地区一个 sheet"。这时用 ExcelWriter:
with pd.ExcelWriter("split_by_region.xlsx", engine="openpyxl") as writer:for region, group in df.groupby("地区", dropna=False):# sheet 名同样有非法字符限制,且最长 31 字符sheet_name = safe_filename(region)[:31]group.to_excel(writer, sheet_name=sheet_name, index=False)
注意: Sheet 名限制更严——不能含 []:*?/\,最长 31 字符,且不能重名。ExcelWriter 对重名 sheet 会直接报错,需要自己保证唯一。
五、场景 C:按固定行数拆分
chunk_size =1000for i, start in enumerate(range(0,len(df), chunk_size), start=1):chunk = df.iloc[start:start + chunk_size]chunk.to_excel(f"output/part_{i:03d}.xlsx", index=False)
如果分组边界要在"同一订单不能跨文件"这类业务约束下切分,就不能简单按行数切,得先按业务键聚合再累加行数。这属于业务逻辑,pandas 只负责执行。
六、场景 D:大文件 / 内存受限
pd.read_excel 是一次性全量读入内存的,几十万行 xlsx 很容易吃掉几个 G。此时有两条路:
6.1 先转 CSV,再分块处理
# 一次性转换(可用命令行工具或 LibreOffice 加速)df = pd.read_excel("huge.xlsx")df.to_csv("huge.csv", index=False)# 之后用 chunksize 流式读取reader = pd.read_csv("huge.csv", chunksize=100_000)for i, chunk in enumerate(reader):chunk.to_csv(f"output/chunk_{i}.csv", index=False)
6.2 分块累加到不同文件(按分组键)
如果必须按列值拆分且文件巨大,可以先扫一遍拿到所有分组键,再分块 append:
import pandas as pdimport osfrom collections import defaultdict# 确保输出目录存在os.makedirs("output", exist_ok=True)def safe_filename(name):"""将分组键转换为安全的文件名"""invalid_chars = '<>:"/\\|?*'name = str(name)for ch in invalid_chars:name = name.replace(ch, "_")return name.strip() or "empty"# 第一遍:拿到所有唯一分组键keys = pd.read_csv("huge.csv", usecols=["地区"])["地区"].dropna().unique()# 创建 writerswriters = {k: pd.ExcelWriter(f"output/{safe_filename(k)}.xlsx", engine="xlsxwriter")for k in keys}# 记录每个分组已经写入了多少行数据(用于计算 startrow)# 行 0 会写表头,所以数据从行 1 开始row_counts = defaultdict(int) # 已写入的数据行数header_written = defaultdict(bool) # 是否已经写过表头try:for chunk in pd.read_csv("huge.csv", chunksize=100000):for k, g in chunk.groupby("地区"):if k not in writers:# 防御性处理:万一 keys 里没包含(理论上不会)writers[k] = pd.ExcelWriter(f"output/{safe_filename(k)}.xlsx", engine="xlsxwriter")if not header_written[k]:# 第一次写入:从第 0 行开始,带表头startrow = 0header = Trueheader_written[k] = Trueelse:# 后续写入:跳过表头,从已写入数据行数 + 1(表头占 1 行)开始startrow = row_counts[k] + 1header = Falseg.to_excel(writers[k],sheet_name="Sheet1",index=False,startrow=startrow,header=header,)row_counts[k] += len(g)finally:# 确保所有 writer 都被关闭for k, w in writers.items():try:w.close()except Exception as e:print(f"关闭 writer 失败 [{k}]: {e}")
现实建议:
如果文件大到这个程度,输出格式尽量用 CSV/Parquet 而不是 Excel。xlsx 本身是压缩的 XML,写大文件极慢且内存开销大。Excel 单表上限约 104 万行,超过必须换格式。
6.3 性能对比(经验值)
to_excel写 10 万行:几十秒级别。同数据写 CSV:1~2 秒。
如果非要 xlsx,
xlsxwriter通常比openpyxl快,尤其是开启constant_memory模式(但该模式要求按行顺序写,pandas 不易直接用)。
七、场景 E:必须保留原 Excel 的格式/公式/图表
pandas 做不到。read_excel 读进来的是纯数据,样式、公式、合并单元格、图表全丢。此时方案:
7.1 用 openpyxl 复制工作表再删行
from openpyxl import load_workbookwb = load_workbook("template.xlsx")for region in regions:ws = wb.copy_worksheet(wb["Sheet1"])# 复制带格式的 sheet# 然后根据条件删除不匹配的行,或反向保留
思路是"保留格式、删数据",而不是"重新写数据"。适合格式复杂但数据量不大的场景。
7.2 用 xlwings 调用 Excel 本身n
import xlwings as xw# 借助真实 Excel 进程,能完整保留公式/透视表/图表
优点是保真度最高,缺点是依赖本机安装 Excel,不适合 Linux 服务器。
一句话: 需要保格式就别指望 pandas,改用 openpyxl 的"复制-删行"模式或 xlwings。
八、场景 F:多列组合拆分 + 目录结构
按"年份 + 地区"拆分,天然适合目录树:
for(year, region), group in df.groupby(["年份","地区"], dropna=False):d = Path("output") / safe_filename(year) / safe_filename(region)d.mkdir(parents=True, exist_ok=True)group.to_excel(d / "data.xlsx", index=False)
组合键的分组,groupby 传入列表即可,key 会变成 tuple。
九、那些容易踩的坑(重点)
索引列:
to_excel默认index=True,会多出一列。业务文件几乎都要index=False。日期时间:pandas 的
datetime64写出后 Excel 显示格式可能不对。可先df["日期"] = pd.to_datetime(df["日期"]).dt.strftime("%Y-%m-%d")转为字符串,牺牲类型换稳定显示。前导零丢失:工号
00123读进来变123。读入时指定dtype={"工号": str}。大数字精度:超过 15 位的数字(如身份证、长订单号)在 Excel 里会丢精度。务必用 str 类型读入和写出。
groupby 丢弃 NaN:前文已提,
dropna=False或 fillna。sheet_name 传 None vs 0:
sheet_name=None返回所有 sheet 的字典,sheet_name=0返回第一个。批量处理前先确认要处理哪个 sheet。文件被占用:目标文件正在 Excel 中打开时,
to_excel会PermissionError。批量任务要加异常捕获和重试。并发写同一目录:多进程拆分时文件名冲突,需加锁或用进程 id 区分。
中文路径:Python 3 基本没问题,但跨平台协作时注意编码。
公式单元格:pandas 读到的是公式的计算结果还是公式本身,取决于
openpyxl的data_only参数。默认读到的是公式字符串,需要data_only=True读值。隐藏行/列、筛选状态:pandas 写出的文件一律没有,需要 openpyxl 后处理。
内存爆炸:
to_excel内部会构造 XML 树,大表要警惕。优先 CSV/Parquet。
十、封装一个生产级拆分函数
把上面的经验收拢成一个可复用函数:
import reimport pandas as pdfrom pathlib import Pathfrom typing import Union, Listdef split_excel_by_column(input_path: Union[str, Path],column: Union[str, List[str]],output_dir: Union[str, Path] = "output",sheet_name: Union[str, int] = 0,keep_na: bool = True,dtype: dict | None = None,engine: str = "openpyxl",) -> List[Path]:"""按一个或多个列的值,将 Excel 拆分为多个文件。返回生成的文件路径列表。"""input_path = Path(input_path)output_dir = Path(output_dir)output_dir.mkdir(parents=True, exist_ok=True)df = pd.read_excel(input_path, sheet_name=sheet_name, dtype=dtype)cols = [column] if isinstance(column, str) else list(column)# 校验列存在missing = [c for c in cols if c not in df.columns]if missing:raise KeyError(f"以下列不存在: {missing}")# 处理缺失值if keep_na:for c in cols:df[c] = df[c].fillna("未知")generated = []grouped = df.groupby(cols, dropna=not keep_na)for key, group in grouped:# key 可能是标量或 tupleparts = key if isinstance(key, tuple) else (key,)fname = "_".join(_safe(str(p)) for p in parts) or "data"path = output_dir / f"{fname}.xlsx"# 防重名idx = 1while path.exists():path = output_dir / f"{fname}_{idx}.xlsx"idx += 1group.to_excel(path, index=False, engine=engine)generated.append(path)return generateddef _safe(name: str) -> str:name = re.sub(r'[\\/:*?"<>|]', "_", name.strip())return (name or "空值")[:80]
十一、总结与选型建议
groupby + to_excel | |
pd.ExcelWriter | |
chunksize 流式 | |
核心:pandas 负责"数据搬运",不要用它做"格式加工"。 把职责分清楚,代码就稳了。
拆分 Excel 本身不难,难的是把边界情况想全。上面提到的空值、非法字符、精度、内存、格式保真这几类问题,几乎覆盖了 90% 的线上事故。把 _safe 和异常处理写进工具函数,一次封装,长期受益。