diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 816f6f5..3bcd637 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -31,3 +31,4 @@ | 2026-08-09 03:13 | Agent 实现 | 里程碑2 Task7 实现:ExcelParser 编排器(集成全部解析模块)。创建 src/genesis/parsers/excel_parser.py(ExcelParseResult dataclass:file_name/tables/comments/skipped;ExcelParser.parse 编排:open_workbook→逐 sheet sheet_matrix→空则跳过→detect_sheet_type→classify_sheet→FREE_TEXT 走 extract_text_blocks+build_free_text_table(带 detected_type)→否则 merged ranges 转 tuple 后 forward_fill+extract_table→collect_comments 汇总)与 tests/test_excel_parser.py(2 个用例:表型 sheet 两个(機能一覧 FUNCTION / バッチ一覧 BATCH 类型+行数断言)、自由记述 sheet extraction_method="llm_from_free_text");TDD 验证 RED(ImportError: No module named 'genesis.parsers.excel_parser')→ GREEN(2 passed);pytest 全量 36 passed;data_models.py 未改动;对照 dispatch 上下文说明:data_models 无 ParsedWorkbook 命名,按 brief 定义 ExcelParseResult | src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free | | 2026-08-09 03:20 | 测试验证 | 里程碑2 Task8 实现:真实样本集成测试。创建 tests/test_real_samples.py(3 个端到端用例,各样本缺失时 pytest.skip:新規開発→機能一覧 存在且 int rows+DB定義;追加改修→tables 非空且至少一表有数据行;自由記述→存在 extraction_method="llm_from_free_text" 的表);全量验证 39 passed(含 3 个样本用例);真实样本产物: 新規開発 6 表全 openpyxl(機能一覧9/画面一覧7/帳票一覧5/DB定義20/IF定義3/バッチ一覧3); 追加改修 2 表 openpyxl(機能10/画面7)+ 改修ポイント 以 FREE_TEXT 分类改 1 行(MIXED 折叠落观察已确认); 自由記述 3 表全 llm_from_free_text 各 1 行; 3 样本均 0 批注、无空 sheet。调研结论:MIXED 已知缺口#1 在真实样本未触发断言失败(无断言触发),按 brief 保持弱断言 | tests/test_real_samples.py | deepseek-v4-flash-free | +| 2026-08-09 03:30 | 整体迭代 | 里程碑2 最终评审修正(must-fix x2):① find_header_row 结果未传入 extract_table(恒 header_row=0,标题行置上的 sheet 会损坏表头)→ excel_parser.py 结构化路径改用 find_header_row(filled) 并 <0 回落 0;② cell_formatting 死代码(CellValue.formatting 恒 None,取消线/背景色到不了下游)→ extract_table 新增可选 formatting_map 参数,excel_parser 遍历 ws.iter_rows 构建 fmt_map 传入。tests/test_excel_parser.py 新增 2 用例(标题行表头定位、strikethrough 回填);pytest 全量 41 passed | src/genesis/parsers/excel_parser.py, src/genesis/parsers/table_extractor.py, tests/test_excel_parser.py | deepseek-v4-flash-free | diff --git a/src/genesis/parsers/excel_parser.py b/src/genesis/parsers/excel_parser.py index 95d93ca..db6bccb 100644 --- a/src/genesis/parsers/excel_parser.py +++ b/src/genesis/parsers/excel_parser.py @@ -6,10 +6,10 @@ from pathlib import Path from genesis.data_models import CellComment, ExcelTable from genesis.parsers.excel_reader import open_workbook, sheet_matrix from genesis.parsers.sheet_detector import detect_sheet_type -from genesis.parsers.sheet_nature import SheetNature, classify_sheet +from genesis.parsers.sheet_nature import SheetNature, classify_sheet, find_header_row from genesis.parsers.merge_fill import forward_fill from genesis.parsers.table_extractor import extract_table -from genesis.parsers.formatting_detector import collect_comments +from genesis.parsers.formatting_detector import cell_formatting, collect_comments from genesis.parsers.free_text_extractor import build_free_text_table, extract_text_blocks @@ -46,6 +46,20 @@ class ExcelParser: for r in ws.merged_cells.ranges ] filled = forward_fill(matrix, merged) if merged else matrix - result.tables.append(extract_table(ws.title, filled, file_name, detected_type)) + header_row = find_header_row(filled) + if header_row < 0: + header_row = 0 + fmt_map = {} + for row in ws.iter_rows(): + for cell in row: + fmt = cell_formatting(cell) + if fmt is not None: + fmt_map[(cell.row - 1, cell.column - 1)] = fmt + result.tables.append( + extract_table( + ws.title, filled, file_name, detected_type, + header_row=header_row, formatting_map=fmt_map, + ) + ) result.comments.extend(collect_comments(ws, file_name)) return result \ No newline at end of file diff --git a/src/genesis/parsers/table_extractor.py b/src/genesis/parsers/table_extractor.py index 805e140..6ce90e2 100644 --- a/src/genesis/parsers/table_extractor.py +++ b/src/genesis/parsers/table_extractor.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any -from genesis.data_models import CellValue, ExcelTable, Provenance, SheetType +from genesis.data_models import CellFormatting, CellValue, ExcelTable, Provenance, SheetType def column_letter(index: int) -> str: @@ -20,13 +20,18 @@ def extract_table( file_name: str, detected_type: SheetType, header_row: int = 0, + formatting_map: dict[tuple[int, int], CellFormatting] | None = None, ) -> ExcelTable: - """从矩阵提取表格:首行视为表头,其后为数据行。""" + """从矩阵提取表格:首行视为表头,其后为数据行。 + + formatting_map:矩阵坐标 (row, col)(0 起)→ CellFormatting,用于还原取消线/背景色等样式。 + """ if not matrix: return ExcelTable( name=sheet_name, detected_type=detected_type, extraction_method="openpyxl", headers=[], rows=[], ) + fmt_map = formatting_map or {} headers = [str(c) if c is not None else "" for c in matrix[header_row]] rows = [] for r in range(header_row + 1, len(matrix)): @@ -42,6 +47,7 @@ def extract_table( column=column_letter(c + 1), column_header=h, ), + formatting=fmt_map.get((r, c)), ) rows.append(row_dict) return ExcelTable( diff --git a/tests/test_excel_parser.py b/tests/test_excel_parser.py index ebd272e..b8e253e 100644 --- a/tests/test_excel_parser.py +++ b/tests/test_excel_parser.py @@ -24,3 +24,36 @@ def test_parse_free_text_sheet(tmp_path): result = ExcelParser().parse(path) assert len(result.tables) == 1 assert result.tables[0].extraction_method == "llm_from_free_text" + + +def test_parse_uses_detected_header_row(tmp_path): + # 标题行(单格)在首行,真实表头在第二行 + wb = new_workbook({ + "機能一覧": [["機能一覧"], ["機能ID", "機能名"], ["A001", "社員登録"]], + }) + path = save_workbook(tmp_path, wb) + result = ExcelParser().parse(path) + t = result.tables[0] + assert t.headers == ["機能ID", "機能名"] + assert len(t.rows) == 1 + assert t.rows[0]["機能ID"].value == "A001" + + +def test_parse_attaches_strikethrough_formatting(tmp_path): + from copy import copy + from openpyxl import Workbook + wb = Workbook() + ws = wb.active + ws.title = "機能一覧" + ws["A1"] = "機能ID" + ws["B1"] = "機能名" + ws["A2"] = "A001" + ws["B2"] = "社員登録" + font = copy(ws["A2"].font) + font.strike = True + ws["A2"].font = font + path = save_workbook(tmp_path, wb) + result = ExcelParser().parse(path) + cv = result.tables[0].rows[0]["機能ID"] + assert cv.formatting is not None + assert cv.formatting.strikethrough is True