# 06 - 比对模块详细设计 ## 1. 模块概述 比对模块(`comparator/`)是 COBOL 迁移验证平台 V3 的核心验证引擎,负责将 COBOL 原始输出与 Java(Spark)迁移后输出进行逐字段对比,判断迁移正确性。 模块提供五项核心能力: 1. **记录对齐**(aligner):按主键将 COBOL 记录集与 Java 记录集配对 2. **二进制读取**(cobol_binary_reader):解析 COBOL 二进制输出文件为字典 3. **数据标准化**(normalizer):处理 EBCDIC 编码、COMP-3 压缩十进制、日期格式 4. **字段比对**(field_compare):按类型(数值/日期/字符串)进行字段级比较 5. **舍入检测**(rounding_detect):判断数值差异是否由 COBOL ROUNDED 子句引起 ## 2. 文件清单 | 文件 | 职责 | 依赖 | |------|------|------| | `__init__.py` | 包入口,公开 API 导出 | 所有子模块 | | `aligner.py` | COBOL 与 Java 记录对齐 | 无 | | `cobol_binary_reader.py` | 二进制 COBOL 输出解析 | `data.field_tree` | | `field_compare.py` | 字段级比较(decimal/string/date) | `data.diff_result` | | `normalizer.py` | COMP-3/EBCDIC 解码、IR 记录构造 | 无 | | `rounding_detect.py` | 舍入差异检测 | `decimal` | ## 3. 记录对齐算法(aligner.py) ### 3.1 职责 将 COBOL 输出记录集和 Java 输出记录集按主键字段进行配对,输出对齐的记录对列表及匹配状态。 ### 3.2 函数签名 ```python def align_records( cobol_records: list[dict], java_records: list[dict], key_field: str = "CUST-ID" ) -> list[tuple] ``` ### 3.3 算法描述 1. **分组**:分别按 `key_field` 对 COBOL 和 Java 记录建立 `{key: [records]}` 索引 2. **合并键集**:取两侧键的并集,按字符串排序 3. **逐键配对**:对每个键,取两侧记录列表的对应位置进行配对 4. **状态标记**: | 状态 | 含义 | |------|------| | `MATCHED` | 两侧均有记录,成功配对 | | `MISSING_IN_SPARK` | COBOL 有记录,Java 侧缺失 | | `EXTRA_IN_SPARK` | Java 有记录,COBOL 侧缺失 | ### 3.4 返回值 ```python list[tuple[dict | None, dict | None, str]] # (cobol_record, java_record, status) ``` ### 3.5 设计要点 - 使用 `setdefault` 聚合同键多记录,支持一对多场景 - 排序保证输出顺序稳定 - 键值转为字符串处理,兼容数值键和字符串键 - 空记录集直接返回空列表 ## 4. 二进制读取器(cobol_binary_reader.py) ### 4.1 职责 读取 COBOL 程序生成的二进制输出文件,根据 `FieldTree` 定义的字段布局逐记录解析为字典。 ### 4.2 核心类 ```python class CobolBinaryReader: def read(self, path: str, tree: FieldTree) -> list[dict] def _record_size(self, tree: FieldTree) -> int def _parse(self, record_bytes: bytes, tree: FieldTree) -> dict def _comp3(self, raw: bytes, signed: bool, decimal: int) -> str ``` ### 4.3 记录大小计算 ```python def _record_size(self, tree): return max((f.offset + f.length for f in tree.fields), default=0) ``` 取所有顶层字段的最大 `offset + length` 作为记录大小。 ### 4.4 字段解析策略 | USAGE 类型 | 解析方式 | |-----------|----------| | `COMP-3` | 半字节解码 + 符号位处理 + 小数点定位 | | `COMP` / `COMP-5` | `int.from_bytes(raw, "big", signed=...)` | | 其他(DISPLAY 等) | ASCII 解码 + 去尾空格 | ### 4.5 COMP-3 解码算法 ``` 输入: N 字节压缩十进制数据 1. 将每字节拆为两个半字节(高4位 + 低4位) 2. 弹出最后一个半字节作为符号位 3. 逐半字节累加为数值(权值 = 10^(len-1-i)) 4. 符号位 0xD/0xB -> 取反 5. 按 decimal 定位小数点 输出: 字符串形式数值(如 "1234.56") ``` ### 4.6 错误处理 - 文件为空或记录大小为 0 -> 返回空列表 - 记录不完整(字节数 < record_size)-> 跳过该记录 - COMP-3 空数据 -> 返回 "0" - 非 ASCII 字符 -> `errors="replace"` 替换为 `?` ## 5. 数据标准化器(normalizer.py) ### 5.1 职责 提供 COBOL 数据到中间表示(IR)的标准化转换,包括 EBCDIC 解码、COMP-3 解码、日期格式统一、IR 记录构造。 ### 5.2 核心类 ```python class Normalizer: def normalize_encoding(self, raw: bytes, encoding: str) -> str def normalize_comp3(self, raw: bytes) -> str def normalize_date(self, s: str) -> str def to_ir_record(self, name, hex_, val, enc, ft, length=0, scale=0, signed=False) -> IRRecord def to_null_ir(self, name: str, side: str = "java") -> IRRecord ``` ### 5.3 EBCDIC 编码转换 内置 EBCDIC 037 代码页映射表(`EBCDIC_037`),覆盖: - 空格、标点、运算符 - 小写 a-z(0x81-0xA9) - 大写 A-Z(0xC1-0xE9) - 数字 0-9(0xF0-0xF9) 不可映射字符处理: - 可打印 ASCII(32-126)-> 保留原字符 - 其他 -> 替换为 `?` ### 5.4 COMP-3 标准化 与 `CobolBinaryReader._comp3` 算法一致,但返回整数字符串(无小数点处理),适用于无需小数定位的场景。 ### 5.5 日期标准化 - 8 位纯数字字符串(如 `20260822`)-> 格式化为 `YYYY-MM-DD` - 其他格式 -> 原样返回 ### 5.6 中间表示(IR)模型 ```python @dataclass class CobolIRField: raw_hex: str # 原始十六进制 decoded_value: str # 解码后值 encoding: str # 编码类型(EBCDIC/ASCII) field_type: str # 字段类型 length: int # 字节长度 scale: int # 小数位数 signed: bool # 是否带符号 @dataclass class JavaIRField: raw_value: str # 原始值 decoded_value: str # 解码后值 field_type: str # 字段类型 nullable: bool # 是否可空 @dataclass class IRRecord: field_name: str cobol: CobolIRField | None java: JavaIRField | None ``` ### 5.7 空值 IR 构造 `to_null_ir` 为缺失侧构造空 IR 记录(`JavaIRField("", "", "null", True)`),用于只有一侧有数据的场景。 ## 6. 字段比对算法(field_compare.py) ### 6.1 职责 对单个字段的 COBOL 值和 Java 值进行类型感知的比较,返回结构化的比对结果。 ### 6.2 函数签名 ```python def compare_field( name: str, c: str, # COBOL 值 j: str, # Java 值 field_type: str = "decimal", # 字段类型 tolerance: float = 0.01 # 容忍度 ) -> FieldResult ``` ### 6.3 比对策略 | 字段类型 | 比对方式 | 说明 | |----------|----------|------| | `decimal` / `numeric` | 数值差 <= tolerance -> TOLERATED | 使用 `Decimal` 精确计算 | | `date` | 8 位数字 -> `YYYY-MM-DD` 格式化后比较 | 统一格式消除表示差异 | | `string` | strip 后直接比较 | 去除首尾空白 | | 其他 | 字符串直接比较 | 兜底策略 | ### 6.4 状态判定 | 状态 | 条件 | |------|------| | `PASS` | 完全一致 | | `TOLERATED` | 数值差在容忍度范围内 | | `MISMATCH` | 不一致 | | `NOT_SET` | 两侧均为空/None | ### 6.5 数值解析(_num) ```python def _num(v) -> Decimal | None ``` - `None` / `"None"` -> `None` - 空字符串 -> `Decimal("0")` - 含 `\x00` -> 去除后解析 - 非数字字符串 -> `None` 使用 Python `Decimal` 进行精确十进制运算,避免浮点精度问题。 ### 6.6 默认容忍度 ```python DEFAULT_TOLERANCE = 0.01 ``` 可通过 `Config.tolerance` 全局配置调整。 ## 7. 舍入检测(rounding_detect.py) ### 7.1 职责 判断两个数值之间的差异是否由 COBOL `ROUNDED` 子句引起,识别舍入模式并给出置信度。 ### 7.2 函数签名 ```python def detect_rounding(c: str, j: str) -> RoundingResult ``` ### 7.3 RoundingResult 数据类 ```python @dataclass class RoundingResult: mode: str = "EXACT" # 舍入模式 confidence: float = 1.0 # 置信度(0-1) suggestion: str = "" # 建议文本 ``` ### 7.4 检测算法 ``` 1. 解析 COBOL 值 (cv) 和 Java 值 (jv) 为 Decimal 2. 任一解析失败 -> UNKNOWN (confidence=0) 3. cv == jv -> EXACT (confidence=1.0) 4. 计算绝对差 diff = |cv - jv| 5. 判定模式: - diff < 2 -> TRUNCATE (confidence=0.6) - diff < 100 -> ROUNDING (confidence=0.4) - diff >= 100 -> SIGNIFICANT (confidence=0.9) ``` ### 7.5 舍入模式说明 | 模式 | 含义 | 置信度 | 建议 | |------|------|--------|------| | `EXACT` | 完全一致 | 1.0 | 无 | | `TRUNCATE` | 截断差异(diff < 2) | 0.6 | 可能是 COBOL 截断行为 | | `ROUNDING` | 舍入差异(diff < 100) | 0.4 | 可能存在舍入方式差异 | | `SIGNIFICANT` | 显著差异(diff >= 100) | 0.9 | 需要关注的较大差异 | ### 7.6 数值解析(_d) ```python def _d(v) -> Decimal | None ``` - 使用 `Decimal(str(v).strip())` 解析 - 异常时返回 `None` ## 8. 接口定义 ### 8.1 模块公开 API ```python # comparator/__init__.py align_records(cobol_records, java_records, key_field) -> list[tuple] compare_field(name, c, j, field_type, tolerance) -> FieldResult CobolBinaryReader # class Normalizer # class detect_rounding(c, j) -> RoundingResult ``` ### 8.2 典型调用流程 ``` cobol_binary_reader.read(path, field_tree) -> list[dict] | v align_records(cobol_records, java_records, key_field) -> list[tuple] | v (对每个 MATCHED 对) compare_field(name, cobol_val, java_val, field_type, tolerance) -> FieldResult | v (可选) detect_rounding(cobol_val, java_val) -> RoundingResult ``` ### 8.3 数据模型依赖 | 模型 | 定义位置 | 用途 | |------|----------|------| | `FieldTree` / `Field` | `data.field_tree` | 字段布局定义 | | `FieldResult` | `data.diff_result` | 字段比对结果 | | `IRRecord` / `CobolIRField` / `JavaIRField` | `comparator.normalizer` | 中间表示 | | `RoundingResult` | `comparator.rounding_detect` | 舍入检测结果 | ## 9. 错误处理 ### 9.1 错误策略 模块采用**严格解析 + 宽容比对**策略: | 异常场景 | 处理方式 | 影响 | |----------|----------|------| | 二进制文件为空 | 返回空列表 | 无记录可比 | | 记录不完整 | 跳过该记录 | 部分数据丢失 | | COMP-3 空数据 | 返回 "0" | 默认零值 | | EBCDIC 未知字符 | 替换为 `?` | 可能导致 MISMATCH | | 数值解析失败 | 返回 `None` | NOT_SET 状态 | | 两侧均无值 | NOT_SET | 不计入匹配统计 | ### 9.2 已知局限 1. `aligner.py` 仅支持单一主键,不支持复合主键 2. `CobolBinaryReader` 假定所有记录等长,不支持变长记录 3. `Normalizer.normalize_comp3` 不处理小数位(与 `CobolBinaryReader._comp3` 重复实现) 4. `detect_rounding` 的阈值(2/100)为经验值,未基于统计分析 5. `field_compare` 的字符串比较未处理 EBCDIC 与 ASCII 的编码差异