From d93cc77bae07b2fcf37a62ad415e731a9ca05e39 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 9 Aug 2026 04:18:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=81=97=E7=95=99=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=88=E6=9E=9A=E4=B8=BE=E5=90=8C=E6=BA=90/=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E9=98=B2=E5=BE=A1/=E6=96=AD=E8=A8=80=E8=A1=A5?= =?UTF-8?q?=E5=BC=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _AI_USAGE_LOG.md | 1 + src/genesis/data_models.py | 2 +- src/genesis/parsers/free_text_extractor.py | 4 ++-- src/genesis/parsers/merge_fill.py | 5 ++++- src/genesis/parsers/table_extractor.py | 10 +++++++--- tests/test_formatting_detector.py | 13 +++++++++++++ tests/test_free_text_extractor.py | 7 ++++++- tests/test_sheet_nature.py | 12 +++++++++++- tests/test_table_extractor.py | 16 ++++++++++++++-- 9 files changed, 59 insertions(+), 11 deletions(-) diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 0284886..a23a331 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -36,3 +36,4 @@ | 2026-08-09 04:20 | Agent 实现 | 里程碑2.5 Task10 实现:data_models 扩展 MixedParagraph/MixedSheet 段落容器,并新增 ExcelParseResult.mixed 字段(默认空列表)。TDD:先追加 3 个测试(字段存在性与默认行为)确认 RED(ImportError: cannot import name 'MixedParagraph'),再在 data_models.py 末尾追加两个 dataclass(顶部 from typing 加 Literal、dataclasses 加 field),excel_parser.py 更新导入并为 ExcelParseResult 加 mixed 字段,确认 GREEN(3 passed),全量 pytest 49 passed(46 baseline + 3)。未修改既有类与字段,仅追加。 | src/genesis/data_models.py, src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free | | 2026-08-09 04:06 | Agent 实现 | 里程碑2.5 Task11 实现:MIXED 完整段落装配(分割→逐段最优解析)。excel_parser.py 新增 MIXED 分支:整表先 forward_fill(合并单元格)再按段切片;对每段 classify_sheet,TABLE 段用 find_header_row(seg)(段内相对,<0 回落 0)并抽出段内子 map seg_fmt_map(物理行 pr→段内 pr-s,修复 formatting_map 坐标错位)传给 extract_table(header_row=header_row);非表格段归 FREE_TEXT(防二次 MIXED 递归),走 extract_text_blocks + build_free_text_table 构造 kind="free_text"。MixedParagraph 段落追加到 MixedSheet 并进 result.mixed,表/自由文本表进 result.tables。原 else 分支保留为 TABLE 专用,既有 TABLE/FREE_TEXT 路径不变。TDD:先追加 2 测试(段落分割/碎片捕获、表格段不在物理行 0 时的取消线回填)确认 RED(result.mixed 空 + IndexError),再实现确认 GREEN(9 passed),全量 pytest 51 passed(49 baseline + 2)。提交见 git log | src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free | | 2026-08-09 09:40 | 测试验证 | 里程碑2.5 Task12 实现:混合型样本 + 端到端验证。生成 samples/要件定義_混合型.xlsx(機能一覧 表格:表头+3 数据行,空行后接「・改修ポイント」「■対象期間」两条连续碎片文本,无空行);tests/test_real_samples.py 追加 test_mixed_sample_segments_detected(断言 mixed 段落数==2、kinds==["table","free_text"]、表格段第一批数据行機能ID=="F101"、table.rows==3、碎片段 text 含 改修ポイント/対象期間)。验证:real_samples 全绿 4 passed(3 baseline+1 新增,无 skip);pytest 全量 52 passed(51 baseline + 1);实际样本 split_paragraphs 产出两段(0,3)+(5,6)(表格段+碎片段)。提交见 git log | samples/要件定義_混合型.xlsx, tests/test_real_samples.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | +| 2026-08-09 | 整体迭代 | 里程碑2.5 Task13 遗留清理:① extraction_method 枚举同源(free_text_extractor→ExtractionMethod.LLM_FROM_FREE_TEXT.value、table_extractor 空/非空两分支→ExtractionMethod.OPENPYXL.value,值不变既有用例兼容);② table_extractor header_row 越界/负值防御(<0 或 >=len(matrix) 回落 0,keep if not matrix 早返回);③ merge_fill.forward_fill 完整替换(min_row<1/min_col<1 非法值跳过 + 越界防御);④ data_models.Provenance.row 语义注释;⑤ 补测试 6 个(sheet_nature MIXED ・/■ 正向 x2、formatting_detector 字体色正向、table_extractor 枚举锁 + forward_fill 防御、free_text_extractor 枚举锁)。TDD:先加测试确认 RED(仅 forward_fill 防御用例失败,其余为已通过回归锁)→ 实现 → GREEN;pytest 全量 58 passed(52 baseline + 6) | src/genesis/parsers/free_text_extractor.py, src/genesis/parsers/table_extractor.py, src/genesis/parsers/merge_fill.py, src/genesis/data_models.py, tests/test_sheet_nature.py, tests/test_formatting_detector.py, tests/test_free_text_extractor.py, tests/test_table_extractor.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | diff --git a/src/genesis/data_models.py b/src/genesis/data_models.py index 7b5d024..6033767 100644 --- a/src/genesis/data_models.py +++ b/src/genesis/data_models.py @@ -54,7 +54,7 @@ class ExtractionMethod(Enum): class Provenance: file_name: str sheet_name: str - row: int + row: int # 数据行号(从 1 起:表格为物理行-表头行;自由文本为块序) column: str column_header: str diff --git a/src/genesis/parsers/free_text_extractor.py b/src/genesis/parsers/free_text_extractor.py index 4b3eb90..341a95d 100644 --- a/src/genesis/parsers/free_text_extractor.py +++ b/src/genesis/parsers/free_text_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 CellValue, ExcelTable, ExtractionMethod, Provenance, SheetType def extract_text_blocks(matrix: list[list[Any]]) -> list[str]: @@ -45,7 +45,7 @@ def build_free_text_table( return ExcelTable( name=sheet_name, detected_type=detected_type, - extraction_method="llm_from_free_text", + extraction_method=ExtractionMethod.LLM_FROM_FREE_TEXT.value, headers=["text"], rows=rows, ) \ No newline at end of file diff --git a/src/genesis/parsers/merge_fill.py b/src/genesis/parsers/merge_fill.py index 377e71a..c137324 100644 --- a/src/genesis/parsers/merge_fill.py +++ b/src/genesis/parsers/merge_fill.py @@ -10,7 +10,10 @@ def forward_fill( """合并单元格:用左上角主格值填充范围内全部单元格。""" out = [list(row) for row in matrix] for (min_row, min_col, max_row, max_col) in merged_ranges: - if not out or min_row > len(out) or min_col > len(out[min_row - 1]): + # 防御:合并范围 1-based,非法(<1)或越界时跳过该范围 + if not out or min_row < 1 or min_col < 1: + continue + if min_row > len(out) or min_col > len(out[min_row - 1]): continue main_value = out[min_row - 1][min_col - 1] for r in range(min_row, min(max_row, len(out)) + 1): diff --git a/src/genesis/parsers/table_extractor.py b/src/genesis/parsers/table_extractor.py index 6ce90e2..38f2ba1 100644 --- a/src/genesis/parsers/table_extractor.py +++ b/src/genesis/parsers/table_extractor.py @@ -2,7 +2,9 @@ from __future__ import annotations from typing import Any -from genesis.data_models import CellFormatting, CellValue, ExcelTable, Provenance, SheetType +from genesis.data_models import ( + CellFormatting, CellValue, ExcelTable, ExtractionMethod, Provenance, SheetType, +) def column_letter(index: int) -> str: @@ -29,8 +31,10 @@ def extract_table( if not matrix: return ExcelTable( name=sheet_name, detected_type=detected_type, - extraction_method="openpyxl", headers=[], rows=[], + extraction_method=ExtractionMethod.OPENPYXL.value, headers=[], rows=[], ) + if header_row < 0 or header_row >= len(matrix): + header_row = 0 fmt_map = formatting_map or {} headers = [str(c) if c is not None else "" for c in matrix[header_row]] rows = [] @@ -52,5 +56,5 @@ def extract_table( rows.append(row_dict) return ExcelTable( name=sheet_name, detected_type=detected_type, - extraction_method="openpyxl", headers=headers, rows=rows, + extraction_method=ExtractionMethod.OPENPYXL.value, headers=headers, rows=rows, ) \ No newline at end of file diff --git a/tests/test_formatting_detector.py b/tests/test_formatting_detector.py index 402d83d..5a214c7 100644 --- a/tests/test_formatting_detector.py +++ b/tests/test_formatting_detector.py @@ -35,6 +35,19 @@ def test_cell_formatting_none_when_plain(): assert cell_formatting(ws["A1"]) is None +def test_cell_formatting_detects_font_color(): + from openpyxl.styles import Font + wb = Workbook() + ws = wb.active + ws["A1"] = "x" + f = copy(ws["A1"].font) + f.color = Font(color="FF0000FF").color + ws["A1"].font = f + fmt = cell_formatting(ws["A1"]) + assert fmt is not None + assert fmt.font_color is not None + + def test_cell_comment_returns_obj(): wb = make_wb() cm = cell_comment(wb.active["A2"], "f.xlsx") diff --git a/tests/test_free_text_extractor.py b/tests/test_free_text_extractor.py index fe1f9f0..b654f56 100644 --- a/tests/test_free_text_extractor.py +++ b/tests/test_free_text_extractor.py @@ -1,4 +1,4 @@ -from genesis.data_models import CellValue, ExcelTable, SheetType +from genesis.data_models import CellValue, ExcelTable, ExtractionMethod, SheetType from genesis.parsers.free_text_extractor import build_free_text_table, extract_text_blocks @@ -15,3 +15,8 @@ def test_build_free_text_table(): assert len(table.rows) == 2 assert isinstance(table.rows[0]["text"], CellValue) assert table.rows[0]["text"].value == "A" + + +def test_free_text_uses_enum_value(): + table = build_free_text_table("メモ", ["A"], "f.xlsx") + assert table.extraction_method == ExtractionMethod.LLM_FROM_FREE_TEXT.value diff --git a/tests/test_sheet_nature.py b/tests/test_sheet_nature.py index 8320271..e5a2b35 100644 --- a/tests/test_sheet_nature.py +++ b/tests/test_sheet_nature.py @@ -19,4 +19,14 @@ def test_free_text_many_empty_rows(): def test_header_row_index(): m = [["機能ID", "名前"], ["1", "田中"]] assert find_header_row(m) == 0 - assert find_header_row([["自由テキスト"]]) == -1 \ No newline at end of file + assert find_header_row([["自由テキスト"]]) == -1 + + +def test_classify_sheet_mixed_with_bullet_line(): + m = [["ID", "名前"], ["1", "田中"], ["・備考行"]] + assert classify_sheet(m) == SheetNature.MIXED + + +def test_classify_sheet_mixed_with_square_line(): + m = [["ID", "名前"], ["1", "田中"], ["■備考行"]] + assert classify_sheet(m) == SheetNature.MIXED \ No newline at end of file diff --git a/tests/test_table_extractor.py b/tests/test_table_extractor.py index 36a3cc8..e80e083 100644 --- a/tests/test_table_extractor.py +++ b/tests/test_table_extractor.py @@ -1,4 +1,4 @@ -from genesis.data_models import CellValue, ExcelTable, SheetType +from genesis.data_models import CellValue, ExcelTable, ExtractionMethod, SheetType from genesis.parsers.merge_fill import forward_fill from genesis.parsers.table_extractor import column_letter, extract_table @@ -37,4 +37,16 @@ def test_extract_table_basic(): assert isinstance(first["ID"], CellValue) assert first["ID"].value == "1" assert first["ID"].provenance.sheet_name == "社員一覧" - assert first["ID"].provenance.column == "A" \ No newline at end of file + assert first["ID"].provenance.column == "A" + + +def test_extract_table_uses_enum_value(): + matrix = [["ID"], ["1"]] + table = extract_table("社員", matrix, "f.xlsx", SheetType.FUNCTION) + assert table.extraction_method == ExtractionMethod.OPENPYXL.value + + +def test_forward_fill_ignores_invalid_range(): + matrix = [["A"], ["B"]] + assert forward_fill(matrix, [(0, 1, 2, 1)]) == matrix # min_row=0 非法 → 不崩溃不改数据 + assert forward_fill(matrix, [(9, 9, 9, 9)]) == matrix # 越界 → 不崩溃 \ No newline at end of file