Coverage for src\genesis\parsers\excel_parser.py: 100%
73 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1from __future__ import annotations
3from dataclasses import dataclass, field
4from pathlib import Path
6from genesis.data_models import CellComment, ExcelTable, MixedParagraph, MixedSheet
7from genesis.parsers.excel_reader import open_workbook, sheet_matrix
8from genesis.parsers.sheet_detector import detect_sheet_type
9from genesis.parsers.sheet_nature import SheetNature, classify_sheet, find_header_row
10from genesis.parsers.merge_fill import forward_fill
11from genesis.parsers.paragraph_splitter import split_paragraphs
12from genesis.parsers.table_extractor import extract_table
13from genesis.parsers.formatting_detector import cell_formatting, collect_comments
14from genesis.parsers.free_text_extractor import build_free_text_table, extract_text_blocks
17@dataclass
18class ExcelParseResult:
19 file_name: str
20 tables: list[ExcelTable] = field(default_factory=list)
21 comments: list[CellComment] = field(default_factory=list)
22 skipped: list[str] = field(default_factory=list)
23 mixed: list[MixedSheet] = field(default_factory=list)
26class ExcelParser:
27 """要件定义 Excel 解析入口。"""
29 def parse(self, path: str | Path) -> ExcelParseResult:
30 wb = open_workbook(path)
31 file_name = Path(path).name
32 result = ExcelParseResult(file_name=file_name)
33 for ws in wb.worksheets:
34 matrix = sheet_matrix(ws)
35 if not matrix:
36 result.skipped.append(ws.title)
37 continue
38 detected_type = detect_sheet_type(ws.title, matrix)
39 nature = classify_sheet(matrix)
40 if nature == SheetNature.MIXED:
41 # 合并单元格:整 sheet 先填充再按段切片
42 merged = [
43 (r.min_row, r.min_col, r.max_row, r.max_col)
44 for r in ws.merged_cells.ranges
45 ]
46 filled_all = forward_fill(matrix, merged) if merged else matrix
47 # 整 sheet 构建格式映射一次(按物理坐标)
48 fmt_map = {}
49 for row in ws.iter_rows():
50 for cell in row:
51 fmt = cell_formatting(cell)
52 if fmt is not None:
53 fmt_map[(cell.row - 1, cell.column - 1)] = fmt
54 mixed_sheet = MixedSheet(name=ws.title)
55 for (s, e) in split_paragraphs(matrix):
56 seg = filled_all[s:e + 1]
57 seg_nature = classify_sheet(seg)
58 if seg_nature == SheetNature.TABLE:
59 header_row = find_header_row(seg)
60 if header_row < 0: # pragma: no cover — classify==TABLE 时 find_header_row 恒 ≥0
61 header_row = 0
62 # extract_table 的 formatting_map 用段内矩阵坐标 (r,c);
63 # 从整 sheet 物理 map 抽出段内子 map(物理行 pr → 段内 pr-s)
64 seg_fmt_map = {}
65 for (pr, pc), fmt in fmt_map.items():
66 if s <= pr <= e:
67 seg_fmt_map[(pr - s, pc)] = fmt
68 table = extract_table(
69 ws.title, seg, file_name, detected_type,
70 header_row=header_row, formatting_map=seg_fmt_map,
71 )
72 result.tables.append(table)
73 mixed_sheet.paragraphs.append(MixedParagraph(
74 kind="table", matrix=seg, table=table,
75 source_range=(s, e),
76 ))
77 else:
78 blocks = extract_text_blocks(seg)
79 table = build_free_text_table(ws.title, blocks, file_name, detected_type)
80 result.tables.append(table)
81 mixed_sheet.paragraphs.append(MixedParagraph(
82 kind="free_text", matrix=seg,
83 text="\n".join(blocks), source_range=(s, e),
84 ))
85 result.mixed.append(mixed_sheet)
86 elif nature == SheetNature.FREE_TEXT:
87 blocks = extract_text_blocks(matrix)
88 result.tables.append(
89 build_free_text_table(ws.title, blocks, file_name, detected_type)
90 )
91 else:
92 # 现有 TABLE 路径(MIXED 已独立,不折叠至此)
93 merged = [
94 (r.min_row, r.min_col, r.max_row, r.max_col)
95 for r in ws.merged_cells.ranges
96 ]
97 filled = forward_fill(matrix, merged) if merged else matrix
98 header_row = find_header_row(filled)
99 if header_row < 0: # pragma: no cover — classify==TABLE 时 find_header_row 恒 ≥0
100 header_row = 0
101 fmt_map = {}
102 for row in ws.iter_rows():
103 for cell in row:
104 fmt = cell_formatting(cell)
105 if fmt is not None:
106 fmt_map[(cell.row - 1, cell.column - 1)] = fmt
107 result.tables.append(
108 extract_table(
109 ws.title, filled, file_name, detected_type,
110 header_row=header_row, formatting_map=fmt_map,
111 )
112 )
113 result.comments.extend(collect_comments(ws, file_name))
114 return result