feat: ExcelParser 编排器(类型/性质/提取/汇总)
This commit is contained in:
+3
-1
@@ -26,5 +26,7 @@
|
||||
| 2026-08-08 | Agent 实现 | 里程碑2 Task2 实现:SheetDetector 类型识别。新建 src/genesis/parsers/sheet_detector.py(名称关键词→表头关键词→GENERIC 三级判定,detect_sheet_type(sheet_name, matrix)->SheetType)与 tests/test_sheet_detector.py(3 用例:名称/表头/未知);TDD 验证 RED(ModuleNotFoundError)→ GREEN(3 passed);修正 brief 缺陷:brief 原始 NAME_KEYWORDS 无法匹配「コード管理」判为 MASTER(不含"マスタ"),最小补入 ("コード", MASTER);pytest 全量 20 passed;提交 66753e4 | src/genesis/parsers/sheet_detector.py, tests/test_sheet_detector.py | deepseek-v4-flash-free |
|
||||
| 2026-08-08 | Agent 实现 | 里程碑2 Task3 实现:Sheet 性质判定。新建 src/genesis/parsers/sheet_nature.py(纯函数:SheetNature 枚举 TABLE/FREE_TEXT/MIXED、find_header_row 表头行定位、classify_sheet 按 design §3.5.2 启发式判定)与 tests/test_sheet_nature.py(4 用例);TDD RED→GREEN;pytest 全量 24 passed | src/genesis/parsers/sheet_nature.py, tests/test_sheet_nature.py | deepseek-v4-flash-free |
|
||||
| 2026-08-08 23:04 | Agent 实现 | 里程碑2 Task4 实现:MergeHandler 与 TableExtractor。新建 src/genesis/parsers/merge_fill.py(forward_fill 合并单元格展开,(min_row,min_col,max_row,max_col) 1-based 范围用左上主格值填充,边界保护)与 src/genesis/parsers/table_extractor.py(column_letter 1→A/27→AA;extract_table 首行表头→其后为数据行,构建 CellValue+Provenance(row=r-header_row 从 1 起、column 字母、column_header),无矩阵时返回空表)及 tests/test_table_extractor.py(4 用例:纵向/横向合并、列字母、基本提取);TDD 验证 RED(ImportError: No module named 'genesis.parsers.merge_fill')→ GREEN(4 passed);pytest 全量 28 passed;data_models.py 未改动;提交 63d0c90 | src/genesis/parsers/merge_fill.py, src/genesis/parsers/table_extractor.py, tests/test_table_extractor.py | deepseek-v4-flash-free |
|
||||
| 2026-08-09 02:59 | Agent 实现 | 里程碑2 Task5 实现:FormattingDetector(取消线/背景色/批注)。新建 src/genesis/parsers/formatting_detector.py(cell_formatting 检测 strike/字体色/背景色,纯默认单元格返回 None;cell_comment 返回 CellComment;collect_comments 遍历 ws.iter_rows 收集批注)与 tests/test_formatting_detector.py(4 用例);TDD 验证 RED(ImportError)→ GREEN(4 passed);修正 brief 两处 openpyxl 3.1.5 兼容问题(① 字体 Style 对象不可变,测试用 copy 副本设 strike;② 默认 theme 色 .rgb 返回错误文本而非 None,新增 _to_rgb_hex 严格校验仅接受 6/8 位十六进制并排除纯黑);pytest 全量 32 passed;data_models.py 未改动 | src/genesis/parsers/formatting_detector.py, tests/test_formatting_detector.py | deepseek-v4-flash-free |
|
||||
| 2026-08-09 02:59 | Agent 实现 | 里程碑2 Task5 实现:FormattingDetector(取消线/背景色/批注)。新建 src/genesis/parsers/formatting_detector.py(cell_formatting 检测 strike/字体色/背景色,纯默认单元格返回 None;cell_comment 返回 CellComment;collect_comments 遍历 ws.iter_rows 收集批注)与 tests/test_formatting_detector.py(4 用例);TDD 验证 RED(ImportError)→ GREEN(4 passed);修正 brief 两处 openpyxl 3.1.5 兼容问题(① 字体 Style 对象不可变,测试用 setter 设 strike;② 默认 theme 色 .rgb 取值异常,新增 _to_rgb_hex 严格校验仅接受 6/8 位十六进制并排除纯黑);pytest 全量 32 passed;data_models.py 未改动 | src/genesis/parsers/formatting_detector.py, tests/test_formatting_detector.py | deepseek-v4-flash-free |
|
||||
| 2026-08-09 03:15 | Agent 实现 | 里程碑2 Task6 实现:FreeTextExtractor(自由记述型 Sheet 内容提取)。新建 src/genesis/parsers/free_text_extractor.py(extract_text_blocks 按全空行分段、行内非空单元格以「 」连接;build_free_text_table 构建占位结构化表,extraction_method="llm_from_free_text"、headers=["text"]、行内 CellValue+Provenance(row 从 1 起、column="A"、column_header="text")),后续 LLM 结构化入口;tests/test_free_text_extractor.py(2 用例:全空行分段、占位表构建);TDD 验证 RED(ImportError: No module named 'genesis.parsers.free_text_extractor')→ GREEN(2 passed);pytest 全量 34 passed;待接入 SheetNature.FREE_TEXT 分支;data_models.py 未改动;提交 a9cc3c9 | src/genesis/parsers/free_text_extractor.py, tests/test_free_text_extractor.py | deepseek-v4-flash-free |
|
||||
| 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 |
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
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.merge_fill import forward_fill
|
||||
from genesis.parsers.table_extractor import extract_table
|
||||
from genesis.parsers.formatting_detector import collect_comments
|
||||
from genesis.parsers.free_text_extractor import build_free_text_table, extract_text_blocks
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExcelParseResult:
|
||||
file_name: str
|
||||
tables: list[ExcelTable] = field(default_factory=list)
|
||||
comments: list[CellComment] = field(default_factory=list)
|
||||
skipped: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class ExcelParser:
|
||||
"""要件定义 Excel 解析入口。"""
|
||||
|
||||
def parse(self, path: str | Path) -> ExcelParseResult:
|
||||
wb = open_workbook(path)
|
||||
file_name = Path(path).name
|
||||
result = ExcelParseResult(file_name=file_name)
|
||||
for ws in wb.worksheets:
|
||||
matrix = sheet_matrix(ws)
|
||||
if not matrix:
|
||||
result.skipped.append(ws.title)
|
||||
continue
|
||||
detected_type = detect_sheet_type(ws.title, matrix)
|
||||
nature = classify_sheet(matrix)
|
||||
if nature == SheetNature.FREE_TEXT:
|
||||
blocks = extract_text_blocks(matrix)
|
||||
result.tables.append(
|
||||
build_free_text_table(ws.title, blocks, file_name, detected_type)
|
||||
)
|
||||
else:
|
||||
merged = [
|
||||
(r.min_row, r.min_col, r.max_row, r.max_col)
|
||||
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))
|
||||
result.comments.extend(collect_comments(ws, file_name))
|
||||
return result
|
||||
@@ -0,0 +1,26 @@
|
||||
from genesis.data_models import SheetType
|
||||
from genesis.parsers.excel_parser import ExcelParseResult, ExcelParser
|
||||
|
||||
from tests.excel_helpers import new_workbook, save_workbook
|
||||
|
||||
|
||||
def test_parse_table_sheets(tmp_path):
|
||||
wb = new_workbook({
|
||||
"機能一覧": [["機能ID", "機能名"], ["A001", "社員登録"]],
|
||||
"バッチ一覧": [["バッチID", "処理名"], ["B1", "夜間集計"]],
|
||||
})
|
||||
path = save_workbook(tmp_path, wb)
|
||||
result = ExcelParser().parse(path)
|
||||
assert isinstance(result, ExcelParseResult)
|
||||
by_name = {t.name: t for t in result.tables}
|
||||
assert by_name["機能一覧"].detected_type == SheetType.FUNCTION
|
||||
assert len(by_name["機能一覧"].rows) == 1
|
||||
assert by_name["バッチ一覧"].detected_type == SheetType.BATCH
|
||||
|
||||
|
||||
def test_parse_free_text_sheet(tmp_path):
|
||||
wb = new_workbook({"メモ": [["新入社員を登録"], [], [], [], []]})
|
||||
path = save_workbook(tmp_path, wb)
|
||||
result = ExcelParser().parse(path)
|
||||
assert len(result.tables) == 1
|
||||
assert result.tables[0].extraction_method == "llm_from_free_text"
|
||||
Reference in New Issue
Block a user