夜雨聆风学习资料网

ARTICLE · 1117605

Python批量生成PDF报告,测试数据自动出图

Python批量生成PDF报告,测试数据自动出图
上个月开质量会,主管问了一句:"这 9 个批次的过程能力,能不能按批给我一份报告?"

我当时是这么干的:从测试机导数据 → Excel 里算均值标准差 → 手画趋势图 → 贴到 Word 里排版 → 导出 PDF → 换个批次重来一遍。一个批次四十分钟,9 个批次干了一整个下午,还画错了两个规格限。

后来我把它写成了脚本:同样的 9 份报告,13.1 秒出完。 而且 Cp/Cpk 是脚本算的,不会再画错线。

这篇就把整条链路拆开讲:pandas 算、matplotlib 画、reportlab 排、for 循环批量出。文末附可运行的脚本。

解决方案概览

整条链路四段,每段各司其职:

环节
工具
干什么
① 数据真相源
纯 Python + assert
3 产品 × 3 批 × 3 测试项 × 50 点 = 1350 行,勾稽先过
② 算过程能力
pandas
groupby 出均值/σ,算 Cp/Cpk、超差数、判定
③ 画三张图
matplotlib
趋势图(标超差点)、分布图(标规格限+Cpk)、不良柏拉图
④ 排版出 PDF
reportlab
标题+信息行+汇总表+三图+页脚页码,循环批量

实测结果先摆出来:

✅ LOT-260902 | 最小Cpk 0.43 | ⚠ 不达标 | 317 KB✅ LOT-260910 | 最小Cpk 2.12 | 合格 | 278 KB...完成 9 份 PDF,用时 13.1 s

9 份报告、27 张图、1350 行数据,13.1 秒。

分步实操

第 1 步:先锁真相源,再谈出图

出图这件事最容易骗人——图很漂亮,数据是错的。

所以第一步永远是:把生成逻辑写成一个函数,用 assert 卡住,尤其是"问题批必须真的不合格"这种设计意图。

defassert_truth_source():    n_lots = len(PRODUCTS) * LOTS_PER_PRODUCTassertlen(LOTS) == n_lotsassertlen(MEAS) == n_lots * len(ITEMS) * SAMPLES_PER_ITEM     # 1350 行# 不良模式拆分必须等于总不良数(否则柏拉图是假的)for lot in LOTS:assertsum(lot["不良模式"].values()) == lot["总不良"]# 设计意图:问题批必有不达标项,正常批必须全部达标for lot in LOTS:        s = SUMMARY[SUMMARY["批次号"] == lot["批次号"]]if lot["是否问题批"]:assert (s["Cpk"] < CPK_TARGET).any()else:assert (s["Cpk"] >= CPK_TARGET).all()

最后两条断言看着"多此一举",但它救过我:有一次随机数据凑巧让"问题批"看起来合格,报告里写着"判定放行"——如果没这条断言,我就把一份说反了的报告发出去了。

第 2 步:pandas 算 Cp/Cpk

过程能力计算就两个公式,别怕:

defcalc_spec_stats(mean, sigma, item):"""双侧算 Cp 和 Cpk;单边(只有 LSL)只给 Cpk。"""    usl, lsl = item["USL"], item["LSL"]if usl isnotNoneand lsl isnotNone:        cp  = (usl - lsl) / (6 * sigma)        cpk = min(usl - mean, mean - lsl) / (3 * sigma)elif lsl isnotNone:        cp, cpk = None, (mean - lsl) / (3 * sigma)     # 键合拉力这种"≥3.0"的单边规格else:        cp, cpk = None, (usl - mean) / (3 * sigma)return cp, cpk

聚合用 groupby 一行出:

g = (MEAS.groupby(["批次号", "测试项"], as_index=False)     .agg(n=("实测值", "count"), 均值=("实测值", "mean"), 标准差=("实测值", "std")))df = g.merge(SPEC, on="测试项", how="left")

注意这里 std 默认是样本标准差(ddof=1),跟 Minitab 一致;用 numpy 的 np.std() 默认是总体标准差(ddof=0),两者算出来的 Cpk 会差一点点,被客户对上账就尴尬了。

跑出来的 27 条统计看得很清楚:

批次号
产品
机台
最小Cpk
不良率
判定
LOT-260900
PA-2201
L01
2.286
0.50%
放行
LOT-260901
PA-2201
L02
2.162
0.67%
放行
LOT-260902
PA-2201
L03
0.4264.25%不达标
LOT-260912
PA-3302
L04
0.544
4.92%
不达标
LOT-260922
PA-4410
L01
0.386
4.83%
不达标

规律很直白:正常批不良率 0.33%0.83%,问题批 4.25%4.92%,差了近 8 倍。 这种"一眼看出异常批"的能力,才是自动化的真正价值——不是为了省那 40 分钟。

第 3 步:matplotlib 出图(先解决两个坑)

坑一:中文字体。 不设置的话,标题里的中文全是"豆腐块"。

import matplotlibmatplotlib.use("Agg")     # 无显示器环境(计划任务/服务器)必须切 Agg 后端font_manager.fontManager.addfont("C:/Windows/Fonts/msyh.ttc")plt.rcParams["font.family"] = font_manager.FontProperties(fname=FONT_PATH).get_name()plt.rcParams["axes.unicode_minus"] = False# 负号显示成方块的老毛病

坑二:图的留白。 多子图不加 tight_layout 会互相压字。

fig, axes = plt.subplots(1, len(ITEMS), figsize=(12.6, 3.0))...fig.suptitle(f"{lot_id} 测量值趋势(红点=超规格)", fontsize=11)fig.tight_layout(rect=[0, 0, 1, 0.93])    # 给 suptitle 留出顶部空间fig.savefig(path, dpi=150)plt.close(fig)                            # 批量循环里必须关,否则内存越跑越高

三张图分别是:

  1. 趋势图——50 个点的测量序列,规格线画上,超差点标红。
  2. 分布图——直方图 + 规格限 + 目标线,右上角直接打 Cp=0.90 Cpk=0.45,判定不达标就标红。
  3. 不良柏拉图——柱 + 累计百分比双轴,一眼看出"哪一类不良占大头"。

第 4 步:reportlab 排版

reportlab 的思路是"用 Python 拼版":段落、表格、图片都是 flowable 对象,塞进 story 列表,最后 doc.build()。

from reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.ttfonts import TTFontfrom reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,                                Table, TableStyle, Image, PageBreak)# 中文字体:.ttc 是字体集合,必须指定 subfontIndex,否则注册直接报错pdfmetrics.registerFont(TTFont("CN", "C:/Windows/Fonts/msyh.ttc", subfontIndex=0))story = [    Paragraph(f"{lot['批次号']} 测试数据分析报告", title_st),    Paragraph(f"产品 {lot['产品']} | 机台 {lot['机台']} | 日期 {lot['日期']} ...", p_st),    Spacer(1, 12),    tbl,                                        # 过程能力汇总表    Paragraph("二、测量值趋势", h_st),    Image(figs[0], width=17.8 * cm, height=4.2 * cm),    PageBreak(),    Image(figs[1], width=17.8 * cm, height=4.2 * cm),    Image(figs[2], width=12.0 * cm, height=5.3 * cm),]doc.build(story, onFirstPage=decorate, onLaterPages=decorate)

两个细节值得单独说:

① 表格里的中文也要指定字体,否则表头还是方块:

tbl.setStyle(TableStyle([    ("FONTNAME", (0, 0), (-1, -1), "CN"),     # 关键    ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1F4E79")),    ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#9DB7CE")),]))

② 不合格行要标红,让审报告的人不用自己找:

for i, r in sl.reset_index(drop=True).iterrows():if r["Cpk"] < CPK_TARGET:        style += [("BACKGROUND", (7, i + 1), (9, i + 1), colors.HexColor("#FFC7CE")),                  ("TEXTCOLOR",  (7, i + 1), (9, i + 1), colors.HexColor("#9C0006"))]

③ 页脚页码用回调函数打印,每页自动带批次号和生成时间:

defdecorate(canvas, doc_):    canvas.saveState()    canvas.setFont(font_name, 8)    canvas.drawString(1.6 * cm, 0.9 * cm, f"{lot['批次号']} · 自动生成 · {datetime.now():%Y-%m-%d %H:%M}")    canvas.drawRightString(A4[0] - 1.6 * cm, 0.9 * cm, f"第 {canvas.getPageNumber()} 页")    canvas.restoreState()

第 5 步:批量循环 + 总览表

最后一步最简单,一个 for 循环搞定——注意顺序:先出图,再进 PDF。

for lot in routes:    chart_trends(lid, figs[0])      # 1) 出图    chart_hists(lid, figs[1])    chart_pareto(lot, figs[2])    build_pdf(lot, pdf, figs, FONT_NAME)   # 2) 图存好了再排版

顺手再出一张总览表,开会的时候一页看完全部批次:

with pd.ExcelWriter(OVERVIEW_XLSX, engine="openpyxl") as w:    ov.to_excel(w, sheet_name="总览", index=False)

关键参数说明

  • matplotlib.use("Agg"):无 GUI 环境必须切,否则在计划任务里直接崩。必须在 import pyplot 之前调用。
  • fig.savefig(dpi=150):报告用 150 足够(打印够清晰、体积可控)。dpi 拉到 300,PDF 体积大概翻三倍。
  • fig.tight_layout(rect=[0,0,1,0.93]):多子图 + suptitle 必加,不然后者压前者。
  • plt.close(fig):批量循环里必须关图,否则跑几十份就把内存吃满。
  • TTFont(..., subfontIndex=0):.ttc 中文(msyh/simsun)是字体集合,不加这个参数注册会失败或找不到字形。
  • std 的 ddof:pandas 默认 ddof=1(样本标准差),与 Minitab 口径一致,别混用 np.std。
  • Cp vs Cpk:Cp 只看"宽度"(规格宽 / 6σ),Cpk 还看"偏心"。只要 Cpk 达标,Cp 一定达标;Cpk 不达标但 Cp 很高,说明是偏心,去调设备中心值而不是收紧公差。
  • Cpk 门槛 1.33:对应约 4σ,是行业常用的量产门槛;样品/试产阶段可放宽到 1.67 或 1.33 视客户要求。
  • 单边规格只算 Cpk:像键合拉力"≥3.0gf"只有下限,Cp 没有意义,代码里直接返回 None 并在表里显示"—"。
  • Image(..., width=17.8*cm):A4 宽 21cm 减左右各 1.6cm 边距 = 17.8cm,只给宽度 reportlab 会按比例算高度。

常见问题和避坑提醒

  1. PDF 里中文变成方块或空白:字体没注册,或者表格没设 FONTNAME。段落、表格、页脚三处都要指定中文字体,漏一处就有一处方块。
  2. .ttc 注册报错:msyh.ttc/simsun.ttc 是字体集合,必须加 subfontIndex=0。用 .ttf 的 simhei.ttf 可以不用这个参数。
  3. 服务器上跑报错 no display:忘了 matplotlib.use("Agg")。
  4. PDF 里图被裁掉一半:Image 给的 width/height 比例和图本身不一致。要么只给 width 让 reportlab 等比算,要么按图片实际宽高比算高度。
  5. 跑 20 份之后内存爆掉:循环里没 plt.close(fig)。
  6. Cpk 和客户算的对不上:多半是 ddof 差异(np.std vs pandas.std),或 σ 用的是全批数据、客户用的是子组内标准差。
  7. 图里超差点没标出来:先确认判定掩码是"小于 LSL 或大于 USL",单边规格千万别去比对 None,会直接抛异常。
  8. 文件名带中文导致某些环境乱码:脚本里统一 # -*- coding: utf-8 -*-,输出用 sys.stdout.reconfigure(encoding="utf-8")(Windows 控制台尤其需要)。
  9. 批次一多就跑不动:瓶颈几乎全在 matplotlib 出图(每张 0.3~0.5 秒),不是 PDF。要提速可以复用同一个 figure 只换数据,或者上多进程。
  10. 报告日期是"生成时间"而不是"测试时间":这两个别搞混,页脚写生成时间,表头写测试日期,否则追溯时会打架。

总结

手工做测试报告的三个死穴:算错(Cp/Cpk 画错线)、漏批(批次多了忘几个)、不可追溯(三个月后没人记得怎么算的)。

自动化一次就把三个都解决了:

  • 算:pandas 统一口径,公式写死在代码里;
  • 批:for 循环遍历,一个不漏,13 秒出 9 份;
  • 溯:页脚带批次号和生成时间,脚本本身就是算法说明书。

这次跑出来的数据也挺说明问题——正常批不良率 0.33%,问题批 4.9%,差近 8 倍。真正省下来的不是那 40 分钟,而是让你在开会前就能指着屏幕说:"这 3 个批次有问题,其余 6 个放行。"

领取资料

回复【测试数据PDF脚本】领取《测试数据PDF批量生成.py》——单文件可直接运行,含完整注释:

  • 真相源 dict + assert 勾稽(不良模式拆分、问题批必须不达标)
  • pandas 计算 Cp/Cpk/超差数/判定(含单边规格处理)
  • matplotlib 三张图:趋势(超差标红)/ 分布(规格限+Cpk)/ 不良柏拉图
  • reportlab 排版:标题、汇总表(不合格行标红)、三图、页脚页码
  • 批量循环 + 总览表输出,覆盖前自动备份

三种模式:--selftest 断言 + 生成 1 份验证 / --dry-run 只看计划 / --limit N 试跑前 N 份。中文字体自动探测(Windows/macOS/Linux 常见路径都覆盖了)。


第 45 篇 · 半导体工程师 AI 提效系列(第七周)

相关学习资料