Python 与 VBA:工业工程自动化的两条路径怎么选
引言:一个 200 小时的问题
先看一个 IE 的日常:
每周一早上,需要做上周的生产周报。流程是:
- 从 MES 导出 5 个 Excel 文件(产量、停机、质量、人员、物料)
- 逐个打开,筛选上周日期,删除无关列
- 复制粘贴到一个汇总表
- 做透视表,算各产线的产量、良率、OEE
- 按格式排版,加图表
- 生成 PDF,发给 12 个人
耗时:3~4 小时。每周如此。
一年 50 周,就是 150~200 小时,相当于 20~25 个工作日。换句话说,这个 IE 每年有一个多月的时间,在做纯粹的复制粘贴。
而这还只是一份报表。加上日报、月报、临时取数、数据核对……IE 花在"搬运数据"上的时间,往往超过花在"分析数据"上的时间。
这类工作的共同特征是:规则固定、重复执行、耗时但不产生判断。这正是自动化的最佳目标。
本文要解决的就是:用 VBA 还是 Python?各自能做什么?怎么开始?
一、选型:VBA 还是 Python
1.1 一句话结论
| 选 VBA | 选 Python | |
|---|---|---|
| 场景 | 操作 Excel 本身(格式、单元格、图表、窗体) | 处理数据(清洗、计算、分析、跨系统) |
| 用户 | 只会 Excel 的业务人员 | 愿意学编程、要做复杂处理 |
| 部署 | 零部署(在 Excel 里就能跑) | 需安装环境(但也可以打包) |
| 学习曲线 | 低(会 Excel 就能入门) | 中(需要理解编程概念) |
| 生态 | 弱(只有 Office) | 极强(数据分析、机器学习、Web、数据库…) |
1.2 详细对比
| 维度 | VBA | Python |
|---|---|---|
| 出身 | 1993 年随 Excel 5.0 发布,微软官方 Office 自动化语言 | 1991 年发布,通用编程语言 |
| 运行环境 | 内置于 Office,无需安装 | 需安装 Python 解释器 + 库 |
| 处理 Excel | 原生、最强大(能操作 Excel 的一切:格式、图表、透视表、条件格式、窗体) | 通过库(openpyxl 读写、pandas 处理),格式控制弱于 VBA |
| 数据处理能力 | 弱(数组、集合,没有数据框) | 极强(pandas 是数据分析的事实标准) |
| 跨系统能力 | 弱(能连数据库但麻烦,能调用 API 但麻烦) | 强(数据库、Web API、爬虫、文件系统、邮件全都有成熟库) |
| 统计与建模 | 几乎要自己写 | 丰富(scipy、statsmodels、sklearn) |
| 代码量 | 相对冗长 | 简洁 |
| 调试 | VBE 编辑器(老旧但能用) | 现代 IDE(VS Code、PyCharm)+ Jupyter |
| 可维护性 | 差(代码嵌在 Excel 文件里,版本管理困难) | 好(纯文本 .py 文件,可 git 管理) |
| 运行速度 | 慢(单元格逐格操作极慢,需用数组优化) | 快(pandas 向量化,C 实现) |
| 安全性 | 宏病毒风险,企业常禁用宏 | 相对安全(但也可执行任意代码) |
| 趋势 | 微软不再重点发展(但仍长期支持) | 持续火热,生态爆炸 |
1.3 决策树
你的任务是什么?
│
├─ 操作 Excel 本身(改格式、生成图表、做透视表、做交互窗体)
│ └─ 你在意格式的精确控制吗?
│ ├─ 是 → VBA(或 Python + VBA 混合)
│ └─ 否 → Python(openpyxl)
│
├─ 处理数据(清洗、合并、计算、分析)
│ └─ 数据量多大?
│ ├─ < 10 万行,且是一次性 → Excel 手工/公式也许够
│ ├─ 任意量,需要重复执行 → Python(pandas)
│ └─ 在 Excel 内完成,且要给用户一个"按钮" → VBA
│
├─ 跨系统(连数据库、调 API、读写文件、发邮件)
│ └─ Python
│
├─ 统计分析(回归、检验、时间序列、机器学习)
│ └─ Python
│
└─ 给不懂技术的同事用
├─ 他只会 Excel → 做成带按钮的 Excel 文件(VBA)或独立的 exe
└─ 能用浏览器 → 做成 Web 应用(Python + Streamlit/Gradio)
1.4 最实用的答案:都要,但侧重不同
现实中最高效的工作方式是混合:
Python 负责"重活":
- 从数据库取数
- 清洗、合并、计算(pandas)
- 生成分析结果
Excel/VBA 负责"交付":
- 接收 Python 输出的数据
- 用 Excel 的格式、透视表、图表呈现
- 给用户熟悉的界面和交互
典型架构:
[数据库/ERP/MES]
↓ Python(定时运行)
[中间数据 CSV/Excel]
↓ Excel(Power Query 连接)
[报表/看板]
或者更彻底的自动化:Python 直接生成格式化的 Excel 报表(用 openpyxl 或 xlwings),甚至直接生成 PDF 并邮件发送。
二、VBA 核心能力
2.1 入门:宏录制
宏录制是 VBA 最好的老师。
操作:开发工具 → 录制宏 → 手动做一遍操作 → 停止录制 → Alt+F11 查看生成的代码。
示例:录制"给 A1:D10 加边框、设表头加粗"的操作,会得到类似代码:
Sub Macro1()
Range("A1:D10").Select
Selection.Borders.LineStyle = xlContinuous
Range("A1:D1").Select
Selection.Font.Bold = True
End Sub
录制代码的问题:大量 .Select 和 .Selection,冗长且低效。学会后可以手动优化:
' 优化后(去掉 Select)
Sub FormatTable()
With Range("A1:D10")
.Borders.LineStyle = xlContinuous
End With
Range("A1:D1").Font.Bold = True
End Sub
2.2 核心对象模型
Excel VBA 的对象层次:
Application(Excel 应用)
└── Workbook(工作簿)
└── Worksheet(工作表)
└── Range(单元格区域)
├── Cells(row, col)
├── Value(值)
├── Formula(公式)
├── Interior.Color(背景色)
└── Font(字体)
最常用的是 Range 对象:
' 引用单元格的几种方式
Range("A1") ' A1 单元格
Range("A1:B10") ' 区域
Cells(1, 1) ' 第1行第1列 = A1
Cells(1, "A") ' 同上
Range("A1", "B10") ' 起止区域
[A1] ' 简写
' 常用操作
Range("A1").Value = 100 ' 写值
x = Range("A1").Value ' 读值
Range("A1").Formula = "=SUM(B1:B10)" ' 写公式
Range("A1:D1").Font.Bold = True ' 加粗
Range("A1:D1").Interior.Color = RGB(200, 200, 200) ' 背景色
Range("A:A").ColumnWidth = 12 ' 列宽
Range("A1").NumberFormat = "0.00%" ' 数字格式
Range("A1").ClearContents ' 清除内容
Range("A1").EntireRow.Delete ' 删除整行
' 找到最后一行(极其常用)
lastRow = Cells(Rows.Count, 1).End(xlUp).Row
lastCol = Cells(1, Columns.Count).End(xlToLeft).Column
Cells(Rows.Count, 1).End(xlUp).Row 是 VBA 中最常用的技巧之一,用于动态获取数据区域的最后一行。相当于在 A 列最后一个单元格按 Ctrl+↑。
2.3 变量、循环与判断
Sub Demo()
' 变量声明
Dim ws As Worksheet
Dim lastRow As Long, i As Long
Dim total As Double
Dim name As String
Set ws = ThisWorkbook.Sheets("数据表") ' 对象赋值要用 Set
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
' For 循环
total = 0
For i = 2 To lastRow ' 从第2行到最后一行(跳过表头)
total = total + ws.Cells(i, 3).Value
Next i
' 判断
For i = 2 To lastRow
If ws.Cells(i, 3).Value > 1000 Then
ws.Cells(i, 4).Value = "高"
ElseIf ws.Cells(i, 3).Value > 500 Then
ws.Cells(i, 4).Value = "中"
Else
ws.Cells(i, 4).Value = "低"
End If
Next i
' For Each 循环
Dim cell As Range
For Each cell In ws.Range("A2:A100")
If IsEmpty(cell) Then cell.Interior.Color = vbYellow
Next cell
' Do While
i = 2
Do While ws.Cells(i, 1).Value <> ""
i = i + 1
Loop
End Sub
2.4 性能:数组是关键
VBA 逐格操作单元格极慢。处理 1 万行数据,逐格读写可能需要几分钟;用数组则只要零点几秒。
' 慢:逐格读写
For i = 1 To 10000
ws.Cells(i, 2).Value = ws.Cells(i, 1).Value * 2
Next i
' 快:一次性读入数组,处理,一次性写回
Dim arr As Variant
arr = ws.Range("A1:A10000").Value ' 一次性读入(二维数组)
Dim i As Long
For i = 1 To UBound(arr, 1)
arr(i, 1) = arr(i, 1) * 2
Next i
ws.Range("B1:B10000").Value = arr ' 一次性写回
性能提速的四个技巧:
Sub FastMacro()
Application.ScreenUpdating = False ' 关闭屏幕刷新(最重要)
Application.Calculation = xlCalculationManual ' 手动计算
Application.EnableEvents = False ' 关闭事件触发
Application.DisplayAlerts = False ' 关闭警告提示
' ... 你的代码 ...
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Application.DisplayAlerts = True
End Sub
注意:如果代码中可能出错,要用错误处理确保这些设置被恢复,否则 Excel 会处于"卡死"状态。
Sub SafeMacro()
On Error GoTo CleanUp
Application.ScreenUpdating = False
' ... 代码 ...
CleanUp:
Application.ScreenUpdating = True
If Err.Number <> 0 Then MsgBox "出错: " & Err.Description
End Sub
2.5 事件(自动化触发)
工作表和工作簿事件可以自动响应操作:
' 放在工作表代码页(如 Sheet1)
Private Sub Worksheet_Change(ByVal Target As Range)
' 当 A 列变化时,自动记录修改时间到 B 列
If Not Intersect(Target, Range("A:A")) Is Nothing Then
Application.EnableEvents = False
Cells(Target.Row, 2).Value = Now
Application.EnableEvents = True
End If
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
' 选中单元格时执行
End Sub
' 放在 ThisWorkbook 代码页
Private Sub Workbook_Open()
' 打开文件时执行(如自动刷新数据)
MsgBox "欢迎使用生产报表模板"
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
' 关闭前执行(如自动保存备份)
End Sub
注意:在事件处理过程中修改单元格会再次触发事件,造成无限循环。必须用 Application.EnableEvents = False 保护(如上例)。
2.6 用户窗体(UserForm)
给非技术用户做界面的方式:
- 在 VBE 中
插入 → 用户窗体 - 拖放控件(文本框、按钮、下拉框、复选框)
- 写事件代码
' 按钮点击事件
Private Sub cmdRun_Click()
Dim startDate As Date
startDate = CDate(txtStartDate.Value)
If Not IsDate(txtStartDate.Value) Then
MsgBox "请输入有效日期", vbExclamation
Exit Sub
End If
Call GenerateReport(startDate)
MsgBox "报表生成完成", vbInformation
Unload Me
End Sub
更简单的方式:不用 UserForm,直接在 Excel 里用单元格做"输入区",加一个形状(Shape)作为按钮,右键"指定宏"。这样用户更容易理解。
2.7 常用代码片段
' ① 遍历文件夹里的所有 Excel 文件并合并
Sub MergeFiles()
Dim fso As Object, folder As Object, file As Object
Dim wb As Workbook, ws As Worksheet
Dim targetRow As Long
Set fso = CreateObject("Scripting.FileSystemObject")
Set folder = fso.GetFolder("D:\数据\")
targetRow = 2
For Each file In folder.Files
If LCase(fso.GetExtensionName(file.Name)) = "xlsx" Then
Set wb = Workbooks.Open(file.Path)
Set ws = wb.Sheets(1)
' 复制数据
ws.Range("A2:D100").Copy
ThisWorkbook.Sheets("汇总").Cells(targetRow, 1).PasteSpecial xlPasteValues
targetRow = targetRow + 99
wb.Close False
End If
Next file
End Sub
' ② 拆分工作表为多个文件(按某列的值)
Sub SplitByColumn()
Dim ws As Worksheet, dict As Object
Dim lastRow As Long, i As Long, key As String
Set ws = ThisWorkbook.Sheets("数据")
Set dict = CreateObject("Scripting.Dictionary")
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
' 按 A 列分组
For i = 2 To lastRow
key = ws.Cells(i, 1).Value
If Not dict.Exists(key) Then dict.Add key, New Collection
dict(key).Add i
Next i
' 为每个 key 创建新表
Dim k As Variant, newWs As Worksheet, r As Variant, outRow As Long
For Each k In dict.Keys
Set newWs = ThisWorkbook.Sheets.Add
newWs.Name = Left(CStr(k), 30)
ws.Rows(1).Copy newWs.Rows(1) ' 复制表头
outRow = 2
For Each r In dict(k)
ws.Rows(r).Copy newWs.Rows(outRow)
outRow = outRow + 1
Next r
Next k
End Sub
' ③ 自动生成图表
Sub MakeChart()
Dim cht As Chart
Set cht = ThisWorkbook.Charts.Add
With cht
.SetSourceData Source:=Range("数据!A1:B20")
.ChartType = xlLine
.HasTitle = True
.ChartTitle.Text = "产量趋势"
.Axes(xlCategory).HasTitle = True
.Axes(xlCategory).AxisTitle.Text = "日期"
.Axes(xlValue).HasTitle = True
.Axes(xlValue).AxisTitle.Text = "产量"
End With
End Sub
' ④ 导出为 PDF
Sub ExportPDF()
ThisWorkbook.Sheets("报表").ExportAsFixedFormat _
Type:=xlTypePDF, _
Filename:="D:\输出\生产周报_" & Format(Date, "yyyymmdd") & ".pdf", _
Quality:=xlQualityStandard
End Sub
' ⑤ 发送邮件(Outlook)
Sub SendMail()
Dim olApp As Object, olMail As Object
Set olApp = CreateObject("Outlook.Application")
Set olMail = olApp.CreateItem(0)
With olMail
.To = "manager@company.com"
.CC = "team@company.com"
.Subject = "生产周报 - " & Format(Date, "yyyy-mm-dd")
.Body = "请查收附件。"
.Attachments.Add "D:\输出\生产周报.pdf"
.Send ' 或 .Display 先预览
End With
End Sub
2.8 VBA 的局限与未来
微软对 VBA 的态度:仍然支持,但不再发展新功能。宏安全策略越来越严(默认禁用宏,打开带宏文件需要手动"启用内容")。
实务影响:
- 企业环境常禁用宏 → 你的工具发给同事打不开/不能用
- 文件要存为
.xlsm格式(不是.xlsx) - 代码嵌在文件里,版本管理困难
建议:
- 个人效率工具 → VBA 完全够用,继续用
- 要分发给别人的工具 → 考虑 Python + 打包成 exe,或考虑 Office 脚本(Office Scripts,新一代自动化,基于 TypeScript,可在 Excel 网页版运行)
- 长期、重要的工具 → Python
三、Python 工业工程数据处理栈
3.1 环境搭建
推荐方式:Anaconda 或 Miniconda
- Miniconda(推荐):精简版,只含 Python 和 conda,需要什么装什么
- Anaconda:完整版,预装 200+ 科学计算包,体积大约 3GB
安装后创建环境:
conda create -n ie python=3.11
conda activate ie
conda install pandas openpyxl numpy matplotlib
# 或
pip install pandas openpyxl numpy matplotlib
开发工具:
| 工具 | 适合 |
|---|---|
| VS Code | 通用,轻量,推荐 |
| PyCharm | 专业 Python IDE |
| Jupyter Notebook / JupyterLab | 数据分析探索,强烈推荐(可交互、可边写边看结果) |
| Spyder | 类似 MATLAB 的科学计算 IDE(Anaconda 自带) |
给 IE 的建议:用 Jupyter Notebook 入门。它的"分单元格执行 + 即时看结果"特性,非常适合数据探索——你读入一批数据,一步步处理,每步都能立刻看到中间结果。这比写完整脚本再运行要高效得多。
3.2 核心库
| 库 | 用途 | IE 场景 |
|---|---|---|
| pandas | 数据框(DataFrame)处理 | 核心中的核心,一切数据处理 |
| numpy | 数值计算、数组 | 矩阵运算、随机数、仿真 |
| openpyxl | 读写 .xlsx(保留格式) | 生成格式化 Excel 报表 |
| xlrd / xlwings | 读 Excel / 控制 Excel 应用 | xlwings 可在 Python 里操控 Excel |
| matplotlib | 绘图 | 基础图表 |
| seaborn | 统计图表 | 更好看的统计图 |
| plotly | 交互式图表 | 可交互的看板 |
| scipy | 科学计算、统计检验 | t 检验、分布拟合、优化 |
| statsmodels | 统计建模 | 回归、时间序列 |
| scikit-learn | 机器学习 | 预测、分类、聚类 |
| simpy | 离散事件仿真 | 轻量仿真 |
| pyodbc / sqlalchemy | 数据库连接 | 从 MES/ERP 取数 |
| requests | HTTP 请求 | 调用 API、爬虫 |
| schedule / APScheduler | 定时任务 | 自动跑报表 |
| smtplib / yagmail | 发邮件 | 自动发送报表 |
3.3 pandas 核心操作
DataFrame 是 pandas 的核心数据结构——可以理解为"Excel 的一张表,但有行索引和列名,且能高效处理百万行"。
import pandas as pd
# 读取
df = pd.read_excel("生产数据.xlsx", sheet_name="报工")
df = pd.read_csv("数据.csv", encoding="utf-8-sig") # 中文 CSV 注意编码
df = pd.read_sql("SELECT * FROM mes_report", conn)
# 查看
df.head() # 前 5 行
df.tail(10) # 后 10 行
df.info() # 列类型、非空数
df.describe() # 数值列的统计摘要
df.shape # (行数, 列数)
df.columns # 列名
df.dtypes # 数据类型
# 选择
df["产量"] # 一列(Series)
df[["产量", "良品数"]] # 多列
df.loc[0:10, ["产量", "日期"]] # 按标签选择
df.iloc[0:10, 0:3] # 按位置选择
# 筛选
df[df["产量"] > 1000]
df[(df["产线"] == "A线") & (df["产量"] > 1000)]
df[df["产品"].isin(["A001", "A002"])]
df[df["备注"].isna()] # 空值
df[~df["备注"].isna()] # 非空
df[df["工单号"].str.contains("WO2026")] # 包含
# 新增/修改列
df["良率"] = df["良品数"] / (df["良品数"] + df["不良数"])
df["日期"] = pd.to_datetime(df["日期"])
df["月份"] = df["日期"].dt.month
df["是否延期"] = df["实际完工"] > df["计划完工"]
# 分组聚合
df.groupby("产线")["产量"].sum()
df.groupby(["产线", "月份"])["产量"].agg(["sum", "mean", "count"])
df.groupby("产线").agg({
"产量": "sum",
"不良数": "sum",
"工时": "mean"
})
# 透视表
pd.pivot_table(df, values="产量", index="产线", columns="月份", aggfunc="sum")
# 排序
df.sort_values("产量", ascending=False)
df.sort_values(["产线", "日期"])
# 合并
pd.merge(df1, df2, on="工单号", how="left") # 类似 SQL JOIN
pd.concat([df1, df2, df3]) # 纵向拼接
# 处理缺失
df.dropna() # 删除含空值的行
df.dropna(subset=["产量"]) # 只删除"产量"为空的行
df.fillna(0) # 填充为 0
df["产量"] = df["产量"].fillna(df["产量"].mean()) # 均值填充
# 去重
df.drop_duplicates()
df.drop_duplicates(subset=["工单号"], keep="last")
# 类型转换
df["产量"] = df["产量"].astype(int)
df["日期"] = pd.to_datetime(df["日期"], errors="coerce") # 转换失败的变 NaT
# 重命名列
df.rename(columns={"旧名": "新名"})
# 应用函数
df["等级"] = df["产量"].apply(lambda x: "高" if x > 1000 else "低")
df["良率"] = df.apply(lambda row: row["良品"] / row["总数"], axis=1)
# 写出
df.to_excel("输出.xlsx", index=False)
df.to_csv("输出.csv", index=False, encoding="utf-8-sig")
pandas 与 SQL 的对应关系(帮助理解):
| SQL | pandas |
|---|---|
SELECT col FROM t |
df["col"] |
WHERE 条件 |
df[条件] |
GROUP BY + SUM |
df.groupby("k")["v"].sum() |
JOIN |
pd.merge() |
ORDER BY |
df.sort_values() |
DISTINCT |
df.drop_duplicates() |
CASE WHEN |
np.where() / .apply() |
窗口函数 |
.groupby().transform() / .rolling() |
3.4 openpyxl:生成格式化 Excel
pandas 的 to_excel 只能输出裸数据。要控制格式(颜色、边框、公式、图表、多 sheet),用 openpyxl。
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.chart import LineChart, BarChart, Reference
# 新建工作簿
wb = Workbook()
ws = wb.active
ws.title = "周报"
# 写数据(从 DataFrame)
df = pd.read_excel("源数据.xlsx")
for r in dataframe_to_rows(df, index=False, header=True):
ws.append(r)
# 样式
header_font = Font(bold=True, color="FFFFFF", size=11)
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
thin_border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin")
)
# 应用表头样式
for cell in ws[1]:
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center", vertical="center")
# 应用边框
for row in ws.iter_rows(min_row=1, max_row=ws.max_row, max_col=ws.max_column):
for cell in row:
cell.border = thin_border
# 列宽
ws.column_dimensions["A"].width = 15
for col in ws.columns:
max_len = max(len(str(c.value or "")) for c in col)
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 30)
# 数字格式
for cell in ws["D"][1:]:
cell.number_format = "0.00%"
# 条件格式:低于目标的标红
from openpyxl.formatting.rule import CellIsRule
ws.conditional_formatting.add(
"D2:D100",
CellIsRule(operator="lessThan", formula=["0.95"],
fill=PatternFill(start_color="FFC7CE"))
)
# 冻结首行
ws.freeze_panes = "A2"
# 图表
chart = LineChart()
chart.title = "产量趋势"
chart.y_axis.title = "产量"
chart.x_axis.title = "日期"
data = Reference(ws, min_col=2, min_row=1, max_row=ws.max_row)
cats = Reference(ws, min_col=1, min_row=2, max_row=ws.max_row)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "H2")
# 保存
wb.save("生产周报.xlsx")
关键技巧:
- 先做好一个 Excel 模板(含格式、图表、公式),Python 只往里填数据 → 比代码生成格式简单得多
- 用 openpyxl 的
load_workbook打开模板,写入数据,save成新文件
3.5 数据清洗七步
真实数据永远是脏的。标准清洗流程:
import pandas as pd
import numpy as np
# ① 读入并初查
df = pd.read_excel("原始数据.xlsx")
print(df.shape, df.columns.tolist())
print(df.isna().sum()) # 各列缺失数
print(df.duplicated().sum()) # 重复行数
# ② 清理列名(去空格、统一命名)
df.columns = df.columns.str.strip()
df = df.rename(columns={"产 量": "产量", "日期时间": "日期"})
# ③ 删除无关列
df = df[["工单号", "产线", "产品", "日期", "产量", "不良数", "工时"]]
# ④ 类型转换
df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
df["产量"] = pd.to_numeric(df["产量"], errors="coerce")
df["产线"] = df["产线"].astype(str).str.strip().str.upper()
# ⑤ 处理缺失与异常
df = df.dropna(subset=["产量", "日期"]) # 关键字段缺失就删
df = df[df["产量"] > 0] # 删除非正值
df = df[df["产量"] < df["产量"].quantile(0.999)] # 删除极端值(或用业务规则)
# ⑥ 去重与标准化
df = df.drop_duplicates(subset=["工单号", "日期"], keep="last")
df["产线"] = df["产线"].replace({"A": "A线", "A线 ": "A线"}) # 统一取值
# ⑦ 派生字段与校验
df["良率"] = df["产量"] / (df["产量"] + df["不良数"])
assert df["良率"].between(0, 1).all(), "良率超出 [0,1] 范围!"
print(f"清洗完成:{len(df)} 行")
第 7 步的 assert 校验很重要。数据清洗一定会出问题,与其让错误流到报表里,不如在处理时就报错。
四、五个可复制的自动化案例
案例 1:多文件合并(日报汇总)
场景:每天从 MES 导出一个 Excel,每月要合并成月报。
import pandas as pd
import glob
import os
# 找到所有文件
files = glob.glob(r"D:\MES导出\*.xlsx")
print(f"找到 {len(files)} 个文件")
# 逐个读取并合并
dfs = []
for f in files:
df = pd.read_excel(f, sheet_name="报工")
df["来源文件"] = os.path.basename(f) # 记录来源,便于追溯
dfs.append(df)
print(f" 读取 {os.path.basename(f)}: {len(df)} 行")
combined = pd.concat(dfs, ignore_index=True)
print(f"合并后共 {len(combined)} 行")
# 去重(如果文件之间有重叠)
combined = combined.drop_duplicates(subset=["工单号", "报工时间"])
# 输出
combined.to_excel(r"D:\输出\月度报工汇总.xlsx", index=False)
变体:如果文件名含日期,可以从文件名提取日期作为一列。
案例 2:BOM 比对(新旧版本差异)
场景:工程变更(ECN)后,需要知道新旧 BOM 的差异。
import pandas as pd
# 读入新旧 BOM
old = pd.read_excel("BOM_旧.xlsx")
new = pd.read_excel("BOM_新.xlsx")
# 统一关键字段
for df in (old, new):
df["物料编码"] = df["物料编码"].astype(str).str.strip()
# 外连接,标记来源
merged = old.merge(new, on="物料编码", how="outer",
suffixes=("_旧", "_新"), indicator=True)
# 分类差异
新增 = merged[merged["_merge"] == "right_only"]
删除 = merged[merged["_merge"] == "left_only"]
共有 = merged[merged["_merge"] == "both"].copy()
# 数量变化
共有["数量变化"] = 共有["用量_新"] - 共有["用量_旧"]
数量变化 = 共有[共有["数量变化"] != 0]
# 输出报告
with pd.ExcelWriter("BOM_差异报告.xlsx") as writer:
新增.to_excel(writer, sheet_name="新增物料", index=False)
删除.to_excel(writer, sheet_name="删除物料", index=False)
数量变化.to_excel(writer, sheet_name="用量变化", index=False)
print(f"新增 {len(新增)} 项,删除 {len(删除)} 项,用量变化 {len(数量变化)} 项")
这个脚本的价值:手工比对两个几百行的 BOM 需要 1~2 小时且容易漏;脚本 10 秒完成且零遗漏。
案例 3:工时数据清洗(剔除异常)
场景:从系统导出的工时数据里有大量异常(负值、超大值、测试记录),需要清洗后才能做标准工时分析。
import pandas as pd
import numpy as np
df = pd.read_excel("工时原始数据.xlsx")
# 记录处理过程
log = {"原始": len(df)}
# 1. 删除测试记录
df = df[~df["工单号"].astype(str).str.contains("TEST|测试|试用", na=False)]
log["删除测试"] = len(df)
# 2. 删除关键字段为空
df = df.dropna(subset=["工时秒", "操作人"])
log["删除空值"] = len(df)
# 3. 删除非正值
df = df[df["工时秒"] > 0]
log["删除非正"] = len(df)
# 4. 用 IQR 方法剔除离群值(按产品-工序分组)
def remove_outliers(group):
q1 = group["工时秒"].quantile(0.25)
q3 = group["工时秒"].quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
return group[(group["工时秒"] >= lower) & (group["工时秒"] <= upper)]
df = df.groupby(["产品编码", "工序"], group_keys=False).apply(remove_outliers)
log["剔除离群"] = len(df)
# 5. 剔除样本量不足的组(<10 条)
counts = df.groupby(["产品编码", "工序"])["工时秒"].transform("count")
df = df[counts >= 10]
log["样本不足"] = len(df)
# 6. 计算标准工时(用中位数更稳健)
std_time = df.groupby(["产品编码", "工序"])["工时秒"].agg([
("样本数", "count"),
("均值", "mean"),
("中位数", "median"),
("标准差", "std"),
("变异系数", lambda x: x.std() / x.mean())
]).round(2)
# 建议标准工时 = 中位数 + 宽放
std_time["建议标准工时秒"] = (std_time["中位数"] * 1.15).round(1) # 15% 宽放
# 输出
with pd.ExcelWriter("标准工时分析.xlsx") as writer:
df.to_excel(writer, sheet_name="清洗后数据", index=False)
std_time.to_excel(writer, sheet_name="标准工时")
pd.Series(log).to_excel(writer, sheet_name="清洗日志", header=["剩余行数"])
print(log)
要点:
- 分组剔除离群值,而不是全局剔除(不同产品的工时量级不同)
- 用中位数而非均值作为标准工时的基础(更稳健,不受极端值影响)
- 变异系数 CV 大的组要重点检查(说明操作不稳定或数据有问题)
- 保留清洗日志,让结果可追溯
案例 4:批量文件处理(重命名/格式转换)
场景:几百个图纸/工艺文件需要按规则重命名,或批量转 PDF。
import os
import re
from pathlib import Path
src_dir = Path(r"D:\工艺文件")
dst_dir = Path(r"D:\工艺文件_已整理")
dst_dir.mkdir(exist_ok=True)
# 读入映射表(旧名 -> 新名)
mapping = pd.read_excel("重命名映射表.xlsx")
mapping_dict = dict(zip(mapping["原文件名"], mapping["新文件名"]))
renamed, skipped = 0, 0
for file in src_dir.glob("*.pdf"):
stem = file.stem # 不含扩展名的文件名
# 从文件名提取编码(如 "DWG-A001-Rev2" 中的 A001)
match = re.search(r"([A-Z]{2}\d{4})", stem)
if stem in mapping_dict:
new_name = mapping_dict[stem] + file.suffix
elif match:
code = match.group(1)
new_name = f"{code}_工艺文件{file.suffix}"
else:
print(f" 跳过(无法识别): {file.name}")
skipped += 1
continue
# 处理重名
target = dst_dir / new_name
if target.exists():
target = dst_dir / f"{Path(new_name).stem}_副本{file.suffix}"
file.rename(target)
renamed += 1
print(f"重命名 {renamed} 个,跳过 {skipped} 个")
安全提醒:批量文件操作前,先备份,先在副本上测试,先打印出"将要做什么"让用户确认,再实际执行。
案例 5:自动生成并发送报表
import pandas as pd
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from datetime import datetime, timedelta
import schedule
import time
def generate_report():
"""生成上周的生产周报"""
end = datetime.now()
start = end - timedelta(days=7)
# 取数
df = pd.read_sql(
f"SELECT * FROM v_production_daily WHERE stat_date >= '{start:%Y-%m-%d}'",
conn
)
# 计算指标
summary = df.groupby("产线").agg({
"产量": "sum",
"不良数": "sum",
"运行时间": "sum",
"停机时间": "sum"
})
summary["良率"] = summary["产量"] / (summary["产量"] + summary["不良数"])
summary["OEE"] = ... # 按公式计算
# 生成 Excel(用模板)
from openpyxl import load_workbook
wb = load_workbook("周报模板.xlsx")
ws = wb["数据"]
for r in dataframe_to_rows(summary.reset_index(), index=False, header=False):
ws.append(r)
output = f"生产周报_{end:%Y%m%d}.xlsx"
wb.save(output)
return output, summary
def send_report(attachment, summary):
"""发送邮件"""
msg = MIMEMultipart()
msg["From"] = "ie@company.com"
msg["To"] = "manager@company.com; team@company.com"
msg["Subject"] = f"生产周报 - {datetime.now():%Y-%m-%d}"
# 正文(把关键指标写进正文,不用打开附件就知道概况)
body = f"""
<html><body>
<h3>上周生产概况</h3>
<table border="1" cellpadding="5">
<tr><th>产线</th><th>产量</th><th>良率</th><th>OEE</th></tr>
{"".join(f"<tr><td>{idx}</td><td>{row['产量']:.0f}</td>"
f"<td>{row['良率']:.2%}</td><td>{row['OEE']:.1%}</td></tr>"
for idx, row in summary.iterrows())}
</table>
<p>详见附件。</p>
</body></html>
"""
msg.attach(MIMEText(body, "html", "utf-8"))
# 附件
with open(attachment, "rb") as f:
part = MIMEApplication(f.read())
part.add_header("Content-Disposition", "attachment",
filename=attachment)
msg.attach(part)
# 发送
with smtplib.SMTP("smtp.company.com", 25) as server:
server.login("ie@company.com", "password")
server.send_message(msg)
print(f"已发送: {attachment}")
def job():
print(f"[{datetime.now()}] 开始生成周报...")
try:
attachment, summary = generate_report()
send_report(attachment, summary)
print("完成")
except Exception as e:
print(f"出错: {e}")
# 出错时也要发通知
# send_error_notification(str(e))
# 定时任务:每周一早上 8:00
schedule.every().monday.at("08:00").do(job)
# 或者直接用操作系统的定时任务(更可靠)
# Windows: 任务计划程序
# Linux: cron
print("定时任务已启动,等待触发...")
while True:
schedule.run_pending()
time.sleep(60)
实务建议:定时任务的可靠性,用操作系统级的调度(Windows 任务计划程序 / Linux cron)比 Python 的 schedule 库更稳(Python 进程挂了就没了)。Python 脚本只负责"跑一次",调度交给系统。
五、VBA 与 Python 的协作
5.1 三种协作方式
| 方式 | 做法 | 适用 |
|---|---|---|
| Python 生成 → Excel 消费 | Python 输出 CSV/Excel,Excel 用 Power Query 连接 | 最简单,推荐 |
| VBA 调用 Python | 用 Shell 命令调用 Python 脚本 |
用户点 Excel 按钮触发 Python |
| xlwings | Python 直接控制 Excel 应用 | 需要双向交互 |
5.2 推荐架构:Python 处理 + Excel 呈现
[数据源]
↓ Python 脚本(定时运行)
[中间数据.csv / 数据.xlsx] ← 放在共享盘
↓ Excel 用 Power Query 连接(自动刷新)
[报表/看板.xlsx] ← 用户在 Excel 里看、切片、透视
优点:
- Python 负责重活(取数、清洗、计算)
- Excel 负责呈现(用户熟悉、格式灵活、可交互)
- 用户不需要装 Python,只需要刷新 Excel
- 逻辑与呈现分离,易于维护
5.3 VBA 调用 Python 示例
Sub RunPython()
Dim pythonPath As String, scriptPath As String
Dim shellCmd As String
pythonPath = "C:\Users\xxx\miniconda3\envs\ie\python.exe"
scriptPath = "D:\scripts\generate_report.py"
shellCmd = """" & pythonPath & """ """ & scriptPath & """"
' 同步执行,等待完成
Dim wsh As Object
Set wsh = CreateObject("WScript.Shell")
Dim exitCode As Integer
exitCode = wsh.Run(shellCmd, 1, True) ' 1=显示窗口, True=等待
If exitCode = 0 Then
' 刷新 Excel 数据连接
ThisWorkbook.RefreshAll
MsgBox "报表已更新", vbInformation
Else
MsgBox "Python 脚本执行失败,错误码: " & exitCode, vbCritical
End If
End Sub
5.4 xlwings:Python 控制 Excel
import xlwings as xw
# 连接已打开的 Excel,或打开新实例
wb = xw.Book("报表.xlsx")
# 或 app = xw.App(visible=True); wb = app.books.open(...)
# 读写单元格
sheet = wb.sheets["数据"]
sheet.range("A1").value = "产量"
sheet.range("A2").value = [[1, 2], [3, 4]] # 写二维数组
# 读入 DataFrame
df = sheet.range("A1").expand().options(pd.DataFrame).value
# 写 DataFrame
sheet.range("A1").value = df
# 调用 Excel 公式
sheet.range("D2").formula = "=B2/C2"
# 运行 VBA 宏
macro = wb.macro("GenerateChart")
macro()
# 保存
wb.save()
xlwings 的价值:Python 的数据处理能力 + Excel 的完整对象模型。适合"数据处理复杂、格式要求也高"的场景。
六、学习路径
6.1 VBA 学习路径(约 20 小时)
| 阶段 | 内容 | 用时 |
|---|---|---|
| 1 | 宏录制 + 修改录制代码 | 3h |
| 2 | 变量、循环、判断、Range 对象 | 6h |
| 3 | 数组优化、事件、错误处理 | 4h |
| 4 | 文件系统、字典、Outlook 集成 | 4h |
| 5 | 实战:做一个自己的自动化工具 | 3h+ |
最好的学习方法:从自己每周重复做的工作开始。打开宏录制,做一遍,看代码,然后改。
6.2 Python 学习路径(约 60 小时)
| 阶段 | 内容 | 用时 |
|---|---|---|
| 基础语法 | 变量、类型、列表、字典、循环、函数、文件读写 | 15h |
| pandas | DataFrame 操作、分组、合并、清洗 | 20h |
| Excel 交互 | openpyxl、xlwings | 8h |
| 可视化 | matplotlib、seaborn | 5h |
| 实战项目 | 自动化报表、数据分析 | 12h+ |
给 IE 的建议:不要从"Python 入门教程"开始(那些教你写猜数字游戏、学生管理系统的内容对 IE 没用)。直接从 pandas 开始,用你自己的数据练。
推荐学习资源:
- 《利用 Python 进行数据分析》(Python for Data Analysis,Wes McKinney) — pandas 作者写的,最权威
- pandas 官方文档的 "10 Minutes to pandas" — 一小时快速上手
- Kaggle Learn — 免费的交互式课程
- B 站/知乎的中文教程 — 注意甄别质量
6.3 从"会写"到"能用"
写好脚本只是开始,要让它真正产生价值,还需要:
| 环节 | 做法 |
|---|---|
| 稳定化 | 加错误处理、日志记录、异常通知 |
| 参数化 | 日期、路径、阈值做成配置,不要硬编码 |
| 文档化 | 写清楚"这个脚本做什么、怎么运行、依赖什么" |
| 可交接 | 别人能跑起来(requirements.txt、说明文档) |
| 自动化 | 用任务计划程序定时跑 |
| 验证 | 输出结果的合理性检查(断言、对账) |
一个"能用的"脚本和"玩具脚本"的区别,全在这些工程细节上。
七、九个新手常见的坑
| # | 坑 | 后果 | 解法 |
|---|---|---|---|
| 1 | 路径写死(C:\Users\张三\...) |
换台电脑就跑不了 | 用相对路径或配置文件;用 Path(__file__).parent |
| 2 | 中文编码问题 | CSV 乱码、读取报错 | CSV 用 encoding="utf-8-sig";文件名避免中文 |
| 3 | Excel 文件被占用 | 无法写入 | 关闭文件;或用 with 确保释放 |
| 4 | 不备份就批量修改 | 数据丢失 | 先备份,先在副本上测试 |
| 5 | 逐格操作单元格(VBA) | 极慢 | 用数组一次性读写 |
| 6 | 不处理缺失值就计算 | 结果全为 NaN | 先 dropna 或 fillna |
| 7 | 浮点比较用 == |
判断出错 | 用 abs(a-b) < 1e-6 |
| 8 | 脚本报错没人知道 | 自动化悄悄失败 | 加 try-except + 错误通知(邮件/日志) |
| 9 | 硬编码日期(WHERE date = '2026-08-01') |
每次要改代码 | 用 datetime.now() - timedelta(days=7) 动态计算 |
第 4 条最严重。批量处理文件的脚本,如果逻辑写错并且没有备份,可能一次性毁掉几百个文件。永远先在小样本上测试,永远保留原始副本。
八、给 IE 的务实建议
8.1 先算 ROI,再动手
自动化一个任务前,先算:
年度耗时 = 单次耗时 × 年频次
开发投入 = 学习 + 编写 + 调试
回收期 = 开发投入 / 年度耗时
经验规则:
- 年度耗时 > 20 小时 → 值得自动化
- 任务规则固定、很少变 → 值得自动化
- 只用一两次 → 不要自动化,手动做更快
不要为了自动化而自动化。 一个耗时 10 分钟的月度任务,花 8 小时写脚本,需要 48 个月才回本——不值得。
8.2 从"最痛的"开始
列出你所有重复性的工作,按"耗时 × 频率"排序,从最痛的开始。通常是:
- 周期性报表(日报/周报/月报)
- 数据合并与清洗
- 格式转换与文件处理
- 对账与核对
8.3 复利效应
自动化有一个重要特点:一次投入,长期收益,且可复制。
- 你写了一个合并报表的脚本,以后每月省 3 小时
- 你把这个脚本改改给同事用,又省了别人的时间
- 你积累了处理同类问题的模式,写下一个脚本只要 1/3 的时间
三年下来,一个善于自动化的 IE,比同僚多出几百小时的分析和改善时间。 这就是差距的来源。
8.4 不要走火入魔
最后一点提醒:自动化的目的是把时间还给思考,不是把工作变成写代码。
见过一些工程师,花两周写一个高度定制化的自动化工具,结果这个工具只用了一次(因为业务需求变了)。判断标准:
- 这个工具会被反复使用吗?
- 需求会频繁变动吗?
- 有没有现成工具能用(Excel 公式、Power Query、低代码平台)?
能用 Power Query 解决的,不必写 Python。能用 Excel 公式解决的,不必写 VBA。选择最简单的、能解决问题的工具。
结语:自动化是 IE 的"杠杆"
IE 的核心工作是消除浪费。而"重复搬运数据"本身就是一种巨大的浪费——只不过它浪费的是 IE 自己的时间,所以常常被忽视。
学会自动化,本质上是给自己装了一根杠杆:
- 同样的时间,能处理 10 倍的数据
- 同样的分析,能覆盖 10 倍的范围
- 同样的结论,能快 10 倍得到
更重要的是,自动化改变了你的工作方式。当你不再需要手工搬运数据,你才有时间和精力去问那些真正重要的问题:
- 这个数字为什么是这个值?
- 这个趋势意味着什么?
- 我们该做什么?
工具解决"怎么做",而 IE 的价值在于"做什么"。把前者交给脚本,把后者留给自己。
相关阅读
- 工业工程必备软件地图:从 Excel 到 FlexSim,每个阶段该学什么:按学习阶段给出 IE 的软件全景图:Excel、统计分析、仿真建模、CAD、企业系统,并给出…
- 用 Python 做 IE 数据分析:从工时数据到线平衡计算:四个可直接运行的 IE 场景代码:标准工时自动计算、产线平衡分析、离散事件仿真、运筹优化排产…
- 工业工程师的 Excel 实战手册:从工时分析、过程能力到线平衡的 30 个技法:面向 IE 场景的 Excel 实操手册:连续测时数据差分还原、IQR 与 3σ 异常值剔除…
- 工业工程数据与指标看板:从指标定义、采集口径到可视化落地的完整手册:从指标定义卡 12 要素讲到看板落地:OEE 三种分母口径对照(负荷 75.45% / 计划…
- Minitab 工业工程实战指南:从数据到结论的完整链路:工业工程领域出镜率最高的统计软件。本文按拿到数据后的真实使用顺序组织:数据导入清洗、图形化汇…