From df9427c76e3c059a3a56fbbba616442b377ecc81 Mon Sep 17 00:00:00 2001 From: lhl Date: Sat, 8 Aug 2026 16:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ExcelParser=20=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=EF=BC=88workbook/sheet=20=E8=AF=BB=E5=8F=96=20+=20source=5Furi?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/genesis/parsers/__init__.py | 1 + src/genesis/parsers/excel_reader.py | 20 ++++++++++++++++++++ src/genesis/parsers/provenance.py | 3 +++ tests/excel_helpers.py | 20 ++++++++++++++++++++ tests/test_excel_reader.py | 24 ++++++++++++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 src/genesis/parsers/__init__.py create mode 100644 src/genesis/parsers/excel_reader.py create mode 100644 src/genesis/parsers/provenance.py create mode 100644 tests/excel_helpers.py create mode 100644 tests/test_excel_reader.py diff --git a/src/genesis/parsers/__init__.py b/src/genesis/parsers/__init__.py new file mode 100644 index 0000000..23773ce --- /dev/null +++ b/src/genesis/parsers/__init__.py @@ -0,0 +1 @@ +"""Parser Agent:输入资料解析层。""" diff --git a/src/genesis/parsers/excel_reader.py b/src/genesis/parsers/excel_reader.py new file mode 100644 index 0000000..dc41b52 --- /dev/null +++ b/src/genesis/parsers/excel_reader.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from openpyxl import load_workbook +from openpyxl.worksheet.worksheet import Worksheet + + +def open_workbook(path: str | Path): + """普通模式打开 .xlsx(保留公式/样式/批注),.xls 报错。""" + path = Path(path) + if path.suffix.lower() != ".xlsx": + raise ValueError(f"不支持的 Excel 格式: {path.suffix}") + return load_workbook(path) + + +def sheet_matrix(ws: Worksheet) -> list[list[Any]]: + """整表矩形值(含 None),保留到 max_column。""" + return [[cell.value for cell in row] for row in ws.iter_rows()] \ No newline at end of file diff --git a/src/genesis/parsers/provenance.py b/src/genesis/parsers/provenance.py new file mode 100644 index 0000000..afbf72d --- /dev/null +++ b/src/genesis/parsers/provenance.py @@ -0,0 +1,3 @@ +def build_source_uri(file_name: str, sheet_name: str, cell_ref: str) -> str: + """单元格来源 URI:file.xlsx#SheetName!CellRef""" + return f"{file_name}#{sheet_name}!{cell_ref}" \ No newline at end of file diff --git a/tests/excel_helpers.py b/tests/excel_helpers.py new file mode 100644 index 0000000..c764fa7 --- /dev/null +++ b/tests/excel_helpers.py @@ -0,0 +1,20 @@ +from openpyxl import Workbook + + +def new_workbook(sheets: dict[str, list[list]]) -> Workbook: + """生成临时 Workbook:key=Sheet 名,value=grid(cell 值)。""" + wb = Workbook() + wb.remove(wb.active) + for name, grid in sheets.items(): + ws = wb.create_sheet(name) + for r, row in enumerate(grid, start=1): + for c, value in enumerate(row, start=1): + ws.cell(row=r, column=c, value=value) + return wb + + +def save_workbook(tmp_path, wb: Workbook) -> str: + """落盘到 tmp_path 并返回路径字符串。""" + path = tmp_path / "source.xlsx" + wb.save(path) + return str(path) \ No newline at end of file diff --git a/tests/test_excel_reader.py b/tests/test_excel_reader.py new file mode 100644 index 0000000..e3b7694 --- /dev/null +++ b/tests/test_excel_reader.py @@ -0,0 +1,24 @@ +import pytest + +from genesis.parsers.excel_reader import open_workbook, sheet_matrix +from genesis.parsers.provenance import build_source_uri + +from tests.excel_helpers import new_workbook, save_workbook + + +def test_build_source_uri_format(): + assert build_source_uri("要求.xlsx", "機能一覧", "A5") == "要求.xlsx#機能一覧!A5" + + +def test_open_workbook_and_sheet_matrix(tmp_path): + wb = new_workbook({"機能一覧": [["機能ID", "機能名"], ["F001", "社員登録"]]}) + path = save_workbook(tmp_path, wb) + ws = open_workbook(path)["機能一覧"] + assert sheet_matrix(ws) == [["機能ID", "機能名"], ["F001", "社員登録"]] + + +def test_open_workbook_rejects_xls(tmp_path): + bad = tmp_path / "old.xls" + bad.write_bytes(b"not really xls") + with pytest.raises(ValueError): + open_workbook(bad)