news 2026/9/16 15:07:41

PyQt5+Excel:领料明细汇总工具的完整开发实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyQt5+Excel:领料明细汇总工具的完整开发实践

简介:这是一套基于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_name=0, header_row: int = 0) -> pd.DataFrame: """读取领料明细Excel,返回规整的DataFrame。""" df = pd.read_excel( path, sheet_name=sheet_name, # 0表示第一个Sheet,也可传工作表名称字符串 header=header_row, # 表头所在行,常见表头在第1行,有的在第2行 dtype=str, # 先全部按字符串读入,避免日期和编码被自动转换 ) df.columns = [str(c).strip() for c in df.columns] # 列名首尾空格去掉 df = df.dropna(how="all") # 整行全为空的行直接删除 return df

三个参数值得展开说。第一个是dtype=str,这是从真实领料单里踩出来的坑:同一列单元格混着"20240510"和"2024-05-10",如果不强制字符串,pandas会把它们解析成不同类型,后面按日期聚合时会多出很多组。第二个是header_row,真实领料单的表头经常从第二行开始,第一行是"XX车间2024年6月领料记录"这类大标题,到时候只要传header_row=1。第三个是dropna(how="all"),领料单里插入整行空行很常见,不删掉的话groupby时可能产生一个键全是NaN的分组。

如果sheet_name传None,pandas返回的是一个字典,键是工作表名称,值是每个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(columns=rename_map)

这里最微妙的一行是col_clean.startswith(standard[:2]),它解决"数量kg"、"数量(袋)"这类带单位后缀的列名。严格相等匹配不上,按标准字段前两个字做前缀匹配就够用了。代价是可能误配,比如一列叫"日期审核"也会被映射成"日期"。所以映射之后要做一次必填字段校验,宁可报错让用户换文件,也不能让错列数据往下游流。

def load_normalized(path: str) -> pd.DataFrame: """单Sheet读取入口:读取、列名映射、必填字段校验。""" df = read_lingliao(path, sheet_name=0) 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 openpyxl

pip 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, engine="openpyxl") frames = [] for sheet in xl.sheet_names: if sheet.startswith("汇总") or sheet.startswith("模板"): continue df = read_lingliao(path, sheet_name=sheet) df = normalize_columns(df) # 每个Sheet都做列名映射 frames.append(df) if not frames: raise ValueError("没有找到可参与汇总的工作表") return pd.concat(frames, ignore_index=True)

pd.ExcelFile在这里的作用是先读取工作簿的Sheet元数据,拿到sheet_names属性,不把整个文件内容都读进内存。判断用startswith而不是in,是为了避免"XX车间汇总表"这类名字被误读成业务数据。concat时ignore_index=True表示拼接后的行索引重新从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(",", "", regex=False) nums = raw.str.extract(r"(-?\d+(?:\.\d+)?)")[0] df["数量_数值"] = pd.to_numeric(nums, errors="coerce") df["数量_异常"] = df["数量_数值"].isna() return df

正则是(-?\d+(?:\.\d+)?),含义是:可选的负号开头、一组数字、可选的小数点和小数部分。先用replace把千分位逗号去掉,再extract取出第一个数字片段,单位后缀和干扰文本都不需要关心。pd.to_numeric配合errors="coerce"会让解析失败的值变成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_index=False)["数量_数值"] .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, engine="openpyxl") as writer: result.to_excel(writer, sheet_name="按物料汇总", index=False) detail.to_excel(writer, sheet_name="清洗后明细", index=False) 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,证明结果文件真实可读。最后这一点不是技术问题,却是最容易让演示效果打折扣的地方。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 15:04:39

使用wechatapi把微信接到 OpenClaw,我踩过的 7 个坑

技术支持 wechatapi.net 最近在把微信接到 OpenClaw,想做一个真正能在微信里使用的 Agent 入口。 这件事看起来好像只是: 收消息 -> 调 OpenClaw -> 发回去但实际做起来,坑比想象中多得多。 下面我把目前踩过的 7 个最典型的坑总结一下…

作者头像 李华
网站建设 2026/9/16 15:02:20

MATLAB相场法凝固组织模拟:从方程到代码实现

简介:一份基于MATLAB的共晶凝固相场法模拟程序,面向材料成型、计算材料学方向的科研人员及相关专业高年级本科生。程序围绕二元共晶合金凝固过程中的两相竞争生长场景,将相场变量演化与溶质扩散方程耦合,通过自由能驱动、相判断、…

作者头像 李华
网站建设 2026/9/16 14:59:42

akshare 0.6.61源码包下载安装与版本锁定实践

简介:akshare-0.6.61.tar.gz 是 PyPI 官方发布的一个 Python 库源码包,专为需要批量获取中国金融行情数据的 Python 开发者、量化交易爱好者和数据分析人员准备。包内包含 236 个文件,压缩后仅 353KB,其中以 216 个 py 源码文件为…

作者头像 李华
网站建设 2026/9/16 14:58:38

微信小程序期末项目:天使童装商城实战指南

简介:这是一份面向高校计算机专业学生及微信小程序初学者的期末大作业实战项目,聚焦电商类应用开发能力培养,帮助学习者系统掌握小程序核心开发流程与典型业务实现。资源为ZIP压缩包,大小2.18MB,包含完整可运行的小程序…

作者头像 李华