夜雨聆风学习资料网

ARTICLE · 1085966

一张大Excel按列拆成几十个文件,别再复制粘贴了,一个脚本几分钟跑完

一张大Excel按列拆成几十个文件,别再复制粘贴了,一个脚本几分钟跑完

2026-09-27 · 栏目:专项攻略

行政、HR、财务都躲不开这种活:

一张全员名单,领导说“按部门拆,每个部门发一个文件给他们自己核对”;一张订单表,要按收货城市拆成几十个文件;一张成绩表,要按班级拆。

手动做法是筛选一个部门、复制、新建工作簿、粘贴、保存,再筛选下一个。十个部门还能忍,五十个城市就是一上午,还容易漏行——筛完忘记取消筛选,直接把上一份发给下一家的事,不少人都干过。

这种活交给脚本最合适:规则死、重复量大、没有任何判断空间。下面这个脚本我们自己刚跑过,下面每一步都有真实输出。

先看效果

我造了一张测试表「全员名单.xlsx」,7行数据,部门列里有综合部、技术/研发、销售*华东,还有一个人部门没填:

姓名  部门      岗位    入职年份 张三  综合部     主管    2019 李四  技术/研发  工程师  2021 王五  综合部     专员    2023 赵六  销售*华东  经理    2018 孙七  (空)      实习    2026 周八  技术/研发  架构师  2017 吴九  销售*华东  代表    2022

跑一条命令:

python3 split_excel.py 全员名单.xlsx 部门 -o 拆分结果

输出:

源文件共 7 行数据,按「部门」拆成 4 个文件:   技术_研发:2 行 -> 拆分结果/全员名单_技术_研发.xlsx   空值:1 行 -> 拆分结果/全员名单_空值.xlsx   综合部:2 行 -> 拆分结果/全员名单_综合部.xlsx   销售_华东:2 行 -> 拆分结果/全员名单_销售_华东.xlsx

几秒钟,4个文件全出来了。每个文件都带着表头,表头加粗有底色,首行冻结,列宽也调好了,打开就能用。

有两个细节值得说:

第一,「技术/研发」里的斜杠在文件名里是非法字符,「销售*华东」的星号也是,脚本自动换成了下划线,不会因为一个部门名字怪就报错中断。

第二,部门没填的那行没有被丢掉,进了「全员名单_空值.xlsx」。这一点故意的——拆文件最怕的是有人悄无声息地消失,你以为7行都有着落,实际漏了1行。空值单独成一个文件,打开一看就知道还有谁的信息要补。

脚本怎么用

命令就一个格式:

python3 split_excel.py 源文件.xlsx 列名

列名写表头里的字,比如「部门」「收货城市」「班级」,不要写列号。这样哪怕别人在前面插了一列,脚本照样找得到。

可选两个参数:-o 指定输出目录,不写就默认建一个「拆分结果」文件夹;-s 指定工作表名,不写就取第一个表。

唯一的前置条件是装了 Python 和 openpyxl 库。装库就一条命令:pip install openpyxl。公司电脑不让装东西的,也可以让信息部装一次,这种库不需要管理员权限以外的特殊条件。

完整脚本

下面就是我实测用的完整代码,复制保存成 split_excel.py 即可:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 大 Excel 按某一列拆分成多个文件。 用法:   python3 split_excel.py 全员名单.xlsx 部门   python3 split_excel.py 订单表.xlsx 收货城市 -o 拆分结果 -s Sheet1 规则:   1. 第一行必须是表头,按表头名找列(不用列号,换列也不怕)   2. 该列每一个不同的值,生成一个文件,文件名=值(非法字符自动替换)   3. 保留表头行,表头加粗+灰底,列宽按内容自适应   4. 拆分单元格、筛选、隐藏列之类的花哨格式不带,只带值和表头样式 """ import argparse import hashlib import re import sys from pathlib import Path  from openpyxl import Workbook, load_workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter  ILLEGAL = re.compile(r'[\\/:*?"<>|\r\n\t]')  HEADER_FONT = Font(bold=True, color="FFFFFF") HEADER_FILL = PatternFill("solid", fgColor="4F6D8E") HEADER_ALIGN = Alignment(horizontal="center", vertical="center")   def safe_filename(name: str) -> str:     name = ILLEGAL.sub("_", str(name)).strip().strip(".")     return name or "空值"   def file_stem_key(key: str, max_bytes: int = 200) -> str:     # 文件名按UTF-8字节限长(Linux上限255字节,一个汉字占3字节),防中文长key报File name too long;截断补短哈希防同名覆盖     b = key.encode("utf-8")     if len(b) <= max_bytes:         return key     short = hashlib.md5(b).hexdigest()[:8]     cut = b[:max_bytes].decode("utf-8", errors="ignore")     return f"{cut}_{short}"   def split(src: Path, column: str, out_dir: Path, sheet_name: str | None):     wb = load_workbook(src, read_only=True, data_only=True)     ws = wb[sheet_name] if sheet_name else wb.active      rows = ws.iter_rows(values_only=True)     try:         header = next(rows)     except StopIteration:         sys.exit("文件是空的,第一行连表头都没有")      header = [("" if c is None else str(c)).strip() for c in header]     if column not in header:         sys.exit(f"找不到列「{column}」。表头实际是:{header}")     key_idx = header.index(column)      groups: dict[str, list] = {}     total = 0     for row in rows:         if row is None or all(c is None for c in row):             continue         key = safe_filename(row[key_idx] if row[key_idx] is not None else "空值")         groups.setdefault(key, []).append(row)         total += 1     wb.close()      if not groups:         sys.exit("表头下面一行数据都没有")      out_dir.mkdir(parents=True, exist_ok=True)     stem = src.stem     report = []     for key, data in groups.items():         out_wb = Workbook()         out_ws = out_wb.active         out_ws.title = key[:31] or "Sheet1"  # sheet名最长31字符          out_ws.append(header)         for cell in out_ws[1]:             cell.font = HEADER_FONT             cell.fill = HEADER_FILL             cell.alignment = HEADER_ALIGN          for row in data:             out_ws.append(["" if c is None else c for c in row])          # 列宽自适应:按该列最长内容估算(中文按2个字符宽算)         for col_idx, col_name in enumerate(header, start=1):             width = len(str(col_name).encode("gbk", errors="ignore"))             for row in data:                 v = row[col_idx - 1]                 if v is not None:                     width = max(width, len(str(v)[:50].encode("gbk", errors="ignore")))             out_ws.column_dimensions[get_column_letter(col_idx)].width = min(width + 4, 60)          out_ws.freeze_panes = "A2"         out_path = out_dir / f"{stem}_{file_stem_key(key)}.xlsx"         out_wb.save(out_path)         report.append((key, len(data), out_path))      print(f"源文件共 {total} 行数据,按「{column}」拆成 {len(report)} 个文件:")     for key, n, p in sorted(report):         print(f"  {key}:{n} 行 -> {p}")   def main():     ap = argparse.ArgumentParser(description="大Excel按一列拆分成多个文件")     ap.add_argument("src", help="源 Excel 文件路径")     ap.add_argument("column", help="按哪一列拆,写表头名,如:部门")     ap.add_argument("-o", "--out-dir", default="拆分结果", help="输出目录(默认:拆分结果)")     ap.add_argument("-s", "--sheet", default=None, help="工作表名(默认取第一个)")     args = ap.parse_args()     split(Path(args.src), args.column, Path(args.out_dir), args.sheet)   if __name__ == "__main__":     main()

用之前,三个要交代清楚的限制

脚本不是万能的,这三个情况你要心里有数:

第一,只认 .xlsx 文件。 老的 .xls 格式 openpyxl 不支持,先用 Excel 打开另存为 .xlsx 再跑。

第二,只搬值和表头,不搬复杂格式。 如果原表里有合并单元格、五颜六色的标记、带公式的列,拆出来的文件里公式会变成数值(脚本读取时就算好了),底色标记也不会跟过来。名单、订单、成绩这种以数据为主的表没问题;如果下游要求格式和原表一模一样,这个脚本不适用。

第三,第一行必须是单行表头。 有的表表头做两行(比如上面一行“上半年”合并三列),这种结构脚本理解不了,先手工整理成一行表头再跑。

列名写错的时候脚本不会硬跑,会直接把实际表头打出来给你看,比如我故意写错:

找不到列「不存在的列」。表头实际是:['姓名', '部门', '岗位', '入职年份']

照着改对就行。

顺带说一句:拆完之后呢

这个脚本和我们之前写的「几十份Word报名表汇总进一张Excel」正好是一对:分发表单时用拆分,收齐后用汇总,数据出门和回家都不用手工搬。

判断一个Excel活值不值得写脚本,标准很简单:你发现自己在重复同一个动作超过五遍,而且每遍动作完全一样,那就该停下来花十分钟找个脚本了。省下的不只是时间,还有那种"我一上午到底干了什么"的空虚感。

有跑不通的情况,把报错信息和表头发我,我帮你看。

投稿:硅基聊斋

相关学习资料