舞台工艺检测表单自动生成:用 Python 与智能体 Skill 打通规范限值到可填表的数据流
一、背景:94 张检测表单,为什么不能再手写
一套《舞台工艺设备第三方检测服务体系》,落到最末端的交付物是检测记录表单——现场人员拿着它去测实物,一栏栏填实测值,最后给出结论。这份体系现有的表单册有 94 张表(版本由 v2.5 升到 v2.6),体系合集 16 章(v1.5 升到 v1.6)。
量级摆在这里,手写就不再是"慢一点"的问题,而是结构上无法维护。原因有三。
其一,限值必须有据可查,手写很容易漂移。 一张表的核心是"依据标准 + 测试条件 + 判定口径 + 限值"。限值从哪来、是哪个版本的正文、有没有被新标准取代,全都要能回溯。手写在 Word 里,改了第三版之后没人说得清第一版的限值是从哪本标准抄的。
其二,编号与张数是联动的,手写必然不同步。 表单号由表型字母、段号、段内张数共同决定;某一段补建几张表,段内编号要重排,册内总张数要改,体系合集目录要改,参数清单要改——牵一处动四处。最近一次补建就是典型:视频显示性能方向一次补齐,JL-A4 段由 5 张扩到 10 张(P02 亮度与亮度均匀性、P03 对比度、P04 像素失控率、P05 拼缝与平整度、P06 色度与光学特性),参数清单侧的表单计数随之由 87 变到 92。
其三,改完没有自检手段。 编号重排之后,上一版的旧数字会残留在各个 md 里:“N 张”"未建表"这些字样总有几处没跟上。没有全库扫描,就只能靠人回忆改过哪些文件。
这类表单的现场使用链路是固定的:领表 → 按测试条件布场 → 逐栏填实测值 → 按判定口径出结论 → 归档进检测报告。它看起来是最后一道工序,实际是整套体系里唯一会被反复复制、反复分发的东西——一份报告只用一次,一张表可能用几十次。正因如此,表本身的任何一处不一致都会被成倍放大:编号写错一次,后面所有引用它的文件跟着错;限值抄错一个字,现场所有按它判的结论都得回头复核。
于是思路很清楚:限值表做成正源,表单由生成器从正源渲染出来,改了正源就重生成,重生成之后跑校验脚本。 人只负责维护正源,编号、张数、页码、排序一致性全部交给脚本。
先把两套体系的口径说清楚,这是本文的前提:检测是事后测实物,对象是已经装好的设备与系统,结论落在"合格/不合格(否决)/限期整改/扩大抽样"这四类上。它与"事前看图提意见"的审图体系是两套东西,判级口径、措辞体系都不通用。本文只讲检测记录表单的生成工程,不涉及任何问题裁定与分级方法。
二、环境与结构:正源、生成器、校验脚本三层
2.1 目录约定
detect-service/
├── data/
│ ├── limits/ # 限值表正源(JSON,唯一可改的地方)
│ │ └── JL-A4_video.json
│ └── std_registry.json # 标准库登记:代号 -> 层级 / 现行性 / 名称
├── scripts/
│ ├── formno.py # 表单号编号器 + 张数联动校验
│ ├── gen_form.py # 限值条目 -> 空白记录表单 docx
│ ├── build_book.py # 合册:目录 + 页码
│ ├── measure_pages.py # 实测页码,回填 _pagemap.json
│ ├── verify_toc.py # 目录页码漂移校验
│ ├── verify_docx_order.py # 成品反抽:依据标准效力序
│ ├── verify_docx_style.py # 样式校验(须先对基线)
│ └── grep_consistency.py # 全库一致性自检
├── out/ # 生成产物(只读)
└── 归档/ # 历史留痕,不改
2.2 运行环境
- 操作系统:Windows 11(Git Bash 下执行)
- Python:3.10 及以上
- 依赖:
python-docx==1.1.2(版本写死,避免输出结构随库升级漂移)
python -V
pip install python-docx==1.1.2
2.3 三个关键约定
约定一:限值的唯一来源是现行标准正文。 不以任何真实项目报告里的实测值当限值,也不凭经验拍一个数。正源里凡是没核到的条目,一律写成待核占位,而不是填一个"看着差不多"的数字。
约定二:表单号由规则生成,不手编。 表型字母 C / I / K / P 决定尾词(检查表/检测表/考核表/性能表),段号加段内序号构成编号串,例如 JL-A4-P02。
约定三:置信标注随表走。 每条限值带 ★ / △ / ○ 三级置信标注:★ 为已核到标准正文,△ 为有出处但需复核,○ 为待核。现场填表的人一眼能看出哪些限值是可直接判的、哪些还要回核。
三、步骤:从规范到可填表的九步数据流
一次补建或一次版本升级,按下面九步走,顺序不能乱。
第 1 步:标准现行性四查。 联网核四件事——性质(强制性还是推荐性)、名称与编号、是否被废止、是否被 GB 55xxx 系列取代。四查不过,后面全部停住。举例:视频显示性能方向的限值依据为 GB/T 43770—2024、SJ/T 11141—2025、SJ/T 11281—2025;而 SJ/T 11141—2017 与 GB/T 29458—2012 已废止,不能作为依据写进表。
第 2 步:建限值表。 结构固定为主表 + 测试条件 + 判定口径 + 待核清单,每条带 ★/△/○ 置信标注。
第 3 步:建表单号。 表型字母 + 段 + 段内张数 → 编号串,尾词由表型字母决定。
第 4 步:同步参数清单。 规范溯源表增标准行、参数表增行、缺口表对应项改成"已闭环"。三处必须同一次改完。
第 5 步:登记标准库。 层级表 + 各系统明细表;出现新代号时必须先扩层级定义,再谈登记。本次引入 SJ/T(电子行标)就是这种情况。
第 6 步:同步周边文件。 这一步最容易漏——一次建表会牵动 4 个文件:体系总纲的版本注记与架构树、检测记录与成果文件体系、服务范围与边界、目录结构说明。漏改任何一个,都会出现"表里有了、总纲里没有"的错位。
这四个文件各自维护着体系的一个切面:总纲管版本与架构,成果文件体系管表单与报告的对应关系,服务范围管哪些项目在边界内,目录结构说明管文件怎么摆。表单是它们的共同下游,所以表单一变,四个切面必须同时反映。实际操作中把这一步单列出来、改完逐个打勾,比指望"改的时候顺手带上"可靠得多。
第 7 步:升级排序脚本常量。 引入新标准族代号时,脚本里的正则与层级映射必须同步改,否则该族标准根本不进检查(详见坑 2)。
第 8 步:回流专家团。 更新成员红线速查,把 references 摘要表按表单号挂上去。
第 9 步:跑排序自检至 0 倒挂 → 记版本号 → 另排一次重生成 docx。
两条原则贯穿全程。
原则一:限值以现行标准正文为准,不以真实项目报告的实测值、也不以经验拍限值。先做第 1 步四查,再建表。
原则二:规范原文没读过的领域,不凭空造表。 例如威亚(WH/T 78.7)、幕布(WH/T 78.9)、A6 电气与消防这类尚未通读原文的领域,宁可挂起、标成待核,也不硬凑一张看起来完整的表出来。一张编出来的表比一张缺失的表危险得多——缺失会被发现,编造的不会。
口径铁律(写在生成器里,不靠人记):检测结论只有四类——合格/不合格(否决)/限期整改/扩大抽样。不使用其他体系的等级标记,也不使用"应/须整改"这类属于另一套体系的措辞。两套体系不串。
四、代码与命令:四块可运行实现
4.1 限值表正源
正源用 JSON,字段与第 2 步的四个部分一一对应。注意 limit 字段留的是占位——没核到标准正文就不填数。
{
"segment": "JL-A4",
"system": "视频显示性能",
"standards": [
{ "code": "GB/T 43770", "year": "2024", "status": "现行" },
{ "code": "SJ/T 11141", "year": "2025", "status": "现行", "replaces": "SJ/T 11141—2017" },
{ "code": "SJ/T 11281", "year": "2025", "status": "现行" },
{ "code": "GB/T 29458", "year": "2012", "status": "已废止", "usable": false }
],
"items": [
{
"id": "P02",
"name": "亮度与亮度均匀性",
"basis": ["GB/T 43770—2024"],
"condition": "环境照度按现场最不利工况取值,设备预热至热平衡",
"criterion": "实测值不低于标准正文限值;均匀性取最不利点",
"confidence": "★",
"limit": "<按标准正文登记>"
},
{
"id": "P06",
"name": "色度与光学特性",
"basis": ["SJ/T 11281—2025"],
"condition": "<待核:测试距离与视场角>",
"criterion": "<待核>",
"confidence": "○",
"limit": "<按标准正文登记>"
}
]
}
4.2 表单号编号器与张数联动校验
# formno.py —— 表型字母决定尾词;段 + 段内序号 -> 编号串
SUFFIX = {'C': '检查表', 'I': '检测表', 'K': '考核表', 'P': '性能表'}
def form_no(kind: str, segment: str, index: int, width: int = 2) -> str:
"""('P', 'JL-A4', 2) -> 'JL-A4-P02'"""
if kind not in SUFFIX:
raise ValueError(f'未知表型 {kind}')
return f'{segment}-{kind}{index:0{width}d}'
def tail_word(kind: str) -> str:
return SUFFIX[kind]
def verify_counts(registry, forms):
"""registry: {段: 段内张数};forms: 实际生成的编号串列表"""
bad = []
for seg, expect in registry.items():
actual = len([f for f in forms if f.startswith(seg)])
if actual != expect:
bad.append((seg, expect, actual))
return bad, sum(registry.values())
编号器解决的是"手编会错",verify_counts 解决的是"张数变了别的地儿没跟上"。JL-A4 段 5 → 10 那次,registry['JL-A4'] 改成 10,重跑一次 verify_counts 就能确认册内实际张数与登记一致,总数字段也一并回吐。
4.3 生成器:限值条目 → 空白记录表单
# gen_form.py —— 从限值条目渲染空白检测记录表单
from docx import Document
HEADERS = ['序号', '检测项目', '依据标准', '测试条件', '判定口径', '置信', '实测值', '结论']
CONCLUSION_HINT = '结论仅填:合格 / 不合格(否决) / 限期整改 / 扩大抽样'
def add_meta_table(doc, no, item):
t = doc.add_table(rows=0, cols=2)
t.style = 'Table Grid'
basis = item['basis']
for k, v in [
('表单号', no),
('检测项目', item['name']),
('依据标准', '、'.join(basis) if isinstance(basis, list) else basis),
('测试条件', item['condition']),
('判定口径', item['criterion']),
('置信标注', item['confidence']),
]:
cells = t.add_row().cells
cells[0].text = k
cells[1].text = v
return t
def add_record_table(doc, rows=10):
t = doc.add_table(rows=1, cols=len(HEADERS))
t.style = 'Table Grid'
for i, h in enumerate(HEADERS):
t.rows[0].cells[i].text = h
for r in range(rows):
t.add_row().cells[0].text = str(r + 1)
return t
def build_form(item, no, out_path):
doc = Document()
doc.add_heading(f'{no} {item["name"]}检测记录', level=1)
add_meta_table(doc, no, item)
doc.add_paragraph(CONCLUSION_HINT)
add_record_table(doc)
doc.save(out_path)
注意 CONCLUSION_HINT 是被写进成品的。口径铁律不让靠人记,就让它变成表单上的一行字——现场填表的人不需要先读过体系文件才知道该填什么。
还有一处设计取舍值得说明:生成器只渲染空白表,不把限值固化进成品正文。 限值、测试条件、判定口径都放在元数据区,随正源走。这样标准更新时只需改正源重生成,不必去几十张成品里逐个替换数字;反过来,如果当初把限值敲死在成品里,一次标准换版就意味着一轮全量返工,而且极容易漏改。
4.4 效力序:把 SJ/T 加进层级映射
效力序序列为 GB → GB/T → JGJ → WH/T → SJ/T → DB。其中建筑行标与文化行标同档(同档内 JGJ 先于 WH/T),电子行标 SJ/T 为独立一档(层级值 3),地方标准 DB 为最后一档。行标一律不得排在 GB / GB/T 之前。
# std_tier.py —— 效力序:六个代号、五档层级
import re
TIER = {'GB': 0, 'GB/T': 1, 'JGJ': 2, 'WH/T': 2, 'SJ/T': 3, 'DB': 4}
SUB_ORDER = {'JGJ': 0, 'WH/T': 1} # 同档内建筑行标先于文化行标
STD_RE = re.compile(r'\b(GB/T|GB|JGJ|WH/T|SJ/T|DB\d*/T|DB)\s*(\d+(?:\.\d+)*)')
def parse(code):
m = STD_RE.search(code)
if not m:
return None
parts = tuple(int(x) for x in m.group(2).split('.'))
while len(parts) < 2:
parts += (0,) # 无分部号补 0,保证同构可比
return m.group(1).upper(), parts
def tier_of(prefix):
return TIER.get(prefix, 9)
def sort_key(code):
got = parse(code)
if got is None:
return (9, 0, (0, 0))
prefix, parts = got
return (tier_of(prefix), SUB_ORDER.get(prefix, 0), parts)
parts 用元组而不是数字:标准号点号后面是分部号,不是小数位,按数值比会判反方向。
新增代号必须同时改 STD_RE 和 TIER。 只改其中一个,该族标准根本不会被这个脚本看见——这不叫"检查通过",这叫"没检查"。
4.5 成品反抽:合并单元格要先去重
从成品 docx 反抽依据标准做复核时,有一类假阳性必须处理:参数表的"依据标准"行若跨列合并,python-docx 会把同一个单元格文本在每一列上都返回一次,接缝处相邻出现的两个标准号会被误判成倒挂。
# verify_docx_order.py —— 片段:同文本去重后再收号
def cell_texts(table):
"""跨列合并单元格会被重复返回,先按首次出现去重"""
seen, out = set(), []
for row in table.rows:
for c in row.cells:
t = c.text.strip()
if t and t not in seen:
seen.add(t)
out.append(t)
return out
def collect_codes(texts):
return [m.group(0).strip() for t in texts for m in STD_RE.finditer(t)]
去重的顺序很重要:先去重、再收号。反过来先收号再去重,会把同一格里本来合法的顺序重复算进来,反而制造新的假阳性。
4.6 全库一致性自检
# grep_consistency.py —— 改完全库扫一遍残留旧数
import re
import sys
from pathlib import Path
COUNT_RE = re.compile(r'(\d{2,3})\s*张')
STATUS_RE = re.compile(r'未建表|不建表')
SKIP = re.compile(r'(归档|backup|备份|\.git)')
def scan(root, expect_total):
"""info:张数已对齐的位置(留痕用);bad:需要人工处理的残留"""
info, bad = [], []
for p in Path(root).rglob('*.md'):
if SKIP.search(str(p)):
continue
for i, line in enumerate(p.read_text(encoding='utf-8').splitlines(), 1):
text = line.strip()[:60]
if STATUS_RE.search(line):
bad.append(('建表状态残留', str(p), i, text))
m = COUNT_RE.search(line)
if m:
if int(m.group(1)) != expect_total:
bad.append(('张数不一致', str(p), i, text))
else:
info.append((str(p), i, text))
return info, bad
if __name__ == '__main__':
root, total = sys.argv[1], int(sys.argv[2])
info, bad = scan(root, total)
print(f'已对齐张数 {len(info)} 处')
for h in bad:
print(h)
print(f'待核 {len(bad)} 处')
sys.exit(1 if bad else 0)
脚本把两类结果分开:info 是正常的、张数已对齐的位置,只做留痕;bad 才是"张数不一致"与"建表状态残留",非空就返回 1。这样跑一次就能确认全库只剩一种数字。
SKIP 那行是刻意的:归档目录与各备份目录是历史留痕,里面留着旧数字属于正常,扫出来也不改、也不计入 bad。
4.7 一次完整生成的命令串
# 第一遍:先出成品,拿到真实页码
python scripts/build_book.py --src data/limits/ --out out/
python scripts/measure_pages.py out/某剧院_检测记录册.docx # 回填 _pagemap.json
# 第二遍:页码收敛后重新合册
python scripts/build_book.py --src data/limits/ --out out/
# 校验
python scripts/verify_toc.py out/某剧院_检测记录册.docx # 期望"漂移=无"
python scripts/verify_docx_order.py out/某剧院_检测记录册.docx # 期望倒挂 0
python scripts/grep_consistency.py docs/ 92 # 期望 0 处待核
页码之所以要两遍,是因为目录页码是成品的真实属性,第一遍还没有成品、无从得知;measure_pages.py 回填 _pagemap.json 之后第二遍才能把目录写准。223/223 全对这个结果,就是两遍收敛跑出来的。
五、踩坑实录:十个真实坑位
坑 1|排序脚本只查 4 类对象。 它的扫描范围是:.py 里的依据串、md 表格行、md 表列(含跨行清单)、docx 成品反抽。blockquote(> 开头)与 - 列表项不在扫描范围内。 后果很具体:同一个顺序错误写在表格里会被抓出来,写在引用段落或列表项里则完全不报。因此这两个位置必须人工补核,不能因为脚本全绿就认为全文没问题。
坑 2|新代号静默跳过。 引入 SJ/T 这类新标准族代号时,必须同时改 STD_RE 正则与 TIER 映射。只改一处,该族标准根本不进入检查——脚本照常返回 0,看起来一切正常,实际等于没检查。这类"静默跳过"比报错危险得多。
坑 3|合并单元格假阳性。 "依据标准"行跨列合并时,python-docx 会把同一单元格文本重复 N 次,接缝处相邻两个标准号被误判倒挂。对策是先按首次出现对同文本去重,再收号(见 4.5)。
坑 4|生成器改了版本号,配套校验小脚本要一起改。 measure_pages.py、verify_toc.py、verify_docx_order.py、verify_coll_toc.py、update_fields_coll.py 里如果硬编码了默认的 docx 文件名,主生成器升版后不同步改,这些脚本就会静默地继续校验旧版文件,新版的问题一个都发现不了。改版时把这五个脚本当作一个整体来改。
坑 5|页码要两遍收敛。 build → measure_pages.py 回填 → build,重复至 verify_toc.py 报"漂移=无"。指望一遍成型是不现实的,因为页码取决于成品本身。
坑 6|样式校验先对基线。 verify_docx_style.py 报出问题后,第一件事是拿上一版跑一遍:新版有、旧版无的,才是本次引入的回归;旧版也有的,属于既有缺陷,应当单独立项,不要混在本次改动里"顺手修",否则会把回归记录搅乱。
坑 7|版本号按各文件实际序列 +1,不要凭直觉。 曾经出现过末版是 v1.4、新的却写成 v1.6 的情况——凭印象跳了一位,导致版本链对不上。升版前先读一遍该文件自己的版本序列。
坑 8|归档目录不动。 归档/ 与各备份目录是历史留痕,里面即使留着旧数字、旧编号也不改。改动它们的唯一后果是破坏可追溯性;一致性扫描时应当把它们排除在外(见 4.6 的 SKIP)。
坑 9|PowerShell 脚本零中文。 PowerShell 5.1 按 ANSI 读取脚本文件,插入中文注释会导致解析崩掉。这类脚本的注释一律用英文写,不要让一个注释毁掉整条自动化链路。
坑 10|改完必全库 grep 一次。 关键词至少包括:N 张、未建表、不建表、以及本次新增的主题词(例如"视频显示性能")。本次 JL-A4 段补建后,靠这一遍扫出了多处残留旧数。这一步不贵,但漏了它,前面九步的成果会带着旧数字一起发出去。
六、总结:把"造表"变成"维护正源"
回头看,这套东西真正的改变不是"省了打字",而是把维护对象换掉了:从维护 94 张成品表,变成维护一份限值正源加一组脚本。
落到可勾选的清单上:
- 限值只认现行标准正文,四查(性质/名称编号/未废止/未被取代)不过就停住,不用实测值、不用经验值
- 核不到的条目留占位,标 ○ 待核,不硬凑;原文没读过的领域宁可挂起
- 表单号由规则生成,表型字母决定尾词,段内张数变化走
verify_counts联动校验 - 结论口径写进成品,合格/不合格(否决)/限期整改/扩大抽样,四选一,不串用其他体系的等级与措辞
- 新标准族代号进脚本要改两处:正则与层级映射,少改一处等于没检查
- 成品反抽先去重再收号,规避合并单元格假阳性
- 脚本已知边界要人工补:blockquote 与列表项不在排序扫描范围内
- 升版时把五个配套校验脚本当一个整体改,避免静默校验旧版
- 版本号按实际序列 +1,归档目录不动,PS 脚本零中文
- 收尾全库 grep 一遍,确认没有残留旧数
最后补一句关于边界的话。这套工程解决的是"表单能不能被正确、一致、可回溯地生成",它不解决"现场该测什么、测得对不对"。前者是确定性的,可以全部交给脚本;后者依赖专业判断,脚本给不了结论,也不该给。把确定性部分尽量做干净,人的注意力才能留在真正需要判断的地方。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/ZhuoAIStage/article/details/168531540




