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

1from __future__ import annotations 

2 

3from dataclasses import dataclass, field 

4from pathlib import Path 

5 

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 

15 

16 

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) 

24 

25 

26class ExcelParser: 

27 """要件定义 Excel 解析入口。""" 

28 

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