# 里程碑 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.2:MIXED 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 / MixedSheet;ExcelParseResult 加字段) - 新建:`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`