夜雨聆风学习资料网

ARTICLE · 1079709

python-pandas拆分excel:从入门到生产级实践

python-pandas拆分excel:从入门到生产级实践

在数据工程和日常办公自动化中,"把一个 Excel 按某一列的值拆成多个文件"几乎是最高频的需求之一。看似简单,但真正落地时会遇到一堆细节:编码、日期格式、公式、合并单元格、内存占用、文件命名冲突、性能瓶颈……

今天我们从实际使用场景出发,逐层递进地一起看看这个问题怎么使用python脚本解决,中间会给出可直接复用的代码。


一、常见的都有哪些场景?

不同场景实现的策略会有很大的变化。常见的场景:

场景
特征
关键考量
A. 小文件快速拆分
几万行以内,一次跑完
代码简洁即可
B. 按列值分组,每组一个文件
最典型,如按"部门""地区"拆
命名、空值、格式保留
C. 拆成固定大小(行数)
每 1000 行一个文件
分块、序号命名
D. 大批量/大文件
几十万行以上,内存吃紧
流式读取、分块写出
E. 保留原格式
需要保留公式、样式、图表
pandas 不够用,需 openpyxl/xlwings
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 = 1    while path.exists():        path = out_dir / f"{fname}_{i}.xlsx"        i += 1    group.to_excel(path, index=False)    print(f"共拆分 {df['地区'].nunique(dropna=False)} 个文件")

这里的坑点:

  1. groupby 默认丢弃 NaN:如果"地区"有空值,这些行会凭空消失。要加 dropna=False,或提前 fillna。

  2. 文件名非法字符:Windows 不允许 \ / : * ? " < > |,不处理会直接报错。

  3. 数字型分组键:如果地区是数字,groupby 出来的 key 是 int,文件名拼接时没问题,但要注意前导零丢失(如 001 变 1)。建议读入时用 dtype=str。

  4. 重名覆盖:上面用 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 = 0                header = True                header_written[k] = True            else:                # 后续写入:跳过表头,从已写入数据行数 + 1(表头占 1 行)开始                startrow = row_counts[k] + 1                header = False            g.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。


九、那些容易踩的坑(重点)

  1. 索引列:to_excel 默认 index=True,会多出一列。业务文件几乎都要 index=False。

  2. 日期时间:pandas 的 datetime64 写出后 Excel 显示格式可能不对。可先 df["日期"] = pd.to_datetime(df["日期"]).dt.strftime("%Y-%m-%d") 转为字符串,牺牲类型换稳定显示。

  3. 前导零丢失:工号 00123 读进来变 123。读入时指定 dtype={"工号": str}。

  4. 大数字精度:超过 15 位的数字(如身份证、长订单号)在 Excel 里会丢精度。务必用 str 类型读入和写出。

  5. groupby 丢弃 NaN:前文已提,dropna=False 或 fillna。

  6. sheet_name 传 None vs 0:sheet_name=None 返回所有 sheet 的字典,sheet_name=0 返回第一个。批量处理前先确认要处理哪个 sheet。

  7. 文件被占用:目标文件正在 Excel 中打开时,to_excel 会 PermissionError。批量任务要加异常捕获和重试。

  8. 并发写同一目录:多进程拆分时文件名冲突,需加锁或用进程 id 区分。

  9. 中文路径:Python 3 基本没问题,但跨平台协作时注意编码。

  10. 公式单元格:pandas 读到的是公式的计算结果还是公式本身,取决于 openpyxl 的 data_only 参数。默认读到的是公式字符串,需要 data_only=True 读值。

  11. 隐藏行/列、筛选状态:pandas 写出的文件一律没有,需要 openpyxl 后处理。

  12. 内存爆炸: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 可能是标量或 tuple        parts = 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 = 1        while path.exists():            path = output_dir / f"{fname}_{idx}.xlsx"            idx += 1        group.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]

十一、总结与选型建议

需求
推荐方案
普通按列拆分
pandas groupby + to_excel
一个文件多个 sheet
pd.ExcelWriter
大文件
先转 CSV,再 chunksize 流式
保留格式/公式
openpyxl 复制 sheet 删行 / xlwings
极致性能写出
xlsxwriter,或换 CSV/Parquet
超 104 万行
放弃 Excel,用 CSV/数据库

核心:pandas 负责"数据搬运",不要用它做"格式加工"。 把职责分清楚,代码就稳了。

拆分 Excel 本身不难,难的是把边界情况想全。上面提到的空值、非法字符、精度、内存、格式保真这几类问题,几乎覆盖了 90% 的线上事故。把 _safe 和异常处理写进工具函数,一次封装,长期受益。

相关学习资料