359 lines
11 KiB
Markdown
359 lines
11 KiB
Markdown
# 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 的编码差异
|