资讯详情

Python实现Excel自动合并去重与报告生成:从需求拆解到完整交付

📅 2026/9/26 21:15:14 | 华诺云谱 👁 阅读
Python实现Excel自动合并去重与报告生成:从需求拆解到完整交付
前些天同事扔给我一个压缩包文件名就俩字“无标题”。解压以后里头躺着一个Markdown文档、几张截图和一段半成品代码。他挠着头说“就是想搭个小工具但写到一半卡住了你帮我看看这东西到底能不能做成。”我翻了翻内容发现他其实连需求都没写清楚满篇都是“大概”“好像”“也许”。这就是典型的“脑子里有想法手上没方案”——产品逻辑没想透技术选型没定就开始动手写代码结果写到一半发现哪哪都不对。这种事我在项目里见过太多次了。很多人以为“做东西”是从写代码开始实际上是从拆解需求开始的。你连“这个工具到底为谁解决什么问题”都没想清楚后面每一步都是在给不确定打地基塌是迟早的事。所以这篇我就以这个“无标题”的真实场景为引子完整走一遍从需求拆解、技术选型、核心模块实现到联调排错的全部过程。不讲虚的全是可直接套用的方法。1. 核心需求拆解先搞清楚“这东西到底是干嘛的”1.1 需求盲区比功能缺失更致命那个压缩包里的Markdown文档说白了就是一份“想法草稿”。我通读下来核心诉求其实就一句想做一个内部用的数据整理小工具能定期把分散在几个Excel里的内容合并成一张总表按指定字段去重再生成一份汇总报告。但这句话并不是他文档里明确写出来的而是我从十几个“大概可以”“说不定能”的句子里拎出来的。这就是第一个真正的坑需求经常被表达成模糊感受而不是可执行描述。建设团队的人说要“提升效率”开发的人以为要做“一键自动化”实际上业务方只是不想手动复制粘贴但流程规则、异常情况、输出样式全都没定。你在这种状态下开工等于蒙着眼睛开车。所以在动手前我给自己定了一条铁律只信可验证的行为不信修饰词。所谓的“定期”是多长时间一次“几个Excel”是三个还是三十个“按指定字段去重”到底是保留第一条还是最后一条“报告”是PDF还是Excel这些不搞清楚后面全是猜。1.2 用“最小可运行版本”反向逼出真需求我建议他把项目描述改写成一问一答的形式而不是把脑子里的感叹句全写下来。后来我们坐到一起我拿着他的草稿一句一句问只问出五个问题答案就全清楚了数据源是三张固定表头的Excel放在同一个文件夹里每周更新一次。去重字段是“项目编号”重复时保留最新更新的那一行。合并结果要单独生成一个Excel文件带Sheet区分原始数据和汇总结果。汇总报告只需要总数、去重数、各来源表贡献量这三个数。运行环境是他自己的Windows电脑没有服务器也不会部署。五个答案一出来这个项目的复杂度直接从“猜不透”降到“半天能写完”。我反手又问他一句“你先别管技术手工做一遍这个流程要多久”他说大概十五分钟。我说那自动化能做到五分钟以内这事儿就值得做如果只能省三分钟你就别做了。这就是判断项目价值的另一条原则自动化节省的时间必须显著大于维护自动化本身的时间否则就是负收益。1.3 拒绝“全功能”诱惑砍需求也是方案的一部分聊的过程中他加了不少“如果”“如果以后要接数据库呢”“如果报表要发到企业微信呢”“如果能做成网页给所有人用呢”我一律先记下来然后问他一句这些“如果”里有哪一个是下周之前必须完成的他说没有。那就对了——统统不纳入第一版。这个项目的核心就是“合并Excel、去重、生成汇总”其他全是伪装成需求的好奇心。砍掉客户端的B/S架构、砍掉可视化报表、砍掉在线预览最终交付物就两个一个能跑的脚本一份使用说明。工具的价值不在功能多在于恰好覆盖工作流的断点。多出来的每一个功能都是往你未来的维护工作量上叠砖头。2. 工具选型为什么用Python而不是点击鼠标操作2.1 “用Python”不是一个技术偏好是一个决策结果同事问我“这场景用VBA不是更快吗Excel自带的功能还能录宏。”这话不假但他说的是“能做”我要的是“好维护”。VBA写出来的东西调试靠弹窗版本管理靠另存为而且除了写的人团队里基本没人看得懂。更关键的是他的三张Excel表格格式并不完全一致VBA在处理“列顺序不同、字段名大同小异”这种情况时代码会越写越死。Python用pandas来做两行代码就能完成列重命名和顺序调整。选Python的另一个原因是生态。excel合并、去重、统计这种活儿pandas就是干这个的生成报告需要格式控制openpyxl直接操作单元格样式将来真有接企业微信或者数据库的需求requests和SQLAlchemy就在那儿等着。你用VBA每换一个需求都是一场硬仗用Python很多场景只是换个函数的问题。2.2 依赖管理能少装就少装能内置就内置这个项目的依赖只有三个pandas、openpyxl、pathlibpathlib是标准库不需要装。很多新手一上来就上FastAPI、上SQLite我这边的原则很简单依赖清单越短别人复现的成本越低。尤其当你交付的对象是个对命令行不熟的同事他听到“先装Python再pip install xxx”就已经开始紧张了。依赖越少培训成本越小出错的概率越小。另外我在代码里刻意没写任何“发送邮件”“上传服务器”之类的外部动作。内部工具第一版最忌讳的就是掺入网络请求——机器没联网、公司防火墙拦截、认证证书过期任何一个环节都能让本来好好跑的脚本瞬间变成玄学。能本地完成就别去碰网络等需求真到那儿了再加。2.3 环境与路径不要让用户碰绝对路径我在代码里用了Path(__file__).resolve().parent来定位脚本所在目录而不是写死C:\Users\xxx\Desktop。这个细节看起来不起眼实际上决定了这个工具能不能“随处可跑”。同事把这个脚本拷到U盘里从任何一台电脑上双击运行它都能找到同级目录下的数据文件夹。如果写死路径他换一台电脑第一件事就是打开代码改路径这已经违背了我“交付物不该让用户看代码”的原则。获取当前目录是标准操作进一步我用BASE_DIR / data拼接输入文件夹路径用BASE_DIR / output作为输出目录并且在启动脚本时用mkdir(exist_okTrue)自动建目录。这样用户只需要按约定放文件、双击运行、拿结果对代码零感知。3. 核心逻辑实现从合并到去重到统计每一步都有讲究3.1 读取与形态统一先让三张表能“拼在一起”三张表的字段名并不完全一致比如表A里的“编号”在表B里叫“项目编码”表C里叫“ID”。直接concat肯定乱套。第一步要做字段映射把不同的列名统一成内部标准名。这是整个流程里最需要审查的一步因为合并错了后面统计全错。我用一个字典来声明映射关系再给每张表加一个“来源”列标记它来自哪个文件。这样后面做统计报告时能直接按“来源”分组计数方便追溯。值得提醒的是读Excel时加上dtypestr参数防止“项目编号”这类看起来是数字的内容被Excel自动转成科学计数法或浮点数尤其是编号中间带“-”的一旦被转类型去重就会出错。3.2 去重逻辑为什么保留“最后一条”不是“第一条”同事的原始需求是“按项目编号去重”但没有说保留哪条。我看了他的数据之后发现每张表的更新时间不一样同一个编号可能在一个月前和一个月后各出现一次后一次往往修正了前一次的遗漏。所以我设计成按“更新时间”排序后用drop_duplicates(subset编号, keeplast)保留时间最新的一条。这个决策不是拍脑袋是业务规则倒推的。数据整理工具的核心原则是“数据处理规则必须向业务规则看齐”如果业务上认定“后来的更新优先级最高”那代码就必须体现这个优先级。偶尔也有人会问“那要是后一次更新是误操作呢”我说那属于数据质量治理范畴不是代码范畴代码只能反映规则不能替你判断规则那得靠数据审核脚本之外解决。3.3 生成报告与文件输出结果不仅要算得对还要看得懂报告我设计了三块内容合并前的总行数、去重后的总行数、每个来源表的贡献行数。统计完直接以追加Sheet的方式写进同一个Excel文件一个Sheet放明细数据一个Sheet放汇总报告。用openpyxl给表头加粗、加底色、设置列宽避免普通人打开Excel后看到一片干巴巴的表格不知道看什么。这里有个小细节为了避免重复运行后输出文件互相覆盖我在输出文件名里加了时间戳YYYYMMDD_HHMM。这样每次运行结果都不会破坏之前的产物便于历史追溯。如果你只写成固定名merged.xlsx第二次运行直接把第一次的结果冲掉万一后面发现数据有问题连回看的机会都没有。4. 实操全流程从零到一完整跑通4.1 代码结构与启动方式项目放本地目录按下面结构组织excel-merge-tool/ ├── main.py ├── data/ # 用户把三张Excel放这里 └── output/ # 脚本自动创建生成结果放这里main.py 的主流程拆成三个函数load_and_normalize负责读取和字段映射merge_and_deduplicate负责合并与去重generate_report负责统计和写出。这样每个函数都能单独测试Excel文件有问题时你能直接定位到是读的阶段、并的阶段还是写的阶段出的错。运行方式很简单在命令行里导航到脚本目录后执行python main.py。运行结束控制台会打印每次处理的文件路径、读取行数、去重前后数量以及输出文件存放位置。别人看不懂代码没关系看得懂这几行打印就够了。4.2 关键参数与细节对照表我整理了一张表把这些细节按“参数/位置/说明”列出来方便你照抄参数或方法用途我的建议值备注dtypestr防止编号被转成数值read_excel时固定加尤其编号含“-”或前导零keeplast重复编号保留最新记录按更新时间排序后使用业务规则优先别照抄Path(__file__).resolve().parent定位脚本所在目录不用绝对路径把脚本整个文件夹拷走也能跑mkdir(exist_okTrue)自动创建输出目录启动时执行避免用户忘记建文件夹pd.concat(ignore_indexTrue)合并多个DataFrame必须加ignore_index不然索引重叠会混乱这里面最容易被忽略的是ignore_indexTrue。如果你不加三张表拼接之后索引还是各自从0开始的后续iterrows()或者sort时会出现索引重复轻则结果怪重则触发异常。这种细节踩一次坑就记住了但我更希望你别踩。4.3 完整代码示例可以直接抄作业的版本下面是完整代码我加了不少注释。这不是展示炫技是为了让你在改需求时知道该动哪一行# main.py import pandas as pd from pathlib import Path from datetime import datetime BASE_DIR Path(__file__).resolve().parent DATA_DIR BASE_DIR / data OUTPUT_DIR BASE_DIR / output # 字段映射表不同Excel表中的列名 - 程序内部统一列名 FIELD_MAPPING { 编号: project_id, 项目编码: project_id, ID: project_id, 名称: project_name, 项目名称: project_name, 更新时间: update_time, 更新日期: update_time, } REQUIRED_COLUMNS [project_id, project_name, update_time] def load_and_normalize(file_path: Path, mapping: dict) - pd.DataFrame: 读取单个Excel并统一列名 df pd.read_excel(file_path, dtypestr) df df.rename(columnsmapping) # 只保留我需要的列其余字段暂时不引入 for col in REQUIRED_COLUMNS: if col not in df.columns: raise ValueError(f文件 {file_path.name} 缺少必需的列: {col}) df df[REQUIRED_COLUMNS] df[source_file] file_path.name return df def merge_and_deduplicate(file_list): 合并所有文件并按项目编号去重保留最新更新 frames [] for f in file_list: df load_and_normalize(f, FIELD_MAPPING) frames.append(df) print(f读取 {f.name}: {len(df)} 行) combined pd.concat(frames, ignore_indexTrue) total_before len(combined) # 按 update_time 排序让最新记录排在最前面然后保留每条编号的最后一条 combined[update_time] pd.to_datetime(combined[update_time], errorscoerce) combined combined.sort_values(update_time, ascendingTrue) deduped combined.drop_duplicates(subsetproject_id, keeplast) print(f合并后总行数: {total_before}) print(f去重后行数: {len(deduped)}) return combined, deduped def generate_report(combined, deduped): 按来源文件统计贡献行数并返回报告DataFrame source_stats combined.groupby(source_file).size().reset_index() source_stats.columns [来源文件, 原始行数] report pd.DataFrame({ 指标: [合并前总行数, 去重后总行数, 重复丢弃数], 数值: [ len(combined), len(deduped), len(combined) - len(deduped) ] }) return report, source_stats def main(): OUTPUT_DIR.mkdir(exist_okTrue) files list(DATA_DIR.glob(*.xlsx)) if not files: print(data 目录下没有找到任何 .xlsx 文件) return combined, deduped merge_and_deduplicate(files) report, source_stats generate_report(combined, deduped) timestamp datetime.now().strftime(%Y%m%d_%H%M) output_path OUTPUT_DIR / fmerged_report_{timestamp}.xlsx with pd.ExcelWriter(output_path, engineopenpyxl) as writer: deduped.to_excel(writer, sheet_name去重后明细, indexFalse) report.to_excel(writer, sheet_name汇总报告, indexFalse) source_stats.to_excel(writer, sheet_name来源统计, indexFalse) # 用openpyxl简单美化一下表头 from openpyxl import load_workbook wb load_workbook(output_path) for ws in wb.worksheets: for cell in ws[1]: cell.font cell.font.copy(boldTrue) cell.fill cell.fill.copy(start_colorDCE6F1, end_colorDCE6F1, fill_typesolid) ws.column_dimensions[A].width 20 ws.column_dimensions[B].width 24 if ws.max_column 3: ws.column_dimensions[C].width 20 wb.save(output_path) print(f处理完成结果已保存至: {output_path}) if __name__ __main__: main()这份代码里的“更新时间”列我用errorscoerce做了解析容错如果单元格里是空值或日期格式不对会被转成NaT最高的排到最后这提示你这条记录的时间异常需要人工检查。内部工具不要装高大上的监控系统但数据质量提示一定要有。4.4 运行结果与验收标准脚本跑完后的Excel文件打开是三个Sheet第一个是去重后的全部明细第二个是“合并前多少行、去重后多少行、丢弃多少行”的指标表第三个是每个来源文件贡献了多少行。同事验收的时候我让他拿着一份历史手工整理的表跟脚本跑出来的结果逐条对比编号要求必须完全一致。这里我多说一句自动化工具的验收不要只看“能不能跑”要看输出是不是跟人工做出来的结果一致。如果两个结果对不上你不光要怀疑代码还要怀疑之前人工操作本身就是错的——自动化有一部分价值就是把“不可靠的人工流程”显性化成“可审查的代码规则”。5. 常见问题与排查技巧这些坑你可能也会踩5.1 输出结果为空但代码没报错这个现象我见过好几次原因大多是“data目录下Excel的Sheet名不一致”。pandas的read_excel默认读第一个Sheet如果其中某张表的第一个Sheet是空的或者放的是目录说明那读进来就是空表。排查方式很简单打印每个文件的df.shape看行数别信文件大小。如果你能确认要用指定Sheet在read_excel里显式加一个sheet_name某个名字参数别依赖那个“恰好正确”的默认行为。5.2 读写时报错“Excel文件损坏”这个错几乎都来源于文件被WPS或者Excel以独占方式打开着。Windows下写同一个路径的Excel文件如果文件还在Office里开着Python是没有权限写入的。解决方式不是改代码是先把Excel关掉再运行脚本。我在代码里没有强制关闭文件的操作也不太建议写因为那是绕过系统锁的激烈手段内部工具用不上。另一个小概率原因是原来的Excel里存在损坏的单元格openpyxl解析会直接报错。这时候你先用Excel打开那个文件另存为xlsx通常就修好了。5.3 同一个编号去重后保留的不是期望的那行大概率是你排序方向搞反了。sort_values默认升序配合keeplast保留的是最后一次出现的记录。如果你想保留最早一次把排序改成降序但仍用keeplast或者在去重时改用keepfirst。这个逻辑并不复杂但确实容易把我绕进去所以我建议你在代码注释里写清楚“保留哪一行以及为什么”下次打开代码三秒想明白。5.4 快速定位问题的方法我在这个项目里用了一个很土但很有效的排查方式把中间结果用to_csv()摊出来看。比如先单独看读取后的DataFrame长什么样再看好去重后的结果最后看好统计结果。哪里不对一眼就能定位到是哪一步的问题。你不需要一上来就打断点先把数据摊开看比什么调试器都好用。我用一张流水账表格把“症状、原因、解决动作”列出来贴在脚本旁边下次任何人再遇到问题时可以先排查而不是急吼吼来找我症状常见原因解决动作跑完输出文件没生成data目录下没有xlsx或文件名不是.xlsx检查文件名后缀打开文件另存为xlsx格式输出行数与预期不符多读了多余Sheet或读错了列打印每个文件读取后的shape检查编号变成科学计数法读Excel时没指定dtypestr在read_excel中加dtype参数报告里全是0统计的是空DataFrame检查数据路径与文件名打印len(df)时间排序不对日期文本格式不统一统一为pd.to_datetimeerrorscoerce6. 经验总结从一个“无标题”项目延伸出的做事方法6.1 好的内部工具是能让人“不用理解”的工具这个项目做完之后同事的使用流程变成把Excel扔进data文件夹双击运行脚本去output文件夹拿结果。他不看代码也不理解pandas但照样能用得稳稳的。这才叫交付成功——工具的价值不在你能讲清楚原理而是用户不需要懂原理就能完成工作。所以我在代码注释里会写清每一个函数的作用但我不会强迫使用者看这些注释。你要做的是把“看不懂”变成“不用看”而不是变成“逼自己学”。清晰的结构和注释是为了方便你三个月后的自己改需求不是为了让用户提前毕业。6.2 需求变化后怎么改代码才不容易翻车用不了两个月同事大概率会提新需求“能不能再筛一下某个状态”“能不能生成图表”“能不能加一列备注”。我的建议是改代码前先改数据规则描述也就是先写清楚“输入长什么样、输出长什么样、判断标准是什么”再动手改函数。凡是能通过加一个参数解决的事就不要重写逻辑凡是能通过加一个Sheet解决的问题就不要动原有Sheet的结构。这个项目的第一版目标就是“先用起来”。后续加字段、加规则、加来源表都是在FIELD_MAPPING和REQUIRED_COLUMNS里追加的事。这就像盖房子先搭好梁柱后面的墙体和隔断想怎么加都行你光图快凑出一个混凝土壳子后面改个窗户都得砸墙。6.3 从“能跑”到“可靠”取决于异常处理到什么程度我最后加了一段try-except包裹主流程异常信息直接打印在控制台方便用户截图发给我。这对内部工具来说够了不需要弹出对话框不需要写日志系统。做内部工具的可靠性和做线上系统的可靠性不一样前者核心是“出了问题能快速定位”后者核心是“出了问题不能影响别人”。你要分清场景别拿生产环境的条条框框来压一个本地跑脚本的小工具那样既费时间又把简单事搞复杂。这个没标题的项目最终从一堆零碎想法变成了一套完整可用的工具。事实上不管这个脚本技术含量是高是低完成它最大的收益是验证了一种工作方式先逼问需求再定周期再选顺手的小工具最后把一切做成不用动脑的交付物。下次再遇到任何想做的事别急着写代码先把你脑子里的“大概”一条条写在纸上把每一条模糊描述磨成一刀能切断的明确规则。别怕项目小再小的项目也能帮你把整个做事逻辑跑通。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑