docs: MIXED 完整段落解析设计(遗留项#1)

This commit is contained in:
lhl
2026-08-09 03:45:08 +08:00
parent 81407afb5a
commit d01c7d4901
@@ -0,0 +1,155 @@
# 里程碑 2.5:MIXED 完整段落解析 设计
- 日期:2026-08-09
- 状态:已批准(用户 2026-08-09 确认)
- 目的:闭环遗留项 #1「MIXED 分段解析」,落地 design §3.5.2「段落分割 → 各段落最优解析」
## 1. 背景与触发条件
当前 `classify_sheet` 判定 MIXED 的条件:**存在表头行,且表头行之后有以「・」「■」开头的碎片行**(`sheet_nature.py:36-43`)。此前实现把 MIXED 折叠进纯表格路径(`extract_table(header_row=0)`),会把片段行误当数据行。
现有真实样本(新規開発 / 追加改修 / 自由記述)中无任何 sheet 触发 MIXED(改修ポイント为独立 sheet)。因此**新增混合型样本** `samples/要件定義_混合型.xlsx` 用于端到端验证。
## 2. 目标
- 落地 design §3.5.2MIXED sheet → 段落分割 → 每段独立判定并选择最优解析
- 显式表达段落边界与段落类型(data_models 扩展,用户选定)
- 不改变既有 TABLE / FREE_TEXT 路径行为(回归保持)
## 3. 设计
### 3.1 段落分割(新模块 `src/genesis/parsers/paragraph_splitter.py`
```python
def split_paragraphs(matrix: list[list[Any]]) -> list[tuple[int, int]]:
"""以空行为界做通用段落分割;返回 (start_row, end_row) 列表(含端)。"""
```
规则:
- 全空行(空矩阵行)为段落分界
- 连续非空段落保持为一个段落
- 尾部空行截断,不产生空段
- 空矩阵 → `[]`
### 3.2 每段最优解析(复用现有能力,不改 sheet_nature / table_extractor
对每个段落矩阵 `seg`(**注意:合并单元格在装配时对整 sheet 先 forward_fill 再按段切片,见 3.4**):
- `classify_sheet(seg)` 判定段落性质
- `TABLE``forward_fill(seg, merged)` + `extract_table(...)``ExcelTable`
- `FREE_TEXT``extract_text_blocks(seg)` + `build_free_text_table(...)``ExcelTable`
- (段内二次出现的 MIXED 视为 FREE_TEXT 处理,防无限递归——段落粒度的 MIXED 罕见)
### 3.3 data_models 扩展(用户选定「扩展 data_models」)
```python
@dataclass
class MixedParagraph:
"""混合 sheet 的一个段落(表格或自由文本)"""
kind: Literal["table", "free_text"]
matrix: list[list[Any]] | None = None # 该段原始矩阵(便于调试/重现/后续增量)
table: ExcelTable | None = None # kind="table" 时填充
text: str | None = None # kind="free_text" 时填充(该段拼接文本)
source_range: tuple[int, int] | None = None # (first_row, last_row) 矩阵 0-based
@dataclass
class MixedSheet:
"""混合 sheet 的段落集合"""
name: str
paragraphs: list[MixedParagraph]
```
`ExcelParseResult`(现有 dataclass,位于 `src/genesis/parsers/excel_parser.py`)增加字段:
```python
mixed: list[MixedSheet] = field(default_factory=list)
```
- `tables` 仍保留(含 MIXED sheet 内的表格段),下游消费者继续用 `tables` 取表
- `mixed` 显式表达段落边界与类型,不丢信息
> 说明:`MixedParagraph.text` 为自由文本段全文(按段拼接),与 `build_free_text_table` 的逐块行并存,供下游两种消费方式。
### 3.4 装配流程(`excel_parser.parse`
```
for ws in workbook.worksheets:
matrix = sheet_matrix(ws)
if empty → skipped
nature = classify_sheet(matrix)
if nature == FREE_TEXT: (现有路径,保持不变)
elif nature == MIXED:
segments = split_paragraphs(matrix)
mixed_sheet = MixedSheet(name=ws.title, paragraphs=[])
for (s, e) in segments:
seg = matrix[s:e+1]
kind = classify_sheet(seg)
if kind == TABLE:
merged = to_tuples(ws.merged_cells.ranges)
filled = forward_fill(seg, merged) if merged else seg
table = extract_table(ws.title, filled, file_name, detected_type)
result.tables.append(table)
mixed_sheet.paragraphs.append(MixedParagraph(kind="table", matrix=seg, table=table, source_range=(s, e)))
else:
blocks = extract_text_blocks(seg)
table = build_free_text_table(ws.title, blocks, file_name, detected_type)
result.tables.append(table)
mixed_sheet.paragraphs.append(MixedParagraph(
kind="free_text", matrix=seg, text="\n".join(blocks), source_range=(s, e),
))
result.mixed.append(mixed_sheet)
result.comments.extend(collect_comments(ws, file_name))
else: 现有 TABLE 路径
```
合并单元格:段内矩阵切片后 `forward_fill` 需要该段内的 merged 范围——实现时对整段做前向填充再切片,或对段内范围坐标平移。**实现时选择对整 sheet 先 forward_fill 再按段切片**(避免坐标换算错误;混合 sheet 中合并集中于表格段)。
- 段内 `extract_table` 使用 `header_row=0`(段的表头在段首行)
### 3.5 样本与验证
- 新增 `samples/要件定義_混合型.xlsx`,Sheet 构成(目标是触发 MIXED 判定):
- `機能一覧`:表格段(表头 + 真实数据,前无标题行,表头后含一条 ・/■ 碎片行即 MIXED 触发)
- 内容规格:表头 `機能ID / 機能名 / 画面ID`,数据 3 行(F101-F103),表头之下全部跟随「・」碎片行若干(碎片文本描述)
- `tests/test_real_samples.py` 新增用例:混合样本 → `result.mixed` 非空、段落 kind 集合={table, free_text}、表格段无碎片污染(首数据行機能ID == 期望)
### 3.6 单元测试
- `tests/test_paragraph_splitter.py`
- 空矩阵 → []
- 无空行 → 单段(整矩阵)
- 中间空行 → 分段;连续非空段
- 前端空行 → 首个非空段从第一个非空行开始;尾端空行 → 无空段
- 段落区间整数正确
- `tests/test_sheet_nature.py` 补 MIXED 正向用例(当前无):
- 表头行后存在「・」开头行 → MIXED
- 表头行后存在「■」开头行碎片 → MIXED
- 表头行后无碎片 → TABLE(已覆盖)
- `tests/test_excel_parser.py` 补 MIXED 装配用例:
- 构造混合矩阵(ftw 单测,不必落盘):表格段 + 碎片段 → `result.mixed` 长度 1、段落 2 个、`tables` 含表格段的表与自由文本表
### 3.7 兼容性
- `data_models.py` 扩展为**追加**(新增 dataclass 与字段),不修改既有字段
- `ExcelParseResult` 新增字段带默认值 → 既有消费(tests、下游)不受破坏
- TABLE / FREE_TEXT 路径代码不动
## 4. 验收标准
1. `classify_sheet` 对混合矩阵判 MIXED(含单测)
2. `split_paragraphs` 空行分段正确(含边界单测)
3. MIXED sheet 装配后 `result.mixed[name]` 段落集合包含 table 与 free_text,且 `tables` 无碎片污染
4. 新样本 `samples/要件定義_混合型.xlsx` 端到端通过
5. 全量回归 `python -m pytest` 全绿
## 5. 非目标(显式延后)
- 公式双保留(§3.5.7)、多级表头扁平化、隐藏行列/密码:仍延后至后续里程碑
- 段内二次 MIXED:段落内不再触发 MIXED 判定(递归防御)
- Impact 对段落数据的消费:下一里程碑契约
## 6. 涉及文件
- 修改:`src/genesis/data_models.py`(追加 MixedParagraph / MixedSheetExcelParseResult 加字段)
- 新建:`src/genesis/parsers/paragraph_splitter.py`
- 修改:`src/genesis/parsers/excel_parser.py`MIXED 装配分支)
- 新建:`tests/test_paragraph_splitter.py`
- 修改:`tests/test_sheet_nature.py`(补 MIXED 用例)、`tests/test_excel_parser.py`(补 MIXED 装配用例)、`tests/test_real_samples.py`(新增样本用例)
- 新增:`samples/要件定義_混合型.xlsx`