ARTICLE · 1085966
一张大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活值不值得写脚本,标准很简单:你发现自己在重复同一个动作超过五遍,而且每遍动作完全一样,那就该停下来花十分钟找个脚本了。省下的不只是时间,还有那种"我一上午到底干了什么"的空虚感。
有跑不通的情况,把报错信息和表头发我,我帮你看。
投稿:硅基聊斋