报表自动化开发
报表自动化是什么
手动统计 = 每天手动抄数据到 Excel:你每天早上打开系统,一条一条复制数据到 Excel,手动画图表,手动调格式,再手动发邮件给领导。10 个人每人花 2 小时,累死。
自动化 = 设好规则让程序自动做:你写一个脚本,设定每天早上 6 点自动跑。程序自动查数据库、生成 Excel、画图表、发邮件。你早上 8 点到工位,报表已经在领导邮箱里了。躺赢!
中际旭创的岗位描述:"设计并实现定期自动推送的Excel报表、PDF报告或邮件报表,减少人工统计工作量"
翻译成程序员语言就是:
- 从 MES / ERP / SQL Server 自动查询生产数据
- 用 Python 自动生成 格式化的 Excel(带公式、图表、条件格式)
- 自动生成 PDF 报告(标题、表格、图表)
- 定时自动通过 邮件推送给相关人员
- 整个过程 无人值守,设置好定时任务就行
❌ 手动做报表
- 10 个人,每人每天 2 小时
- 容易出错:复制粘贴漏一行
- 格式不统一:每个人做的不一样
- 节假日没人做,数据滞后
- 领导要新维度,全部重来
✅ 自动化报表
- 1 个脚本,每天自动跑 5 分钟
- 零错误:直接从数据库取
- 格式统一:模板固定
- 365 天不停歇
- 改个参数就行,秒级生效
| 环节 | 技术工具 | 说明 |
|---|---|---|
| 数据源 | SQL Server / MySQL / API | MES/ERP 系统的数据库 |
| Excel 生成 | openpyxl / xlsxwriter | 带格式、公式、图表的 Excel |
| PDF 生成 | reportlab / weasyprint | 排版精美的 PDF 报告 |
| 邮件发送 | smtplib + email | 带附件的 HTML 邮件 |
| 定时调度 | cron / APScheduler | 每天定时执行 |
| 模板引擎 | Jinja2 | 参数化报表生成 |
Excel 报表生成
手动做 Excel 报表就像用算盘算账——数据多了就手忙脚乱。用 openpyxl 就像装了一台自动收银机,数据进去,格式化报表出来。你只需要定义"规则",程序帮你搞定一切。
# Excel 操作库
pip install openpyxl
# 数据库连接(SQL Server)
pip install pymssql
# 如果用 xlsxwriter(备选)
pip install XlsxWriter
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.formatting.rule import CellIsRule
from openpyxl.chart import BarChart, Reference
from openpyxl.utils import get_column_letter
from datetime import datetime, timedelta
def generate_oee_excel(report_date=None):
"""生成每日 OEE 日报 Excel 文件"""
# ========== 1. 准备数据(实际场景从数据库查询)==========
if report_date is None:
report_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
# 模拟各产线 OEE 数据
production_data = [
{"line": "Line-01", "product": "100G光模块", "plan_qty": 1200, "actual_qty": 1150,
"defect_qty": 23, "run_hours": 22.5, "plan_hours": 24},
{"line": "Line-02", "product": "400G光模块", "plan_qty": 800, "actual_qty": 720,
"defect_qty": 15, "run_hours": 20.0, "plan_hours": 24},
{"line": "Line-03", "product": "800G光模块", "plan_qty": 500, "actual_qty": 485,
"defect_qty": 8, "run_hours": 21.0, "plan_hours": 24},
{"line": "Line-04", "product": "200G光模块", "plan_qty": 1000, "actual_qty": 780,
"defect_qty": 45, "run_hours": 18.5, "plan_hours": 24},
{"line": "Line-05", "product": "100G光模块", "plan_qty": 1200, "actual_qty": 1180,
"defect_qty": 12, "run_hours": 23.0, "plan_hours": 24},
]
# 计算 OEE 三要素
for d in production_data:
d["availability"] = d["run_hours"] / d["plan_hours"] * 100 # 可用率
d["performance"] = d["actual_qty"] / d["plan_qty"] * 100 # 表现指数
d["quality"] = (d["actual_qty"] - d["defect_qty"]) / d["actual_qty"] * 100
d["oee"] = d["availability"] * d["performance"] * d["quality"] / 10000
# ========== 2. 创建 Excel 工作簿 ==========
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "OEE日报"
# ========== 3. 定义样式 ==========
header_font = Font(name="微软雅黑", size=12, bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="1F4E79", end_color="1F4E79", fill_type="solid")
title_font = Font(name="微软雅黑", size=18, bold=True, color="1F4E79")
data_font = Font(name="微软雅黑", size=11)
center_align = Alignment(horizontal="center", vertical="center")
thin_border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin")
)
# ========== 4. 写入标题行 ==========
ws.merge_cells("A1:K1")
title_cell = ws["A1"]
title_cell.value = f"中际旭创 生产OEE日报 - {report_date}"
title_cell.font = title_font
title_cell.alignment = center_align
# ========== 5. 写入表头 ==========
headers = ["产线", "产品", "计划产量", "实际产量", "不良数",
"运行时间(h)", "计划时间(h)", "可用率%",
"表现指数%", "质量指数%", "OEE%"]
for col, h in enumerate(headers, 1):
cell = ws.cell(row=3, column=col, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_align
cell.border = thin_border
# ========== 6. 写入数据行 ==========
for i, d in enumerate(production_data):
row = i + 4
values = [d["line"], d["product"], d["plan_qty"], d["actual_qty"],
d["defect_qty"], d["run_hours"], d["plan_hours"],
round(d["availability"], 1), round(d["performance"], 1),
round(d["quality"], 1), round(d["oee"], 1)]
for col, val in enumerate(values, 1):
cell = ws.cell(row=row, column=col, value=val)
cell.font = data_font
cell.alignment = center_align
cell.border = thin_border
# ========== 7. 写入汇总行(使用 Excel 公式)==========
summary_row = 4 + len(production_data)
ws.cell(row=summary_row, column=1, value="合计/平均").font = Font(bold=True, size=11)
ws.cell(row=summary_row, column=1).alignment = center_align
# SUM 公式
ws.cell(row=summary_row, column=3).value = "=SUM(C4:C8)" # 计划产量合计
ws.cell(row=summary_row, column=4).value = "=SUM(D4:D8)" # 实际产量合计
ws.cell(row=summary_row, column=5).value = "=SUM(E4:E8)" # 不良数合计
# AVERAGE 公式
ws.cell(row=summary_row, column=8).value = "=AVERAGE(H4:H8)" # 平均可用率
ws.cell(row=summary_row, column=9).value = "=AVERAGE(I4:I8)" # 平均表现
ws.cell(row=summary_row, column=10).value = "=AVERAGE(J4:J8)" # 平均质量
ws.cell(row=summary_row, column=11).value = "=AVERAGE(K4:K8)" # 平均 OEE
for col in range(1, 12):
ws.cell(row=summary_row, column=col).font = Font(bold=True, size=11)
ws.cell(row=summary_row, column=col).alignment = center_align
ws.cell(row=summary_row, column=col).border = thin_border
# ========== 8. 条件格式:OEE < 80% 标红 ==========
red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
red_font = Font(color="9C0006", bold=True)
ws.conditional_formatting.add(
"K4:K8",
CellIsRule(operator="lessThan", formula=["80"], fill=red_fill, font=red_font)
)
# ========== 9. 调整列宽 ==========
col_widths = [10, 14, 12, 12, 10, 14, 14, 10, 12, 12, 10]
for i, w in enumerate(col_widths, 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ========== 10. 创建柱状图 ==========
chart = BarChart()
chart.type = "col"
chart.title = f"各产线 OEE 对比 - {report_date}"
chart.y_axis.title = "百分比 (%)"
chart.x_axis.title = "产线"
chart.style = 10
# 图表数据:OEE 列(K列 = 第11列)
data_ref = Reference(ws, min_col=11, min_row=3, max_row=8)
cats_ref = Reference(ws, min_col=1, min_row=4, max_row=8)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(cats_ref)
chart.shape = 4
chart.width = 20
chart.height = 12
ws.add_chart(chart, "A11")
# ========== 11. 保存文件 ==========
filename = f"OEE日报_{report_date}.xlsx"
wb.save(filename)
print(f"[OK] Excel 已生成: {filename}")
return filename
# 运行
if __name__ == "__main__":
generate_oee_excel()
| 产线 | 产品 | 计划产量 | 实际产量 | 不良数 | 运行时间 | 计划时间 | 可用率% | 表现% | 质量% | OEE% |
|---|---|---|---|---|---|---|---|---|---|---|
| Line-01 | 100G光模块 | 1200 | 1150 | 23 | 22.5 | 24 | 93.8 | 95.8 | 98.0 | 88.0 |
| Line-02 | 400G光模块 | 800 | 720 | 15 | 20.0 | 24 | 83.3 | 90.0 | 97.9 | 80.7 |
| Line-03 | 800G光模块 | 500 | 485 | 8 | 21.0 | 24 | 87.5 | 97.0 | 98.4 | 83.6 |
| Line-04 | 200G光模块 | 1000 | 780 | 45 | 18.5 | 24 | 77.1 | 78.0 | 94.2 | 56.7 |
| Line-05 | 100G光模块 | 1200 | 1180 | 12 | 23.0 | 24 | 95.8 | 98.3 | 99.0 | 93.2 |
| 合计/平均 | =SUM(C4:C8) | =SUM(D4:D8) | =SUM(E4:E8) | =AVG(H4:H8) | =AVG(I4:I8) | =AVG(J4:J8) | =AVG(K4:K8) |
import pymssql
from datetime import datetime, timedelta
def query_production_data(report_date=None):
"""从 SQL Server 查询生产数据"""
if report_date is None:
report_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
conn = pymssql.connect(
server="192.168.1.100", # MES 数据库地址
user="report_user",
password="your_password",
database="MES_Production"
)
cursor = conn.cursor()
sql = """
SELECT
line_name,
product_model,
planned_qty,
actual_qty,
defect_qty,
run_hours,
planned_hours
FROM production_daily
WHERE report_date = %s
ORDER BY line_name
"""
cursor.execute(sql, (report_date,))
rows = cursor.fetchall()
# 转换为字典列表
columns = ["line", "product", "plan_qty", "actual_qty",
"defect_qty", "run_hours", "plan_hours"]
data = [dict(zip(columns, row)) for row in rows]
conn.close()
return data
PDF 报告生成
Excel 是给操作员看的工具表,可以筛选、修改。PDF 是给领导看的正式报告,格式固定、不可篡改、看起来高端。就像 Word 文档和盖了章的红头文件的区别。
pip install reportlab
# 中文字体支持(reportlab 默认不支持中文)
pip install reportlab
# 需要下载中文字体文件:SimHei.ttf 或 微软雅黑.ttf
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from datetime import datetime, timedelta
import os
# ========== 注册中文字体 ==========
FONT_PATH = "/usr/share/fonts/SimHei.ttf" # Linux 服务器路径
# FONT_PATH = "C:/Windows/Fonts/simhei.ttf" # Windows 路径
if os.path.exists(FONT_PATH):
pdfmetrics.registerFont(TTFont("SimHei", FONT_PATH))
CN_FONT = "SimHei"
else:
CN_FONT = "Helvetica" # fallback
def generate_pdf_report(report_date=None):
"""生成 PDF 生产日报"""
if report_date is None:
report_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
filename = f"生产日报_{report_date}.pdf"
doc = SimpleDocTemplate(
filename,
pagesize=A4,
rightMargin=2*cm, leftMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm
)
styles = getSampleStyleSheet()
# 自定义中文样式
styles.add(ParagraphStyle(
name="CNTitle",
fontName=CN_FONT,
fontSize=22,
alignment=1, # 居中
spaceAfter=20,
textColor=colors.HexColor("#7EC8F0")
))
styles.add(ParagraphStyle(
name="CNSubtitle",
fontName=CN_FONT,
fontSize=14,
alignment=1,
spaceAfter=10,
textColor=colors.grey
))
styles.add(ParagraphStyle(
name="CNBody",
fontName=CN_FONT,
fontSize=11,
leading=18,
spaceAfter=6
))
styles.add(ParagraphStyle(
name="CNHeading",
fontName=CN_FONT,
fontSize=14,
spaceAfter=8,
textColor=colors.HexColor("#7EC8F0")
))
elements = []
# ========== 1. 标题页 ==========
elements.append(Spacer(1, 80*mm))
elements.append(Paragraph("中际旭创股份有限公司", styles["CNTitle"]))
elements.append(Spacer(1, 10*mm))
elements.append(Paragraph(f"生产日报 - {report_date}", styles["CNTitle"]))
elements.append(Spacer(1, 15*mm))
elements.append(Paragraph("报告类型:每日生产OEE汇总", styles["CNSubtitle"]))
elements.append(Paragraph(f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles["CNSubtitle"]))
elements.append(Paragraph("数据来源:MES 生产管理系统", styles["CNSubtitle"]))
elements.append(PageBreak())
# ========== 2. 概要信息 ==========
elements.append(Paragraph("一、生产概要", styles["CNHeading"]))
summary_data = [
["指标", "数值"],
["报告日期", report_date],
["产线数量", "5 条"],
["总计划产量", "4,700 件"],
["总实际产量", "4,315 件"],
["达成率", "91.8%"],
["平均 OEE", "80.4%"],
["异常产线", "Line-04 (OEE: 56.7%, 需关注)"],
]
summary_table = Table(summary_data, colWidths=[80*mm, 90*mm])
summary_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#7EC8F0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTSIZE", (0, 0), (-1), -1, 11),
("ALIGN", (0, 0), (-1), -1, "CENTER"),
("GRID", (0, 0), (-1), -1, 0.5, colors.grey),
("ROWBACKGROUNDS", (0, 1), (-1), -1),
("FONTNAME", (0, 0), (-1), -1, CN_FONT),
]))
elements.append(summary_table)
elements.append(Spacer(1, 15*mm))
# ========== 3. 详细数据表 ==========
elements.append(Paragraph("二、各产线 OEE 详情", styles["CNHeading"]))
detail_data = [
["产线", "产品", "计划", "实际", "不良",
"可用率", "表现", "质量", "OEE"],
["Line-01", "100G", 1200, 1150, 23, 93.8, 95.8, 98.0, 88.0],
["Line-02", "400G", 800, 720, 15, 83.3, 90.0, 97.9, 80.7],
["Line-03", "800G", 500, 485, 8, 87.5, 97.0, 98.4, 83.6],
["Line-04", "200G", 1000, 780, 45, 77.1, 78.0, 94.2, 56.7],
["Line-05", "100G", 1200, 1180, 12, 95.8, 98.3, 99.0, 93.2],
]
detail_table = Table(detail_data, colWidths=[45]*9)
detail_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#7EC8F0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
# Line-04 OEE 标红
("TEXTCOLOR", (8, 4), (8, 4), colors.red),
("FONTSIZE", (0, 0), (-1), -1, 10),
("ALIGN", (0, 0), (-1), -1, "CENTER"),
("GRID", (0, 0), (-1), -1, 0.5, colors.grey),
("ROWBACKGROUNDS", (0, 1), (-1), -1),
]))
elements.append(detail_table)
elements.append(Spacer(1, 15*mm))
# ========== 4. 嵌入图表(如果有 ECharts 导出的图片)==========
chart_image = f"oee_chart_{report_date}.png"
if os.path.exists(chart_image):
elements.append(Paragraph("三、OEE 对比图", styles["CNHeading"]))
img = Image(chart_image, width=150*mm, height=90*mm)
elements.append(img)
# ========== 5. 构建 PDF ==========
doc.build(elements)
print(f"[OK] PDF 已生成: {filename}")
return filename
if __name__ == "__main__":
generate_pdf_report()
pyecharts 生成图表后调用 snapshot_phantomjs 或 snapshot_selenium 导出为 PNG 图片,再用 reportlab 嵌入。也可以用 matplotlib 直接生成图片。
邮件自动发送
手动发邮件需要打开邮箱客户端、填写收件人、添加附件、点击发送——每天重复操作浪费时间。自动化邮件通过 SMTP 协议调用邮件服务器 API,代码生成报表后自动发送给收件人列表,全程无需人工干预。
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 os
# ========== SMTP 邮件配置 ==========
SMTP_CONFIG = {
"host": "smtp.company.com", # 企业邮箱 SMTP 地址
"port": 465, # SSL 端口(TLS 用 587)
"user": "report@company.com",
"password": "your_app_password", # 授权码,不是登录密码
"use_ssl": True,
}
# 收件人列表
RECIPIENTS = {
"to": [
"manager@company.com", # 生产经理
"director@company.com", # 生产总监
],
"cc": [
"engineer1@company.com", # 工程师
"engineer2@company.com", # 工程师
],
}
def create_html_body(report_date, summary_data):
"""生成 HTML 邮件正文"""
return f"""
<html>
<body style="font-family: Microsoft YaHei, sans-serif; color:#CCColor: #CCC;">
<h2 style="color: #7EC8F0;">
中际旭创 生产OEE日报 - {report_date}
</h2>
<table style="border-collapse: collapse; width: 100%; margin: 20px 0;">
<tr style="background: #7EC8F0; color: white;">
<th style="padding: 10px; border: 1px solid #ddd;">指标</th>
<th style="padding: 10px; border: 1px solid #ddd;">数值</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">总计划产量</td>
<td style="padding: 8px; border: 1px solid #ddd;">
{summary_data['total_plan']:,}
</td>
</tr>
<tr style="background: #f5f5f5;">
<td style="padding: 8px; border: 1px solid #ddd;">总实际产量</td>
<td style="padding: 8px; border: 1px solid #ddd;">
{summary_data['total_actual']:,}
</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">平均 OEE</td>
<td style="padding: 8px; border: 1px solid #ddd; font-weight: bold;
color: {'red' if summary_data['avg_oee'] < 80 else 'green'};">
{summary_data['avg_oee']:.1f}%
</td>
</tr>
<tr style="background: #fff3cd;">
<td style="padding: 8px; border: 1px solid #ddd;">异常产线</td>
<td style="padding: 8px; border: 1px solid #ddd; color: red;">
{summary_data.get('alert_lines', '无')}
</td>
</tr>
</table>
<p style="color:#CCColor: #CCC; font-size: 12px;">
本邮件由报表自动推送系统发送,请勿直接回复。
详细数据请查看附件中的 Excel 报表。
</p>
</body>
</html>
"""
def send_report_email(report_date, excel_file, pdf_file, summary_data):
"""发送报表邮件"""
try:
# 1. 构建邮件
msg = MIMEMultipart()
msg["From"] = SMTP_CONFIG["user"]
msg["To"] = ",".join(RECIPIENTS["to"])
msg["Cc"] = ",".join(RECIPIENTS["cc"])
msg["Subject"] = f"[自动报表] 生产OEE日报 - {report_date}"
# 2. HTML 正文
html_body = create_html_body(report_date, summary_data)
msg.attach(MIMEText(html_body, "html", "utf-8"))
# 3. 添加 Excel 附件
if excel_file and os.path.exists(excel_file):
with open(excel_file, "rb") as f:
excel_attach = MIMEApplication(f.read())
excel_attach.add_header(
"Content-Disposition", "attachment",
filename=os.path.basename(excel_file)
)
msg.attach(excel_attach)
# 4. 添加 PDF 附件
if pdf_file and os.path.exists(pdf_file):
with open(pdf_file, "rb") as f:
pdf_attach = MIMEApplication(f.read())
pdf_attach.add_header(
"Content-Disposition", "attachment",
filename=os.path.basename(pdf_file)
)
msg.attach(pdf_attach)
# 5. 发送邮件
all_recipients = RECIPIENTS["to"] + RECIPIENTS["cc"]
if SMTP_CONFIG["use_ssl"]:
server = smtplib.SMTP_SSL(SMTP_CONFIG["host"], SMTP_CONFIG["port"])
else:
server = smtplib.SMTP(SMTP_CONFIG["host"], SMTP_CONFIG["port"])
server.starttls()
server.login(SMTP_CONFIG["user"], SMTP_CONFIG["password"])
server.sendmail(SMTP_CONFIG["user"], all_recipients, msg.as_string())
server.quit()
print(f"[OK] 邮件已发送给 {len(all_recipients)} 人")
return True
except Exception as e:
print(f"[ERROR] 邮件发送失败: {e}")
return False
# 常见邮箱 SMTP 配置参考
# QQ邮箱: smtp.qq.com, 465(SSL) / 587(TLS)
# 163邮箱: smtp.163.com, 465(SSL)
# 企业微信: smtp.exmail.qq.com, 465(SSL)
# Outlook: smtp.office365.com, 587(TLS)
# Gmail: smtp.gmail.com, 465(SSL) / 587(TLS)
os.environ['SMTP_PASSWORD']
定时调度
你设了早上 6 点的闹钟,到了 6 点它就会响。定时调度就是给程序设闹钟——每天早上 6 点,你的脚本就会自动醒来干活。你还可以设不同的闹钟:日报每天跑、周报每周跑、月报每月跑。
# 编辑定时任务
crontab -e
# 每天早上 6:00 执行日报脚本
0 6 * * * /usr/bin/python3 /opt/reports/daily_report.py >> /var/log/report.log 2>&1
# 每周一早上 7:00 执行周报脚本
0 7 * * 1 /usr/bin/python3 /opt/reports/weekly_report.py >> /var/log/report.log 2>&1
# 每月1号早上 8:00 执行月报脚本
0 8 1 * * /usr/bin/python3 /opt/reports/monthly_report.py >> /var/log/report.log 2>&1
# 工作日(周一到周五)早上 6:30 执行
30 6 * * 1-5 /usr/bin/python3 /opt/reports/daily_report.py
| 字段 | 含义 | 取值范围 | 示例 |
|---|---|---|---|
分 | 分钟 | 0-59 | 0 = 整点, 30 = 半点 |
时 | 小时 | 0-23 | 6 = 早上6点, 18 = 下午6点 |
日 | 日期 | 1-31 | 1 = 每月1号, 15 = 每月15号 |
月 | 月份 | 1-12 | 1 = 一月, * = 每月 |
周 | 星期 | 0-6 (0=周日) | 1 = 周一, 1-5 = 工作日 |
| 表达式 | 含义 | 场景 |
|---|---|---|
0 6 * * * | 每天早上6:00 | 日报 |
0 7 * * 1 | 每周一早上7:00 | 周报 |
0 8 1 * * | 每月1号早上8:00 | 月报 |
30 6 * * 1-5 | 工作日早上6:30 | 工作日报 |
0 */2 * * * | 每2小时 | 实时监控 |
0 9,15 * * * | 每天9:00和15:00 | 早晚班报表 |
*/10 * * * * | 每10分钟 | 高频监控 |
0 0 1 1,7 * | 1月和7月1号0:00 | 半年报 |
# 方法1:通过命令行创建计划任务
schtasks /create /tn "DailyOEEReport" /tr "python C:\reports\daily_report.py" /sc daily /st 06:00
# 方法2:通过 GUI 操作
# 1. Win+R → taskschd.msc → 打开任务计划程序
# 2. 创建基本任务 → 名称: "每日OEE报表"
# 3. 触发器: 每天 → 时间: 06:00
# 4. 操作: 启动程序 → python.exe
# 5. 参数: C:\reports\daily_report.py
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
scheduler = BlockingScheduler()
# 每天 6:00 执行日报
@scheduler.scheduled_job("cron", hour=6, minute=0)
def daily_report_job():
logger.info("[START] 开始生成日报...")
try:
# 导入并执行日报脚本
from daily_report import run_daily_report
run_daily_report()
logger.info("[DONE] 日报生成并发送成功")
except Exception as e:
logger.error(f"[ERROR] 日报生成失败: {e}")
# 每周一 7:00 执行周报
@scheduler.scheduled_job("cron", day_of_week="mon", hour=7, minute=0)
def weekly_report_job():
logger.info("[START] 开始生成周报...")
from weekly_report import run_weekly_report
run_weekly_report()
# 每10分钟检查一次数据异常
@scheduler.scheduled_job("interval", minutes=10)
def monitor_job():
from monitor import check_anomalies
check_anomalies()
if __name__ == "__main__":
logger.info("报表调度器已启动,等待执行...")
scheduler.start()
const cron = require('node-cron');
const { exec } = require('child_process');
// 每天 6:00 执行 Python 日报脚本
cron.schedule('0 6 * * *', () => {
console.log('[START] 开始生成日报...');
exec('python3 /opt/reports/daily_report.py', (error, stdout, stderr) => {
if (error) {
console.error(`[ERROR] ${error.message}`);
return;
}
console.log(`[DONE] ${stdout}`);
});
});
console.log('报表调度器已启动...');
方案对比
- cron:最简单,Linux 原生,生产环境首选
- Windows 任务计划:Windows 环境唯一选择
- APScheduler:Python 内置,灵活,可动态添加任务
- node-cron:如果主项目是 Node.js
推荐方案
- 生产环境:cron + supervisor 守护进程
- 开发环境:APScheduler(调试方便)
- Windows 开发:Windows Task Scheduler
- 容器化:Docker + cron 或 K8s CronJob
完整实战:日报自动推送
这就是你面试时要讲的核心案例:一个完整的自动报表系统,从数据采集到邮件推送,全流程自动化。就像一条全自动生产线——原料(数据)进去,成品(报表邮件)出来,中间不需要任何人操作。
"""
日报自动推送系统 - 完整脚本
功能:每天早上6点自动生成 OEE 日报并推送给管理层
"""
import os
import sys
import logging
from datetime import datetime, timedelta
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("/var/log/daily_report.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 输出目录
OUTPUT_DIR = "/opt/reports/output"
os.makedirs(OUTPUT_DIR, exist_ok=True)
# =====================================================
# 第一步:从数据库查询生产数据
# =====================================================
def fetch_production_data(report_date):
"""从 MES 数据库查询昨日生产数据"""
import pymssql
logger.info(f"正在查询 {report_date} 的生产数据...")
try:
conn = pymssql.connect(
server=os.environ.get("DB_HOST", "192.168.1.100"),
user=os.environ.get("DB_USER", "report_user"),
password=os.environ.get("DB_PASSWORD"),
database="MES_Production"
)
cursor = conn.cursor()
sql = """
SELECT line_name, product_model, planned_qty, actual_qty,
defect_qty, run_hours, planned_hours
FROM production_daily
WHERE report_date = %s
ORDER BY line_name
"""
cursor.execute(sql, (report_date,))
rows = cursor.fetchall()
columns = ["line", "product", "plan_qty", "actual_qty",
"defect_qty", "run_hours", "plan_hours"]
data = [dict(zip(columns, row)) for row in rows]
conn.close()
logger.info(f"查询完成,共 {len(data)} 条记录")
return data
except Exception as e:
logger.error(f"数据库查询失败: {e}")
raise
# =====================================================
# 第二步:生成 Excel 报表
# =====================================================
def create_excel_report(data, report_date):
"""生成带格式和图表的 Excel"""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.formatting.rule import CellIsRule
from openpyxl.chart import BarChart, Reference
logger.info("正在生成 Excel 报表...")
# 计算 OEE
for d in data:
d["availability"] = d["run_hours"] / d["plan_hours"] * 100
d["performance"] = d["actual_qty"] / d["plan_qty"] * 100
d["quality"] = (d["actual_qty"] - d["defect_qty"]) / d["actual_qty"] * 100
d["oee"] = d["availability"] * d["performance"] * d["quality"] / 10000
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "OEE日报"
# 样式定义(同 Section 2,此处省略重复代码)
header_font = Font(name="微软雅黑", size=12, bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="1F4E79", fill_type="solid")
center = Alignment(horizontal="center", vertical="center")
border = Border(
left=Side("thin"), right=Side("thin"),
top=Side("thin"), bottom=Side("thin")
)
# 标题
ws.merge_cells("A1:K1")
ws["A1"].value = f"中际旭创 生产OEE日报 - {report_date}"
ws["A1"].font = Font(name="微软雅黑", size=18, bold=True, color="1F4E79")
ws["A1"].alignment = center
# 表头
headers = ["产线", "产品", "计划产量", "实际产量", "不良数",
"运行时间", "计划时间", "可用率%", "表现%", "质量%", "OEE%"]
for col, h in enumerate(headers, 1):
c = ws.cell(row=3, column=col, value=h)
c.font, c.fill, c.alignment, c.border = header_font, header_fill, center, border
# 数据行
for i, d in enumerate(data):
row = i + 4
vals = [d["line"], d["product"], d["plan_qty"], d["actual_qty"],
d["defect_qty"], d["run_hours"], d["plan_hours"],
round(d["availability"],1), round(d["performance"],1),
round(d["quality"],1), round(d["oee"],1)]
for col, val in enumerate(vals, 1):
c = ws.cell(row=row, column=col, value=val)
c.font = Font(name="微软雅黑", size=11)
c.alignment, c.border = center, border
# 条件格式:OEE < 80% 标红
end_row = 3 + len(data)
red_fill = PatternFill(start_color="FFC7CE", fill_type="solid")
red_font = Font(color="9C0006", bold=True)
ws.conditional_formatting.add(
f"K4:K{end_row}",
CellIsRule(operator="lessThan", formula=["80"], fill=red_fill, font=red_font)
)
# 柱状图
chart = BarChart()
chart.title = f"各产线 OEE 对比 - {report_date}"
chart.y_axis.title = "OEE%"
data_ref = Reference(ws, min_col=11, min_row=3, max_row=end_row)
cats_ref = Reference(ws, min_col=1, min_row=4, max_row=end_row)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(cats_ref)
chart.width, chart.height = 20, 12
ws.add_chart(chart, f"A{end_row + 2}")
# 保存
filepath = os.path.join(OUTPUT_DIR, f"OEE日报_{report_date}.xlsx")
wb.save(filepath)
logger.info(f"Excel 已生成: {filepath}")
return filepath
# =====================================================
# 第三步:生成 PDF 报告
# =====================================================
def create_pdf_report(data, report_date):
"""生成 PDF 报告"""
# (代码同 Section 3,此处简化)
from report_generator import generate_pdf_report # 导入 Section 3 的函数
filepath = generate_pdf_report(report_date)
new_path = os.path.join(OUTPUT_DIR, os.path.basename(filepath))
os.rename(filepath, new_path)
logger.info(f"PDF 已生成: {new_path}")
return new_path
# =====================================================
# 第四步:发送邮件
# =====================================================
def send_email(report_date, excel_file, pdf_file, data):
"""发送报表邮件"""
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
logger.info("正在发送邮件...")
# 汇总数据
total_plan = sum(d["plan_qty"] for d in data)
total_actual = sum(d["actual_qty"] for d in data)
avg_oee = sum(d["oee"] for d in data) / len(data)
alert_lines = [d["line"] for d in data if d["oee"] < 80]
summary = {
"total_plan": total_plan,
"total_actual": total_actual,
"avg_oee": avg_oee,
"alert_lines": ", ".join(alert_lines) if alert_lines else "无",
}
# 构建邮件(复用 Section 4 的代码)
msg = MIMEMultipart()
msg["From"] = os.environ["SMTP_USER"]
msg["To"] = os.environ.get("REPORT_TO", "manager@company.com")
msg["Subject"] = f"[自动报表] 生产OEE日报 - {report_date}"
# HTML 正文
html = f"""
<h2>中际旭创 生产OEE日报 - {report_date}</h2>
<table border="1" cellpadding="8" style="border-collapse:collapse;">
<tr><td>总计划产量</td><td>{total_plan:,}</td></tr>
<tr><td>总实际产量</td><td>{total_actual:,}</td></tr>
<tr><td>平均 OEE</td><td style="color:{'red' if avg_oee<80 else 'green'};font-weight:bold">
{avg_oee:.1f}%</td></tr>
<tr><td>异常产线</td><td style="color:red">{summary['alert_lines']}</td></tr>
</table>
<p style="color:#CCC;font-size:12px;">本邮件由报表系统自动发送</p>
"""
msg.attach(MIMEText(html, "html", "utf-8"))
# 添加附件
for filepath in [excel_file, pdf_file]:
if filepath and os.path.exists(filepath):
with open(filepath, "rb") as f:
attach = MIMEApplication(f.read())
attach.add_header("Content-Disposition", "attachment",
filename=os.path.basename(filepath))
msg.attach(attach)
# 发送
with smtplib.SMTP_SSL(os.environ["SMTP_HOST"], 465) as server:
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
server.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
logger.info("邮件发送成功!")
# =====================================================
# 主流程:串联所有步骤
# =====================================================
def run_daily_report():
"""日报主流程"""
report_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
logger.info(f"========== 开始生成 {report_date} 日报 ==========")
try:
# Step 1: 查询数据
data = fetch_production_data(report_date)
if not data:
logger.warning("无数据,跳过今日报表")
return
# Step 2: 生成 Excel
excel_file = create_excel_report(data, report_date)
# Step 3: 生成 PDF
pdf_file = create_pdf_report(data, report_date)
# Step 4: 发送邮件
send_email(report_date, excel_file, pdf_file, data)
logger.info(f"========== {report_date} 日报完成 ==========")
except Exception as e:
logger.error(f"日报生成失败: {e}", exc_info=True)
# 发送失败通知
try:
send_error_notification(report_date, str(e))
except:
pass
def send_error_notification(report_date, error_msg):
"""发送失败通知(简化版)"""
import smtplib
from email.mime.text import MIMEText
msg = MIMEText(f"日报生成失败\n日期: {report_date}\n错误: {error_msg}")
msg["Subject"] = f"[ERROR] 日报生成失败 - {report_date}"
with smtplib.SMTP_SSL(os.environ["SMTP_HOST"], 465) as s:
s.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
s.sendmail(os.environ["SMTP_USER"], "admin@company.com", msg.as_string())
# 入口
if __name__ == "__main__":
run_daily_report()
# 中际旭创 日报自动推送
# 每天早上 6:00 执行
0 6 * * * report_user /usr/bin/python3 /opt/reports/daily_report.py >> /var/log/daily_report.log 2>&1
# 环境变量在 /opt/reports/.env 中配置
# DB_HOST=192.168.1.100
# DB_USER=report_user
# DB_PASSWORD=xxx
# SMTP_HOST=smtp.company.com
# SMTP_USER=report@company.com
# SMTP_PASSWORD=xxx
# REPORT_TO=manager@company.com
.env 文件中配置,或使用 Kubernetes Secret / Vault 等工具。
进阶:动态报表
静态报表就像打印好的空白表格——每个人拿到的都一样。动态报表就像 Word 的邮件合并功能——同一个模板,填入不同人的数据就变成不同的信。你可以按日期范围、产线、班次生成不同的报表,代码只写一次。
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime, timedelta
from enum import Enum
class ReportType(Enum):
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
class ReportFormat(Enum):
EXCEL = "excel"
PDF = "pdf"
BOTH = "both"
@dataclass
class ReportConfig:
"""报表参数配置"""
report_type: ReportType = ReportType.DAILY
report_format: ReportFormat = ReportFormat.BOTH
start_date: Optional[str] = None # 自定义开始日期
end_date: Optional[str] = None # 自定义结束日期
lines: Optional[List[str]] = None # 指定产线,None=全部
shift: Optional[str] = None # 班次: day/night/None=全部
recipients: List[str] = None # 邮件收件人
send_email: bool = True
class ReportEngine:
"""动态报表引擎"""
def __init__(self, config: ReportConfig):
self.config = config
self._set_date_defaults()
def _set_date_defaults(self):
"""根据报表类型自动设置日期范围"""
if self.config.start_date is None:
today = datetime.now()
if self.config.report_type == ReportType.DAILY:
self.config.start_date = (today - timedelta(days=1)).strftime("%Y-%m-%d")
self.config.end_date = self.config.start_date
elif self.config.report_type == ReportType.WEEKLY:
self.config.start_date = (today - timedelta(days=7)).strftime("%Y-%m-%d")
self.config.end_date = (today - timedelta(days=1)).strftime("%Y-%m-%d")
elif self.config.report_type == ReportType.MONTHLY:
self.config.start_date = (today.replace(day=1) - timedelta(days=1)).replace(day=1).strftime("%Y-%m-%d")
self.config.end_date = (today.replace(day=1) - timedelta(days=1)).strftime("%Y-%m-%d")
def fetch_data(self):
"""根据参数查询数据"""
import pymssql
conn = pymssql.connect(server="192.168.1.100", ...)
# 动态构建 SQL
conditions = ["report_date BETWEEN %s AND %s"]
params = [self.config.start_date, self.config.end_date]
if self.config.lines:
placeholders = ",".join(["%s"] * len(self.config.lines))
conditions.append(f"line_name IN ({placeholders})")
params.extend(self.config.lines)
if self.config.shift:
conditions.append("shift = %s")
params.append(self.config.shift)
where = " AND ".join(conditions)
sql = f"SELECT * FROM production_daily WHERE {where} ORDER BY line_name"
# ... 执行查询
def generate(self):
"""生成报表"""
data = self.fetch_data()
files = []
if self.config.report_format in (ReportFormat.EXCEL, ReportFormat.BOTH):
files.append(self._gen_excel(data))
if self.config.report_format in (ReportFormat.PDF, ReportFormat.BOTH):
files.append(self._gen_pdf(data))
if self.config.send_email:
self._send_email(files, data)
return files
# ========== 使用示例 ==========
# 1. 标准日报:全部产线,昨天数据
config1 = ReportConfig(report_type=ReportType.DAILY)
engine1 = ReportEngine(config1)
engine1.generate()
# 2. 自定义:只看 Line-01 和 Line-03,最近7天
config2 = ReportConfig(
start_date="2026-06-03",
end_date="2026-06-09",
lines=["Line-01", "Line-03"],
report_format=ReportFormat.EXCEL,
)
engine2 = ReportEngine(config2)
engine2.generate()
# 3. 夜班报表
config3 = ReportConfig(
report_type=ReportType.DAILY,
shift="night",
recipients=["night_manager@company.com"],
)
engine3 = ReportEngine(config3)
engine3.generate()
<!-- Jinja2 邮件模板 -->
<html>
<body style="font-family: 'Microsoft YaHei', sans-serif;">
<h2 style="color: #7EC8F0;">
{{ company_name }} 生产{{ report_type_name }} - {{ report_date }}
</h2>
<table border="1" cellpadding="8" style="border-collapse: collapse; width: 100%;">
<tr style="background: #7EC8F0; color: white;">
<th>产线</th>
<th>产品</th>
<th>OEE</th>
<th>状态</th>
</tr>
{% for line in lines %}
<tr>
<td>{{ line.name }}</td>
<td>{{ line.product }}</td>
<td style="color: {{ 'red' if line.oee < 80 else 'green' }}">
{{ "%.1f"|format(line.oee) }}%
</td>
<td>
{% if line.oee >= 85 %} 正常
{% elif line.oee >= 80 %} 关注
{% else %} 异常 {% endif %}
</td>
</tr>
{% endfor %}
</table>
<p style="color:#CCColor: #999; font-size: 12px;">
本邮件由报表自动推送系统发送 | 生成时间: {{ generated_at }}
</p>
</body>
</html>
import os
from datetime import datetime
def organize_report_files(base_dir="/opt/reports/output"):
"""按年/月组织报表文件"""
now = datetime.now()
year_month = now.strftime("%Y-%m")
target_dir = os.path.join(base_dir, year_month)
os.makedirs(target_dir, exist_ok=True)
return target_dir
def cleanup_old_reports(base_dir, keep_months=6):
"""清理超过 keep_months 个月的历史报表"""
import shutil
cutoff = datetime.now()
for dirname in os.listdir(base_dir):
dirpath = os.path.join(base_dir, dirname)
if not os.path.isdir(dirpath):
continue
try:
dir_date = datetime.strptime(dirname, "%Y-%m")
months_diff = (cutoff.year - dir_date.year) * 12 + cutoff.month - dir_date.month
if months_diff > keep_months:
shutil.rmtree(dirpath)
print(f"已清理: {dirname}")
except ValueError:
continue
面试考点
分层架构设计:
1) 数据层:从 MES/ERP 数据库查询原始数据,支持多数据源(SQL Server、MySQL、API)。建议使用参数化查询,按日期范围/产线/班次灵活筛选。
2) 生成层:模板化生成 Excel(openpyxl)和 PDF(reportlab)。关键:将报表模板和业务逻辑分离,用配置文件定义格式,用 Jinja2 做模板引擎。
3) 分发层:通过 smtplib 发邮件,也可扩展为企业微信/钉钉 webhook 推送。
4) 调度层:cron 或 APScheduler 定时触发,支持日志记录和失败重试。
5) 监控层:记录每次执行日志,失败时自动告警通知运维人员。
方案1:分页生成 — 数据量太大时分多个 Sheet 或多个文件,每个 Sheet 不超过 5 万行。
方案2:用 openpyxl 的 write_only 模式 — openpyxl.Workbook(write_only=True),流式写入,内存占用极低。
方案3:换用 xlsxwriter — 专门为大数据量设计,写入速度比 openpyxl 快 3-5 倍。
方案4:汇总后再生成 — 10万行原始数据在 SQL 层做聚合(GROUP BY),只把汇总结果写入 Excel。
方案5:换格式 — 超过 100 万行用 CSV 或 Parquet 替代 Excel,Excel 最多支持 104 万行。
1) 重试机制:发送失败后自动重试 3 次,每次间隔递增(1min → 5min → 15min)。
2) 备用通道:邮件失败后通过企业微信/钉钉 webhook 发送通知。
3) 本地备份:无论邮件是否成功,生成的文件都保存到服务器本地目录。
4) 异常告警:连续 3 次失败后发送告警给运维人员(用另一个邮件服务器或短信)。
5) 常见原因:SMTP 密码错误、附件太大(>25MB 需拆分或用网盘链接)、被拦截为垃圾邮件(加 SPF/DKIM 记录)。
排查步骤:
1) 检查时间范围:报表查的是 00:00-23:59 还是 08:00-08:00(跨班次),时区是否一致。
2) 检查数据源:直接在数据库执行报表用的 SQL,看结果是否一致。
3) 检查筛选条件:是否有 WHERE 条件遗漏(如排除了某个状态的数据)。
4) 检查计算逻辑:OEE = A × P × Q,确认三个子指标的分母是否正确。
5) 检查数据延迟:MES 数据可能有几分钟到几小时的延迟,建议在非高峰时段生成报表。
6) 加数据校验:在报表中增加"数据时间戳"和"记录数",方便追溯。
1) 邮件分发级:不同角色发送不同报表。产线经理只收到自己产线的数据,总监收到全厂数据。
2) 文件加密:Excel 加密码(wb.security.workbookPassword),PDF 加水印(姓名+日期)。
3) Web 门户:搭建内网报表平台,用 RBAC 权限模型控制谁能看哪些报表。
4) 数据脱敏:对外部人员发送的报表,隐藏敏感字段(如成本数据)。
5) 审计日志:记录每次报表生成和访问的操作日志。
1) 模板文件化:将报表格式定义抽离为 JSON/YAML 配置文件,修改格式不需要改代码。
2) 版本管理:模板文件放在 Git 仓库中,每次修改有记录,可回滚。
3) 多模板支持:日报、周报、月报各一套模板,通过参数切换。
4) 模板热更新:程序启动时加载模板,修改模板后不需要重启服务。
5) 模板验证:修改模板后自动跑测试用例,确保格式和公式正确。
| 面试关键词 | 你要回答的内容 | 加分点 |
|---|---|---|
openpyxl | 生成 Excel + 图表 + 条件格式 | 提到 write_only 模式处理大数据 |
reportlab | PDF 生成 + 中文字体 | 提到 weasyprint 替代方案 |
smtplib | 带附件的 HTML 邮件 | 提到失败重试 + 多通道告警 |
cron | Linux 定时任务 | 提到 Docker + K8s CronJob |
APScheduler | Python 内置调度器 | 提到 BlockingScheduler vs BackgroundScheduler |
Jinja2 | 模板化报表生成 | 提到配置和代码分离 |
| 环境变量 | 敏感信息管理 | 提到 .env / Vault / K8s Secret |
报表自动化开发 — 中际旭创制造业可视化工程师培训
openpyxl • reportlab • smtplib • APScheduler • cron