
简介这是一套基于PyQt5与Excel自动化处理的领料明细汇总工具面向在校学生与毕业项目设计也适合Python进阶学习者以及需要处理多表领料数据的管理人员。工具采用可视化图形界面用户选定输入文件夹和输出文件夹后程序会自动读取目录内全部Excel文件提取物料编号、名称、领料数量、领料时间等关键字段并汇总到一份新的Excel工作簿中可显著减少手工复制粘贴广泛用于库存管理、物料追踪与报表生成。压缩包共12个文件大小约804KB其中包含可直接运行的Python主程序、Jupyter说明笔记、工程部与生产部两个示例领料明细Excel、多张界面截图PNG以及程序图标ICO便于对照学习界面布局与数据处理流程。当前已有74人学习下载适合希望借鉴完整GUI项目结构、掌握Excel批量读取与汇总技巧的开发者。1. 领料明细汇总工具PyQt5加Excel的正确打开方式做毕业项目设计时领料明细汇总工具这类题目容易出效果因为需求真实又不复杂库房每天几十条领料记录散落在多个Sheet和Excel文件里月底要按物料、按车间汇总成一张能对账的表。手工SUMIF也能做但文件一多格式一乱就撑不住桌面程序胜在选文件、点按钮、结果落进新Excel。我选PyQt5加pandas加openpyxl不用VBA。原因新版Office默认拦截宏VBA窗体在不同缩放比屏幕下容易错位而PyQt5的布局管理器能自适应。下面按Excel字段约定、PyQt5界面搭建、汇总逻辑、导出与打包验证的顺序把整条链路写清楚适合会Python基础语法、第一次把界面和数据处理耦合起来的同学。2. 数据结构先行用pandas把Excel明细读成统一规格在写任何界面代码之前先把明细怎么读定下来。这一章解决的是多个车间文件格式不统一的问题也是答辩时最值得展开讲的部分。很多毕业设计死在第一步不是因为不会写窗口而是因为读取Excel时没做字段规整后面所有聚合逻辑都在跟脏数据搏斗。2.1 领料单字段设计标准列名是第一个约定领料明细常见的字段有领料日期、车间部门、物料编码、物料名称、规格型号、单位、领用数量、领用人、备注。注意这里说的是常见因为实际拿到手的文件总有出入A车间表头叫生产车间B车间叫领料部门单位列有人填个有人填PCS隔几行还有整行空白。我在项目里定的第一个约定是一张字段映射表把可能出现的别名统一映射到标准字段上而不是每来一个新文件就改一次读取代码。标准字段常见别名读取时要处理的事情日期领料日期 / 领用日期 / date可能是文本日期也可能是Excel序列号车间车间 / 生产车间 / 领料部门去掉首尾空格和换行符物料编码编码 / 物料号 / code可能被Excel自动存成数值导致前导0丢失数量领用数量 / 实发数量 / qty混有数字、文本和单位后缀这张表有两个作用第一它明确了读取时要做的清洗动作去空格、转类型、校验前导0。第二它是后面动态识别列名的依据没有这张表换一个数据源整个程序就要推翻重写。建议在下笔写代码前先整理两三份真实的领料单哪怕只是在草稿纸上列一下字段名也比直接上网套模板可靠得多。2.2 pandas读取Excel的最小可运行代码读取用pandas而不是直接操作openpyxl理由是pandas把文件解析和类型转换都封装好了代码量能少一个数量级。以下是最小可运行的读取函数import pandas as pd def read_lingliao(path: str, sheet_name0, header_row: int 0) - pd.DataFrame: 读取领料明细Excel返回规整的DataFrame。 df pd.read_excel( path, sheet_namesheet_name, # 0表示第一个Sheet也可传工作表名称字符串 headerheader_row, # 表头所在行常见表头在第1行有的在第2行 dtypestr, # 先全部按字符串读入避免日期和编码被自动转换 ) df.columns [str(c).strip() for c in df.columns] # 列名首尾空格去掉 df df.dropna(howall) # 整行全为空的行直接删除 return df三个参数值得展开说。第一个是dtypestr这是从真实领料单里踩出来的坑同一列单元格混着20240510和2024-05-10如果不强制字符串pandas会把它们解析成不同类型后面按日期聚合时会多出很多组。第二个是header_row真实领料单的表头经常从第二行开始第一行是XX车间2024年6月领料记录这类大标题到时候只要传header_row1。第三个是dropna(howall)领料单里插入整行空行很常见不删掉的话groupby时可能产生一个键全是NaN的分组。如果sheet_name传Nonepandas返回的是一个字典键是工作表名称值是每个Sheet的DataFrame。这个特性是后面做多Sheet合并的基础第4章会用它来处理一个Excel里有多个车间表的情况。2.3 动态识别列名兼容不同车间文件的写法再往前走一步。用户拿来的文件列名不可能完全一致如果每换一个文件就要手动挑列程序就谈不上工具。写一个normalize_columns做别名映射FIELD_ALIASES { 日期: [领料日期, 领用日期, date], 车间: [车间, 生产车间, 领料部门, 部门], 物料编码: [物料编码, 编码, 物料号, code], 物料名称: [物料名称, 名称, 品名], 单位: [单位, 计量单位], 数量: [数量, 领用数量, 实发数量, qty], } def normalize_columns(df: pd.DataFrame) - pd.DataFrame: 把别名列名映射成标准字段名找不到别名的列原样保留。 rename_map {} for col in df.columns: col_clean str(col).strip().lower() for standard, aliases in FIELD_ALIASES.items(): if col_clean in aliases or col_clean.startswith(standard[:2]): rename_map[col] standard break return df.rename(columnsrename_map)这里最微妙的一行是col_clean.startswith(standard[:2])它解决数量kg、数量袋这类带单位后缀的列名。严格相等匹配不上按标准字段前两个字做前缀匹配就够用了。代价是可能误配比如一列叫日期审核也会被映射成日期。所以映射之后要做一次必填字段校验宁可报错让用户换文件也不能让错列数据往下游流。def load_normalized(path: str) - pd.DataFrame: 单Sheet读取入口读取、列名映射、必填字段校验。 df read_lingliao(path, sheet_name0) df normalize_columns(df) required [物料编码, 数量] missing [r for r in required if r not in df.columns] if missing: raise ValueError(f缺少必要字段: {missing}) return df这段代码放在第3章的界面调用入口里。用户在选择Excel文件后如果文件缺列程序会直接弹窗提示而不是等汇总时莫名报错。错误发现得越早定位问题的成本就越低这也是为什么数据清洗一定要前置到读取阶段。3. PyQt5界面搭建从文件选择到预览表格的完整代码界面是毕业设计交付的第一印象。PyQt5的优势是控件齐全、布局简单不用写前端代码就能出桌面程序。窗口不需要多花哨把文件选择、表格预览、汇总按钮、导出按钮四个区域摆对就已经是一个完整的工具。3.1 PyQt5环境准备venv和pip安装先准备一个独立的环境避免把系统Python搞乱。以项目目录下创建虚拟环境为例python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate pip install pyqt5 pandas openpyxlpip install pyqt5会自动拉取Qt运行库和sip这两者版本对应不上时会出现导入失败常见报错是ModuleNotFoundError: No module named PyQt5.sip。此时不要反复重装pyqt5检查pip源、清掉缓存再装通常能解决。PyQt5的版本号到5.15.x之后就不再出新功能毕业项目用这个版本完全够。如果用的是PyCharm在Settings里把Project Interpreter指到刚才的venv即可VSCode需要在设置里选解释器路径然后在终端运行python main.py。3.2 主窗口骨架布局、控件和信号槽先看一张控件速查表代码里出现这些控件时对照着理解PyQt5控件作用本工具里的用途QMainWindow主窗口容器承载整个界面QPushButton按钮打开文件、开始汇总、导出结果QLabel文本标签显示当前文件状态QTableWidget表格控件预览明细和汇总结果QFileDialog文件对话框选择源Excel、选择保存路径QMessageBox弹窗报告错误和完成提示QVBoxLayout / QHBoxLayout布局器控件纵向和横向排列主窗口完整代码如下可以直接保存为main.py运行import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem) from loader import load_normalized # 第2章实现的读取函数 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(领料明细汇总工具) self.resize(1000, 620) self.df None # 原始明细 self.result_df None # 汇总结果 central QWidget() self.setCentralWidget(central) layout QVBoxLayout(central) top QHBoxLayout() self.btn_open QPushButton(选择领料Excel) self.btn_open.clicked.connect(self.open_file) self.label_status QLabel(还未读取文件) top.addWidget(self.btn_open) top.addWidget(self.label_status) top.addStretch() self.table QTableWidget() self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setAlternatingRowColors(True) self.table.horizontalHeader().setStretchLastSection(True) layout.addLayout(top) layout.addWidget(self.table) bottom QHBoxLayout() self.btn_summary QPushButton(开始汇总) self.btn_export QPushButton(导出汇总Excel) self.btn_summary.setEnabled(False) self.btn_export.setEnabled(False) self.btn_summary.clicked.connect(self.do_summary) self.btn_export.clicked.connect(self.export_result) bottom.addWidget(self.btn_summary) bottom.addWidget(self.btn_export) bottom.addStretch() layout.addLayout(bottom) def open_file(self): path, _ QFileDialog.getOpenFileName( self, 选择领料明细, , Excel文件 (*.xlsx *.xls) ) if not path: return try: self.df load_normalized(path) except Exception as exc: QMessageBox.critical(self, 读取失败, str(exc)) return self.show_df(self.df.head(300)) # 只预览前300行避免界面卡顿 self.label_status.setText(f已读取 {len(self.df)} 行) self.btn_summary.setEnabled(True) self.btn_export.setEnabled(True) def show_df(self, df): self.table.setRowCount(df.shape[0]) self.table.setColumnCount(df.shape[1]) self.table.setHorizontalHeaderLabels(list(df.columns)) for i, row in df.iterrows(): for j, val in enumerate(row): self.table.setItem(i, j, QTableWidgetItem(str(val))) def do_summary(self): self.result_df aggregate(self.df) # 第4章的聚合函数 self.show_df(self.result_df) self.label_status.setText(f汇总完成共 {len(self.result_df)} 行) def export_result(self): if self.result_df is None: QMessageBox.information(self, 提示, 请先开始汇总) return save_path, _ QFileDialog.getSaveFileName( self, 保存汇总结果, 领料汇总结果.xlsx, Excel文件 (*.xlsx) ) if save_path: export_to_excel(self.result_df, self.df, save_path) QMessageBox.information(self, 完成, f已导出到 {save_path}) def main(): app QApplication(sys.argv) win MainWindow() win.show() sys.exit(app.exec_()) if __name__ __main__: main()这段代码里有几个关键细节。setEditTriggers(NoEditTriggers)把表格设为只读防止用户把预览数据当成电子表格误改预览的目的是核对不是输入。setSelectionBehavior(SelectRows)设置整行选中查看长表格时不容易看串行。head(300)控制预览行数QTableWidget写入几千行数据后界面会明显卡顿只展示前300行能保障流畅度。open_file里先调用load_normalized做字段校验文件缺列时QMessageBox直接提示不再继续执行。do_summary把聚合结果存到result_df导出按钮只消费这份结果不会重复聚合。窗口跑通后这就已经是一个能用的最小工具选文件、看预览、点汇总、导出Excel。把这个流程走顺后面所有逻辑调整都有界面做验证基础。4. 汇总逻辑实现多Sheet合并、groupby聚合和生成Excel结果界面搭好只是第一步真正的业务价值在数据处理这一章。答辩时被问最多的你是怎么汇总的答案都在这里。4.1 多Sheet自动合并替换打开文件时的读取入口一份领料簿里通常有多个工作表一月、二月、三月或者一车间、二车间、三车间。这些Sheet都要参与汇总但有些Sheet不该参与比如汇总、模板这类说明表。读取前先拿到所有Sheet名逐个过滤后纵向拼接def load_all_sheets(path: str) - pd.DataFrame: 读取Excel文件中所有业务Sheet纵向拼接成一个DataFrame。 xl pd.ExcelFile(path, engineopenpyxl) frames [] for sheet in xl.sheet_names: if sheet.startswith(汇总) or sheet.startswith(模板): continue df read_lingliao(path, sheet_namesheet) df normalize_columns(df) # 每个Sheet都做列名映射 frames.append(df) if not frames: raise ValueError(没有找到可参与汇总的工作表) return pd.concat(frames, ignore_indexTrue)pd.ExcelFile在这里的作用是先读取工作簿的Sheet元数据拿到sheet_names属性不把整个文件内容都读进内存。判断用startswith而不是in是为了避免XX车间汇总表这类名字被误读成业务数据。concat时ignore_indexTrue表示拼接后的行索引重新从0编号否则多个Sheet原本各自连续的索引会重复后续按行号定位会错位。不同Sheet的列结构可能不完全一样。concat默认做外连接列取并集缺失列补NaN。正常情况下每个Sheet经过normalize_columns后标准字段一致多余列的差异不影响汇总。把主窗口open_file里的load_normalized(path)换成load_all_sheets(path)多Sheet合并就接入界面了。4.2 数量列清洗从50袋、约20到能求和的数字领料单的数量列是文本和数字混合的重灾区有人填50袋有人填约20有人填12.5kg。这一列直接astype(float)会抛异常需要用正则先抽取数字部分def clean_quantity(df: pd.DataFrame) - pd.DataFrame: 把数量列清洗成数值列同时标识无法解析的行。 raw df[数量].astype(str).str.replace(,, , regexFalse) nums raw.str.extract(r(-?\d(?:\.\d)?))[0] df[数量_数值] pd.to_numeric(nums, errorscoerce) df[数量_异常] df[数量_数值].isna() return df正则是(-?\d(?:\.\d)?)含义是可选的负号开头、一组数字、可选的小数点和小数部分。先用replace把千分位逗号去掉再extract取出第一个数字片段单位后缀和干扰文本都不需要关心。pd.to_numeric配合errorscoerce会让解析失败的值变成NaN而不是抛异常数量_异常列由此标出问题行。紧接着做一次拦截校验def check_quantity(df: pd.DataFrame): bad df.loc[df[数量_异常], [车间, 物料编码, 数量]] if not bad.empty: raise ValueError(f{len(bad)} 行数量无法解析请检查原始文件)这一步强制在汇总前停止流程。问题数据一旦静默流进求和结果月底对账时少了几十行再回溯会非常被动。4.3 groupby聚合按车间和物料编码汇总数量常用的汇总口径是每个车间每种物料累计领了多少。pandas的groupby一行就能写完def aggregate(df: pd.DataFrame) - pd.DataFrame: df clean_quantity(df) check_quantity(df) group_cols [车间, 物料编码, 物料名称, 单位] result ( df.groupby(group_cols, as_indexFalse)[数量_数值] .sum() .sort_values([车间, 数量_数值], ascending[True, False]) ) return result.rename(columns{数量_数值: 累计数量})groupby的关键参数这里拆开说参数 / 写法取值效果group_cols[车间,物料编码,物料名称,单位]分组维度组合值相同的数据归为一组as_indexFalse分组字段保留为普通列写进Excel时表头干净[数量_数值]取列后调用.sum()只对数量求和不会误加其他文本列sort_valuesascending[True, False]车间名升序组内数量降序注意为什么把单位也放进分组维度。同一物料可能在不同车间被登记成不同单位比如一个车间记千克一个车间记克直接求和没有意义。如果检查时发现单位混乱需要先做单位换算再聚合否则结果只能当参考。答辩时主动提这个细节比背概念更有说服力。4.4 导出Excel汇总结果和清洗后明细写入同一工作簿导出端用pandas的ExcelWriter把汇总结果和清洗后的明细分别写到两个Sheet用户拿到文件后既能看汇总又能下钻到原始记录核对def export_to_excel(result: pd.DataFrame, detail: pd.DataFrame, save_path: str) - None: 把汇总结果和清洗后的明细导出到同一个Excel文件。 with pd.ExcelWriter(save_path, engineopenpyxl) as writer: result.to_excel(writer, sheet_name按物料汇总, indexFalse) detail.to_excel(writer, sheet_name清洗后明细, indexFalse) sheet writer.sheets[按物料汇总] for col_idx, name in enumerate(result.columns): col_letter chr(65 col_idx) # 列号转列名字母 A、B、C... width max(10, len(str(name)) * 2) # 列宽按表头长度估算 sheet.column_dimensions[col_letter].width width多个Sheet必须写在同一个with块里pd.ExcelWriter在with块结束时统一刷新到磁盘如果分两次打开同一个路径第二次会覆盖第一次的内容。chr(65 col_idx)把列索引映射成Excel列号0对应A、1对应B超过26列会越界但领料汇总结果通常不到10列。列宽用表头长度乘2估算中文表头基本能完整显示之后字段顺序变了也自动跟着调整不用回来改写死的数字。5. 汇总逻辑验证、PyInstaller打包与演示技巧到这一步工具功能已经完整。最后一节处理三件影响交付的事怎么证明汇总结果是对的、怎么打包成exe、答辩演示怎么准备。5.1 用单测验证聚合函数aggregate是纯函数不依赖界面直接做单元测试。用一个只有三行数据的DataFrame就能确认groupby逻辑没写错def test_aggregate_sums_same_material(): df pd.DataFrame({ 车间: [A, A, B], 物料编码: [M01, M01, M02], 物料名称: [螺丝, 螺丝, 垫片], 单位: [个, 个, 个], 数量: [10, 20, 5], }) res aggregate(df) row_m01 res.loc[res[物料编码] M01].iloc[0] assert row_m01[累计数量] 30这个用例里数量列故意用字符串同时验证clean_quantity的清洗逻辑。跑测试只需要在项目目录执行pip install pytest pytest test_summary.py -v断言失败时优先排查两处数量列的正则是否匹配、groupby的分组列是否被映射干净。答辩前把pytest跑一遍比临时在界面上点点点可靠得多。5.2 PyInstaller打包一包运行文件毕业设计最后要交可执行文件否则评审老师还得装Python环境。用PyInstaller打包pip install pyinstaller pyinstaller --windowed --onefile --name 领料汇总工具 main.py--windowed表示不显示黑色控制台窗口--onefile把所有依赖打成一个exe产物在dist目录下双击即可运行。首次启动会有几秒解压等待这是onefile模式的正常现象答辩时提前开机再演示就不用等。如果exe运行时报could not find or load the Qt platform plugin windows说明Qt插件没打全重打成这样pyinstaller --windowed --onefile --collect-all PyQt5 --name 领料汇总工具 main.py--collect-all会把Qt的插件和翻译文件全部收集进来体积会变大但换来了稳定运行。打包出的exe通常在60MB到80MB交付形式完全够用。5.3 答辩演示准备三步走演示时建议按固定套路来第一步准备一个200行以内、格式规范但故意带一两处别名表头或空行的样例Excel展示读取的容错能力第二步演示顺序固定为打开文件、预览、汇总、导出、打开结果Excel看两个Sheet第三步如果演示机没装Office演示前先装WPS或准备好在线Excel预览地址导出后要当场打开按物料汇总和清洗后明细两个Sheet证明结果文件真实可读。最后这一点不是技术问题却是最容易让演示效果打折扣的地方。本文还有配套的精品资源点击获取