从"给人看的 Excel"到"能入库的规范行":点检表数据清洗逻辑拆解
车间每天产出的是纸质点检表的电子版------供人阅读、签字、归档。项目要的是在线可管理的账票:结构稳定、字段整齐、能按设备追溯。这两者之间的落差,就是数据清洗要解决的问题。
本文拆解这套清洗逻辑:它怎么认出表头、履历和点检项目,怎么决定数据装进哪张表,认不出来时又怎么让人知道。相关实现基于AI办公助手程序,连同界面代码约 7,500 行,纯本地离线运行。
一、先定管理目标:四类对象,一套编码
动手解析 Excel 之前,先要回答的是"数据进库以后怎么管"。围绕这个目标,管理上先定了四件事。
一个 Sheet 就是一张点检报表。 点检表的组织单位是"表"而不是"文件":一份 Excel 里有几个 Sheet,数据库里就有几条报表记录,一一对应。
点检项目挂在报表下面。 报表回答"这是哪台设备、哪种频次的哪张表",项目回答"这张表要检哪些点"。层级固定,不会出现没有归属的项目。
修改履历单独留痕。 谁在什么时候改了什么,一行一条单独存,不和报表主体混在一起;"改过几次"能直接查出来,不必翻历史文件。
实际点检数据另表存放。 在线填报后产生的实测值属于业务运行数据,与"表框架"分开管理,清洗只处理后者。
1.1 串起来的是 report_code,不是三个编码
这里有一个容易说错的点,值得先纠正:三张表各有自己的自增主键 (check_report_id / check_project_id / change_record_id),但跨表关联一律不走主键,统一走 report_code。
点检项目表里还有 check_code(EIRITEM + 日期 + 编号),修改履历表里还有 change_code------它们各自是"业务编号",用于对外标识和外部系统对接,不承担跨表关联 。把三者说成"三个编码各自串联一条线"是错的,真实的连接只有一条:报表是主,项目和履历都靠 report_code 挂上去。
由此形成三条管理口径,后面所有识别规则都往这里对齐:
- 模板统一:一套表单结构适配多产线、多设备;
- 口径统一:同名字段在全系统保持同一含义(这条落地时踩过坑,见 5.5.2 的表头污染);
- 留痕统一:任何改动都能回答是谁、何时、改了什么。
二、清洗的终点与两条硬约束
2.1 三张表的产出关系
| 表 | 角色 | 关联方式 | 每次清洗产出 |
|---|---|---|---|
dv_check_report |
点检报表(主表) | report_code 是该报表的唯一码;靠 machinery_code 与设备关联 |
每个 Sheet 1 行 |
dv_check_project_info |
点检项目 | report_code 指向报表;另有 machinery_id + machinery_id_type(1 车间 / 2 产线 / 3 设备 / 4 工程) |
M 行 |
dv_check_change_record |
修改履历 | report_code 指向报表 |
N 行 |
| 点检记录表 | 记录实际点检数据 | --- | 不在清洗范围 |
完整字段清单见附录 A。
2.2 两个推论
第一,表头字段取不全,设备就关联不上。 machinery_code 是连向设备表的钥匙;钥匙丢了,后面所有数据都挂不上设备。所以表头识别的精度排在所有规则的第一位。
第二,一张点检表只对应一个编码。 多个 Sheet 各自独立成表。该语义已由使用方确认,因此"10 个文件、29 个 Sheet"产出 29 行报表记录属正常语义,不是缺陷。
2.3 两条硬约束
纯本地离线。 清洗链路不调用任何网络或 AI 服务,词典与规则全部在本地。现场环境不能联网,这条没有商量余地。
Excel 不外发。 任何诊断信息都必须在"不泄露业务内容"的前提下,足以定位问题。这条约束直接决定了第八节的设计。
三、难点:不在算法,在表格没有结构约束
点检表由人填写、供人阅读,天然带着大量"便于阅读却不利于解析"的特征。数据库要的是另一种东西:字段固定、类型明确的规范行。中间的翻译就是清洗,难点集中在四条:
- 表头位置不固定:可能在第三行、第五行,也可能到第三十二行才进入点检项目区;
- 标签和值不在相邻列 :实测设备编码的取值位置相对标签偏移 +6 列;
- 同一个概念有多种写法:改订内容 / 修订内容 / 变更内容 / 修改履历 / Change Log;
- "没有"和"没认出来"长得一样:表里本来没有履历区,和程序没识别出履历区,输出完全相同。
3.1 第四条最要命
它意味着:打开结果表看到空白时,你无法判断是这张表本来就没填,还是程序没看懂。
因此这个模块的设计目标不是"聪明",而是可解释、可诊断------每一步判断都能说明依据,判断错了能被记录下来、被人抓出来。后文所有设计(尤其是第六、八节)都服务于这个目标。
3.2 现场 Excel 的九个实测特征
| 特征 | 实测表现 | 对解析的影响 |
|---|---|---|
| 多版本 | 同类点检表存在多个排版版本 | 行列位置不能写死 |
| 多 Sheet | 10 个文件共 29 个 Sheet | 每个 Sheet 独立成一条报表记录 |
| 表头漂移 | 第 3 行、第 5 行,甚至第 32 行才进入点检项目区 | 必须先定位表头再取值 |
| 合并单元格 | 项目、区分列大量纵向合并 | 空白格要靠向下继承补 |
| 标签与值不同列 | machinery_code 相对标签偏移 +6 列 |
同列右邻取不到值 |
| 说明区与图例 | "点检OK:○ 点检NG:×"、签字栏、备注 | 容易被当作数据行 |
| 概念多写法 | 改订内容 / 修订内容 / 变更内容 / 修改履历 / Change Log | 单一关键词覆盖不全 |
| 页脚页码 | 1/3 形式的页码 |
报表名需与页脚拼接 |
| 大段空行 | 样例表自带 14~28 行全空行 | 边界容易被空行打断 |
后两行会在后面反复出现:页码决定报表名能否拼对(5.5.1),空行决定履历边界停在哪(5.5.2)。
四、解法:六步流水线
css
上传(单文件 / 多文件 / .zip)
│
├─① 读表 header=None 原样读入,跳过名字以 Sheet 开头的页
├─② 表头元数据 关键词 + 向右偏移 → line_code / machinery_code / product_no ...
├─③ 修改履历 找表头行 → 定数据区 → 列映射 → 6 个标准列
├─④ 点检项目 三模式探测(日常 / 条件 / 周度)→ 项目 + 基准 + 方法 + 频度
├─⑤ 表角色映射 决定哪个 DDL 装哪类数据(语义优先,非位置)
└─⑥ 导出 一个 xlsx,每个建表语句一张 Sheet(N 张,不固定 3 张)
第①步刻意不做"智能推断" :以 header=None 原样读入,避免 pandas 自行认定表头;并跳过名字以 Sheet 开头的页,排除模板附带的空白页。
4.1 失败即空表:一个刻意的选择
①~④ 任一步认不出来,输出空表,但不报错。
目的是让一张坏表不拖垮整批文件。代价很直接------"表里没数据"和"程序没认出来"在结果上无法区分。这个缺口只能靠过程记录补(第八节),这也是为什么记录不是一个可选项。
4.2 第⑤步:表角色映射曾出过一次严重错位
早期实现按位置 映射:第 1 张 DDL 装主表、第 2 张装履历、第 3 张装点检项目。而用户实际上传的 DDL 顺序是 report → project_info → change_record,第 2、3 张正好对调------
结果是点检项目和修改履历互相装错表,字段名对不上,两张表同时全空。用户看到的现象就是"点检项目一个内容都没有,修改履历也没有"。
现在改为语义三轮判定:表名关键字 → DDL 字段特征 → 位置兜底,并把判定依据写进过程记录:
scss
dv_check_project_info(idx1) → project_info(依据:表名含 project_info)
t_c(idx2) → change_record(依据:字段含 change_content)
x2(idx1) → change_record(依据:第 2 张表位置兜底)
五、三层识别:词典、位置、边界
整套识别没有使用任何机器学习,是三层启发式规则叠加的结果。
5.1 第一层:词典
默认配置 DEFAULT_CONFIG 中维护五张词表:
| 词表 | 作用 | 示例 |
|---|---|---|
header_keywords |
表头字段 | 生产线 / 机械番号 / 制品品番 → line_code / machinery_code / product_no |
| 履历表头(代码内 11 种) | 定位履历区 | 改订内容 / 修订内容 / 变更履历 / Change Log / Revision |
revision_column_keywords |
履历列 | 年月日 / 担当 / 确认 / 承认 |
check_item_column_keywords |
点检项目列 | 点检项目 / 水准 / 方法 / 频度 / № / 区分 / 表示 |
section_markers.end_markers |
结束边界 | 签字 / 确认 / 承认 / 批准(项目侧另有 点检者 / 监督者) |
匹配方式是去空格后的子串匹配:"点检 项目"也能命中"点检项目",用于应付人工敲入的不规则空格。
5.2 第二层:位置
值主要靠位置找,而不是靠列名。三处偏移各不相同:
| 位置 | 向右扫描 | 说明 |
|---|---|---|
表头元数据 extract_module1_header |
1~12 格 | 取得标签右侧第一个非空值 |
表单键值对 _scan_form_pairs |
1~5 格 | 扫 25×50 区域,构建"标签 → 值"字典 |
报表字段兜底 _build_report_data |
1~15 格 | 兜底扫描,范围最大 |
偏移量会写进过程记录(如 machinery_code+6)。偏多少,直接反映这张表的排版有多不规矩。
5.3 第三层:边界
| 场景 | 边界规则 |
|---|---|
| 修改履历 | 从表头下第一个非空行开始,遇到第一个整行全空就停;或遇到"签字 / 确认 / 承认 / 批准"(且该格不超过 10 字);都找不到时取起点 +50 行 |
| 点检项目 | 遇到"签字"、单元格精确等于结束词、或含"点检者 / 监督者"就停 |
点检项目侧特别强调"精确等于":若按子串匹配,"目视确认"这类正常内容会被当成结束词,数据区被提前截断。
5.4 为什么不用 AI
四条理由都是工程约束,不是偏好:
- 离线:现场不能联网;
- 确定性:同一张表跑两次必须得到相同结果,否则无法回归、无法审计;
- 可解释:入库数据出问题时要能回答"这个值来自哪个单元格";
- 可维护:规则写在 JSON 里,换产线改配置而不改代码。
代价是规则的边界就是识别的上限------词表没覆盖的表达一律认不出。这也决定了优化路径:不是换模型,而是把"认不出"变成可枚举、可逐个修掉的问题。
5.5 三个模块的识别细节
5.5.1 表头元数据
javascript
扫描前 20 行 × 前 80 列
命中关键词 → 向右 1..12 格取第一个非空 → 字段值
字段名含 "code" 的自动转大写
报表名:前 10 行找含「点检表 / 检查表 / 日常点检 / 周点检 / 月度点检 / 日点检」的单元格
再用正则 \d+/\d+ 抓页脚(如 1/3),拼成「报表名1/3」
具体到某张表:machinery_code 的标签与值相隔 6 列,程序按规则右扫 12 格,在第 6 格取到值,并把 +6 写进记录。
报表行字段有三条来源,优先级 ③ > ② > ①:
| 序号 | 来源 | 说明 |
|---|---|---|
| ① | extract_module1_header |
关键词 + 向右偏移 |
| ② | _scan_form_pairs |
扫描 25×50,"标签 → 右邻 1..5 格" |
| ③ | 场景 JSON 的 search_keywords |
直接覆盖 ② 的标签映射 |
这条优先级对排障很实用:改了 header_keywords 却不生效,多半是被场景配置的 search_keywords 覆盖了。这是排查字段缺失的第一步。
5.5.2 修改履历
① 全表扫 11 种履历表头写法 → 记录所有命中的表头行(支持多张履历表)
② 起点:表头行下 4 行内第一个有任意非空值的行
③ 终点:第一个整行全空 / 命中结束词 / 兜底 +50 行
④ 列映射:表头行的关键词 → 6 个标准列(内容 / 日期 / 担当 / 确认 / 承认 / 序号)
⑤ 位置兜底:一个列名都没命中时,按列位置硬套 0..10
第 ⑤ 步是整个模块最危险的设计。 它让程序"总能给出答案",包括错误的答案------它会把"确认"的数据当成"承认"装进库,而且不报错。目前用 W_M2_POSITION_FALLBACK 单独标注,供人工复核。
这里还踩过一个更隐蔽的坑:表头行被当成表单扫描 。表头是 改订内容 | 年月日 | 担当,按"标签 → 右邻"取值时,"改订内容"的右邻正好是"年月日",于是 content 字段取到了 年月日------29 行报表记录全被污染。修法是先划出表头区域,区域内不参与取值;右邻本身还是表头词就继续往右找,找不到宁可留空。
全表扫描并允许多张履历表,是为了适配同一 Sheet 内分段记录多次修订;而边界只认"第一个整行全空",表里本身含大段空行时就容易截早------这条与"删除空行"规则天然冲突,详见第九节。
5.5.3 点检项目:三种模式按固定优先级探测
| 顺序 | 判定条件 | 提取方式 |
|---|---|---|
| 1 | 前 40 行出现 项目: / 项目: |
日常表:逐格扫描,序号自增,向下 1~3 行找"点检方法" |
| 2 | 同一行 同时出现 表示 与 频度 |
条件表:列映射 + 合并单元格继承,拼成 项目,表示为:X(1/W段取时),频度强制改写为 1/W |
| 3 | 出现 点检项目 表头 |
周度表:按列关键词映射,item_no / check_item_type 向下继承 |
三种模式的差异不只在触发条件,也在数据组织方式:日常表以 项目: 为锚点逐格扫描,方法常写在项目名下方而非同一行;条件表依赖"表示"与"频度"同行出现,需列映射并用合并单元格继承补全层级,最后把多个字段拼成一句文本;周度表走标准表头映射,用向下继承补偿合并单元格造成的空值。
这里出现的 item_no、check_item_type、check_frequency 是清洗中间列名,入库时还要再映射到 content、judgment_criterion、cycle 等 DDL 字段。
条件表的"强制 1/W"是已知硬编码,后果是无法从数据反推真实周期。这也是后来把条件表固定按周处理的原因(见第六节)。
六、场景与表单类型
场景 = 一套可覆盖 DEFAULT_CONFIG 的配置 + 一个 report_type。界面下拉里的场景,就是 cleaning_scenarios/*.json 的文件名。
| 场景 | report_type | 配置形态 |
|---|---|---|
| 日常点检表 | 1 | 全量(12 个键,可逐项改规则) |
| 周度点检表 | 2 | 精简(只写名字、说明、类型,其余回落默认) |
| 月度点检表 | 3 | 精简 |
| 年度点检表 | 4 | 精简 |
| 条件点检表 | 2(固定按周) | 全量(列拼接规则依赖配置) |
| 安全装置点检表 | 3(与月度同号) | 精简 |
配置形态的差异有明确理由:周、月、年三张表与日常共用同一套默认提取逻辑,只差一个 report_type;若每种都写全量,以后改默认规则就要改四处,迟早漏改。条件表相反,它的"区分 + 项目 + 表示"拼接逻辑依赖具体配置项,不能靠默认值兜底。
选错场景不会静默通过。 清洗时拿报表名与所选场景比对:报表名含"条件"却选了非条件表、或写"周度"却选了月度,都会记一条 W_SCENARIO_NAME_MISMATCH。比对前先剔除"2026年"这类日期片段,避免把年份误判成年度表。
七、批量处理与失败隔离
- 输入 :单文件、多文件,或一个
.zip(可含子目录); - 安全阀 :单包 ≤ 500 条目,解压总量 ≤ 512MB,单条目 ≤ 128MB;
../与绝对路径一律拒绝; - 合并 :每个文件各自清洗,结果按表纵向
concat(ignore_index); - 隔离 :某个文件解析失败就跳过,记
E_FILE_FAILED,其余继续; - 输出 :一个 xlsx,Sheet 数 = 建表语句数,而不是固定 3 张;即使某张表 0 行,也写一张只有表头的 Sheet,保证这一约定永远成立。
八、过程留痕:一份能外发的"体检报告"
Excel 不能外发,过程记录是唯一能被带出去分析的东西。它不装业务内容,只装判断依据。
每次清洗在 output/ 下生成 清洗记录_<时间戳>.md,并覆盖 清洗记录_最新.md,五段结构:
| 段 | 内容 |
|---|---|
| 一、总览 | 文件数、成功数、失败数、ERROR/WARN 数、场景、DDL 顺序、规则开关、每个文件的识别健康分 |
| 二、逐文件明细 | 每个 Sheet 的三模块识别过程:表头命中关键词及单元格坐标、字段命中率、位置偏移量;履历的表头行、匹配词、数据区、边界原因;点检项目的判定模式及依据;产出行数;表角色及判定依据 |
| 三、结构诊断 | 30×30 类型码结构线框图、行画像、列画像与数据区统计、边界细节、关键词近似匹配、健康分构成 |
| 四、问题清单 | 按严重度排列,每条带代码、文件、Sheet、说明 |
| 五、机器可读 | 同内容的 JSON,便于脚本统计"哪类识别最常失败" |
8.1 结构线框图:不泄露内容也能传递结构
r
1 2 3 4 5
1 T ← 标题行
5 T T T T T ← 履历表头行
7 ← 让它停下的空行
8 T D ← 边界之后还有数据 → 截早了
不打开原表,光看这张图就能判断"表头占几行、数据区从哪开始、空行断在哪、边界是否截早",而不会看到任何品番、设备名或项目内容。这是保密场景下一个可复用的技巧。
8.2 问题代码:把"猜错了"变成可枚举的条目
18 个代码按类别分八组:
| 类别 | 代码 |
|---|---|
| 表头 | E_M1_NO_HEADER、W_M1_LOW_HIT_RATE |
| 履历 | E_M2_NO_HEADER、W_M2_EMPTY、W_M2_POSITION_FALLBACK、I_M2_END_FALLBACK |
| 点检项目 | E_M3_NO_HEADER、W_M3_EMPTY、W_M3_FORCED_FREQ |
| 边界与一致性 | W_BOUNDARY_TOO_EARLY、W_HEADER_ROW_DRIFT、W_REPORT_NAME_MISMATCH |
| 表角色 | I_ROLE_BY_NAME、I_ROLE_BY_FIELDS、I_ROLE_BY_INDEX、W_ROLE_DUPLICATE |
| 配置与场景 | W_REPORT_TYPE_INVALID、W_SCENARIO_NAME_MISMATCH、W_TABLE_COUNT_LT3 |
| 运行与文件 | E_PARSE_EXCEPTION、E_ENGINE_EXCEPTION、E_INPUT_NOT_FOUND、E_SCHEMA_EMPTY、E_NO_SHEET、E_FILE_FAILED、E_EXPORT_FAILED |
| 质量 | W_LOW_HEALTH_SCORE、W_TEXT_KEPT_RATIO_HIGH |
8.3 健康分:先看哪个文件最可疑
健康分 = 表头字段命中率 × 40
+ 履历有产出 × 20
+ 点检项目有产出 × 25
+ 未使用位置兜底 × 15
(< 60 🔴,60~80 🟡,> 80 🟢)
多文件批量时先看 🔴 的,不必逐个打开表排查。
8.4 脱敏:白名单式,而不是按行放行
| 保留原文 | 掩码 |
|---|---|
① 词典词覆盖该格 ≥60% 字符,且该格不是"标签:值"形态(如 改订内容) |
其余全部:T(长度) / N / D / . |
② 关键词近似候选 top3(如 变更内容) |
|
| ③ 报表名单元格 | |
④ 疑似表头窄行整行保留(默认关闭) |
第 ④ 条为什么关掉,有实测依据:日常点检表的"标签行"里天然混着值,开启整行保留后,会把"详见工序一览表"(制品品番的值)、"主冷却电动水泵"(制品名的值)写进记录。结论是任何按行放行的规则都会漏,所以只做单元格级白名单。
每份记录还会给出"原文保留 N/M 格(X%)",超过 30% 就记 W_TEXT_KEPT_RATIO_HIGH,提示外发前须人工复核。上线时做过 619 条 原文可追溯复检(凡出现在记录里的原文,必须能追溯到白名单格 / 词典词 / 报表名),无法追溯的为 0 条。
九、最难的七种情况
真正卡住人的不是写代码,而是下面这些判定上的两难。逐条列出应对与代价,因为代价往往比方案更需要提前说清。
| 情况 | 卡在哪 | 应对 | 代价 |
|---|---|---|---|
| 履历区"没有"与"没认出来" | 两种情况输出同样为空表 | 空表照出,但写 E_M2_NO_HEADER / W_M2_EMPTY 与边界原因 |
结果表本身仍看不出区别,必须读记录 |
| 位置兜底误判 | 一个列名都没命中时按位置硬套 0..10 | 记 W_M2_POSITION_FALLBACK,列入人工复核 |
兜底仍会产出"看起来正确"的错误数据 |
| 表头区被当成表单 | "改订内容"右邻是"年月日",content 取到"年月日" |
先划表头区域,区域内不参与取值;右邻仍是表头词就继续右找 | 表头区域划错时错误向内传导 |
| 空行把边界截早 | 样例表自带 14~28 行全空行;删空行规则(R1)开启后履历从 2 行变 7 行 | R1 默认关闭,记 W_BOUNDARY_TOO_EARLY |
边界仍需人工确认是否截早 |
| 模糊匹配命中多个候选 | "工程№"与词典"工程名"相似度仅 0.67 | 近似候选只写进记录,不入库、不自动选 | 词表漏项仍要人工补词 |
| 表角色错位 | 按位置映射时第 2、3 张 DDL 对调 | 改语义三轮判定,并把依据写进记录 | 表名和字段都不具特征时仍需位置兜底 |
| 批量中单表异常 | 一张坏表可能拖垮整批 | 失败隔离,记 E_FILE_FAILED,其余文件继续 |
被跳过的文件需单独重跑 |
这些情况背后是同一个结论:启发式识别不会主动告诉你"我猜错了",必须自己把猜测依据记下来。
十、怎么证明它是对的
讲逻辑容易,证明逻辑没跑偏才是工程量的主要部分。这套实现用四层验证兜住。
第一层:逐单元格回归。 5 个样例表在改动前后各跑一次,把三张表的每一格(类型 + 值)快照下来逐格比对。规则改动必须做到 diff = 0,只允许出现"有意变更"的字段(例如 report_type 从 1 改成 2)。"看起来对"不算验收。
第二层:真实数据复核。 用客户实际的 10 个文件跑,修复前后对比同一张表的关键列非空计数:
| 表 | 修复前 | 修复后 |
|---|---|---|
dv_check_project_info |
2 行,content / method 全空 |
9 行,content / method / check_code / cycle_type 各 9 个非空 |
dv_check_change_record |
5 行,change_content 全空 |
2 行,change_content / change_time 各 2 个非空 |
dv_check_report.content |
年月日 |
作成:2021年07月28日 修订:2023年08... |
| 表头污染单元格 | 29 | 0 |
第三层:多种 DDL 顺序。 同一批数据分别用三种 DDL 顺序(用户真实顺序、原顺序、把表名改成 t_a/t_b/t_c 只保留字段特征)跑,三张表的数据归属必须完全一致。
第四层:端到端。 打包成 exe 后启动、拿到 HTTP 200、关窗口进程退出并释放端口;这些都要有实测输出,而不是"应该没问题"。
十一、效果与局限
这套逻辑已能覆盖日常、周度、月度、年度、条件、安全装置六类表单的常规排版:表头能定位到设备编码,履历与点检项目按各自模式拆成行,批量处理有失败隔离与安全阀,每次清洗都留下一份可外发的诊断记录。
但边界也很清楚:
- 安全装置点检表挂的是默认词典,没有真实样例,表头写法一旦不同,识别就会偏;
- 月度、年度场景未用真实月检、年检表验证 ,目前只是把
report_type下发对了; - 词表未覆盖现场所有表达,"工程№"与"工程名"相似度 0.67 就是一例;
- 条件表频度被硬编码为 1/W,业务上暂按周处理,无法从数据反推周期;
- 多 Sheet 即多报表,这是已确认的业务语义,不算缺陷。
十二、接下来怎么做得更好
改进按优先级排定,都是可以落地的具体动作,而不是"引入模型":
| 级别 | 内容 | 状态 |
|---|---|---|
| P0 | 让失败可见:自检告警、结构化诊断、静默异常落日志、空表必须带原因 | 已完成 |
| P1 | 魔法数字可配:扫描范围、偏移区间、结束词下沉到场景 JSON,边界改为"连续 N 行无数据才停" | 进行中 |
| P1 | 条件表周期不再硬编码,按配置推断周 / 月 | 待做 |
| P2 | 标准清洗能力:文件指纹去重、质量报告 Sheet、清洗血缘 manifest、契约测试 | 待做 |
| P2 | 词表扩充与近似候选闭环,把"工程№"这类漏项补回词典 | 待做 |
十三、三个可迁移的结论
如果把这件事抽象一层,有三条结论对其他"表格转数据"的场景同样成立:
- "没数据"和"没认出"必须能被区分。 这是可观测性的起点,也是所有后续优化的前提------分不清这两件事,改规则就是盲改。
- 位置兜底是最危险的设计。 它让程序"总能给出答案",包括错的答案。兜底可以有,但必须留下痕迹、必须可被人工推翻。
- 按行脱敏不可靠。 只要表里有"标签和值混排"的行,任何按行放行的规则都会漏。要脱敏就做到单元格级白名单,并给出"保留比例"这样的可度量指标。
附录 A:三张表完整字段
A.1 dv_check_report
| 字段 | 含义 | 类型 / 取值 |
|---|---|---|
check_report_id |
主键 | bigint,自增 |
report_code |
文件编号、业务唯一码 | varchar(64);格式 check + 年月日 + 序号(000001) |
report_name |
表头名称 | varchar(64) |
line_id / line_code / line_name |
产线 ID / code / 名称 | bigint(NOT NULL)/ varchar(64) / varchar(64)(NOT NULL) |
engineering_code / engineering_name |
工程 code / 名称 | varchar(64) |
machinery_code / machinery_name |
设备番号 / 设备名称 | varchar(64) |
product_no / product_name |
制品品番 / 品名 | varchar(64) |
report_type |
报表类型 | int:1 日、2 周、3 月、4 年 |
line_type |
车间树类型 | int:1 车间、2 产线、3 设备、4 工程 |
content |
报表修订内容 | varchar(255) |
recording_method |
记录方式 | varchar(300) |
object_product |
对象制品 | varchar(64) |
status |
启用状态 | char(1):1 启用、0 停用,默认 1 |
start_time / end_time |
启用开始 / 停用结束 | datetime |
chart_show |
模板显示 | bigint:1 日、2 周、3 月、4 年、5 折线 |
sort / del_flag |
排序 / 删除标志 | int / char(1):0 存在、2 删除 |
其余字段是三段签字的 ID 列(report_create_id / report_confirm_id / report_admit_id 等)与 create_by、create_time、update_by、update_time。
两点需要注意:其一,report_code 在 DDL 注释里同时被定义为文件编号,兼有"手编辑、自动生成"两种来源,既是关联键也是对外编号------本文只用它做关联,不从它反解日期。其二,chart_show 在报表侧是"模板显示类型"(含折线),在项目侧却是 0/1 开关,同名不同义,映射时不能混用。
A.2 dv_check_project_info
| 字段 | 含义 | 类型 / 取值 |
|---|---|---|
check_project_id |
主键 | bigint,自增 |
report_code |
所属报表 | varchar(255),注释为"统一表头唯一码" |
machinery_id / machinery_id_type |
关联对象 ID / 类型 | bigint / int:1 车间、2 产线、3 设备、4 工程 |
content |
点检项目 | varchar(255) |
judgment_criterion |
判定标准 | varchar(255) |
method |
点检方法 | varchar(255) |
cycle_type |
周期类型 | int:1=1/始、2=1/终、3=1/值、4=日、5=周、6=月、7=年 |
cycle |
周期 | varchar(255) |
input_type |
录入类型 | int:1 单选、2 数值、3 文本 |
option_value |
选项 / 上下限 | varchar(255),逗号分隔,格式随 input_type 变化 |
unit / length / decimals / default_value |
单位 / 长度 / 小数位 / 默认值 | varchar / int / int / varchar |
important_id / classify_id |
重点符号 / 分类(备用) | varchar(255) |
check_code |
项目自动编码 | varchar(255),EIRITEM + 日期 + 编号 |
enable |
启停 | int:1 启用、0 停用 |
need_photo / need_video(含 _num) |
是否照相 / 录像及数量 | int:0 停用、1 启用 |
file_url |
作业指导书、参考图片 | varchar(255),逗号分隔的 ID |
start_time / end_time |
生效 / 结束 | datetime |
其余字段是 sort、chart_show(0 不显示 / 1 显示)、remark、del_flag 与审计四件套。
这张表信息量最大,有三处结构需要单独说明:cycle_type 是枚举而 cycle 是文本,两者并存说明周期既要能被机器判定,也要人能读;option_value 用逗号数组承载选项或上下限,单选类型按 [key,value,1,key,value,0] 重复,数值类型则是"下限、预知下限、预知上限、上限"四组符号与数值;need_photo / need_video 说明点检不只填数字,还可能要拍照留证。这三点决定了项目行不能被当成几列普通字符串。
A.3 dv_check_change_record
| 字段 | 含义 | 类型 / 取值 |
|---|---|---|
change_record_id |
主键 | bigint,自增 |
report_code |
所属报表 | varchar(64),注释为"标识" |
change_time |
修改日期 | datetime |
change_content |
修改内容 | varchar(64) |
change_code / change_add |
修改编号 / 修改定位 | varchar(200) / varchar(64) |
change_create_id / _name / _time |
担当人及其确认时间 | bigint / varchar(64) / datetime |
change_confirm_id / _name / _time |
确认人 | 同上 |
change_admit_id / _name / _time |
承认人 | 同上 |
remark / del_flag |
备注 / 删除标志 | varchar(64) / char(1):0 存在、2 删除 |
履历与主表是同一套签字模型:change_create / change_confirm / change_admit 三组人与时间,对应主表的 report_create / report_confirm / report_admit。清洗时定为 6 个标准列,正是为了对齐这套结构:内容 → change_content、日期 → change_time、担当 → change_create_name、确认 → change_confirm_name、承认 → change_admit_name。
A.4 关联与来源
| 关系 | 连接条件 | 基数 |
|---|---|---|
| 报表 → 项目 | dv_check_project_info.report_code = dv_check_report.report_code |
1 : N |
| 报表 → 履历 | dv_check_change_record.report_code = dv_check_report.report_code |
1 : N |
| 项目 → 车间树 | machinery_id + machinery_id_type |
N : 1 |
| 报表 → 车间树 | line_id + line_type |
N : 1 |
按字段来源再分一次类:关联键 是 report_code、machinery_id / machinery_id_type、line_id,由清洗判定填入;业务填写 是表头上的人可读信息(表头名称、制品品番、修订内容、记录方式)与项目内容(项目、判定标准、方法、周期、单位),来自表内文字;系统生成 是三个自增主键、check_code、del_flag、status / enable 的默认值以及审计四件套,入库时赋值;签字是两侧的 create / confirm / admit 三组,来自表内签字栏。
附录 B:术语表
| 术语 | 含义 |
|---|---|
| 三大模块 | 表头元数据 / 修改履历 / 点检项目 |
| 表角色 | 某个 DDL 表在这次清洗里承担哪种数据(report / change_record / project_info) |
| 场景 | 一套可覆盖默认配置的参数 + 一个 report_type |
| 健康分 | 0~100 的单文件识别质量评分 |
| 过程记录 | 每次清洗输出的诊断 Markdown,可外发 |
| 位置兜底 | 列名一个都没命中时,按列位置硬套字段 |
| 契约测试 | 用固定样例的快照锁住行为,防止规则改动悄悄改变结果 |