|
|
@@ -18,14 +18,7 @@ _current_theme_fonts = {}
|
|
18
|
18
|
|
|
19
|
19
|
|
|
20
|
20
|
def _load_theme_fonts(docx_path: Path) -> dict:
|
|
21
|
|
- """从 Word 文档中加载主题字体定义
|
|
22
|
|
-
|
|
23
|
|
- Args:
|
|
24
|
|
- docx_path: Word 文档路径
|
|
25
|
|
-
|
|
26
|
|
- Returns:
|
|
27
|
|
- 主题字体映射字典,例如: {'minorEastAsia': '宋体', 'majorEastAsia': '黑体'}
|
|
28
|
|
- """
|
|
|
21
|
+ """从 Word 文档中加载主题字体定义"""
|
|
29
|
22
|
# 检查缓存
|
|
30
|
23
|
cache_key = str(docx_path)
|
|
31
|
24
|
if cache_key in _theme_fonts_cache:
|
|
|
@@ -83,14 +76,7 @@ def _load_theme_fonts(docx_path: Path) -> dict:
|
|
83
|
76
|
|
|
84
|
77
|
|
|
85
|
78
|
def _get_eastasia_font_from_element(element):
|
|
86
|
|
- """从 XML 元素中提取 eastAsia 字体(用于中文字体)
|
|
87
|
|
-
|
|
88
|
|
- Args:
|
|
89
|
|
- element: rPr XML 元素
|
|
90
|
|
-
|
|
91
|
|
- Returns:
|
|
92
|
|
- eastAsia 字体名称或 None
|
|
93
|
|
- """
|
|
|
79
|
+ """从 XML 元素中提取 eastAsia 字体(用于中文字体)"""
|
|
94
|
80
|
if element is None:
|
|
95
|
81
|
return None
|
|
96
|
82
|
rFonts = element.find(qn('w:rFonts'))
|
|
|
@@ -102,19 +88,7 @@ def _get_eastasia_font_from_element(element):
|
|
102
|
88
|
|
|
103
|
89
|
|
|
104
|
90
|
def _get_font_name(run, theme_fonts: dict = None):
|
|
105
|
|
- """获取 run 的字体名(优先 eastAsia,其次 ascii,支持主题字体)
|
|
106
|
|
-
|
|
107
|
|
- 特殊处理:如果 run 只定义了 ascii 字体(如 Times New Roman),
|
|
108
|
|
- 但没有定义 eastAsia,则忽略 run 的字体,返回 None 让其从样式继承中文字体。
|
|
109
|
|
- 这样可以正确处理混合语言的字体继承。
|
|
110
|
|
-
|
|
111
|
|
- Args:
|
|
112
|
|
- run: python-docx Run 对象
|
|
113
|
|
- theme_fonts: 主题字体映射字典(可选,默认使用全局的 _current_theme_fonts)
|
|
114
|
|
-
|
|
115
|
|
- Returns:
|
|
116
|
|
- 字体名称或 None
|
|
117
|
|
- """
|
|
|
91
|
+ """获取 run 的字体名(优先 eastAsia,其次 ascii,支持主题字体;特殊处理混合语言字体继承)"""
|
|
118
|
92
|
if theme_fonts is None:
|
|
119
|
93
|
theme_fonts = _current_theme_fonts
|
|
120
|
94
|
|
|
|
@@ -153,16 +127,7 @@ def _get_font_name(run, theme_fonts: dict = None):
|
|
153
|
127
|
|
|
154
|
128
|
|
|
155
|
129
|
def _get_paragraph_style_font(para):
|
|
156
|
|
- """从段落样式中提取字体(当 run 级别没有字体设置时使用)
|
|
157
|
|
-
|
|
158
|
|
- 优先提取 eastAsia(中文字体),如果没有则查找基础样式的 eastAsia
|
|
159
|
|
-
|
|
160
|
|
- Args:
|
|
161
|
|
- para: python-docx 段落对象
|
|
162
|
|
-
|
|
163
|
|
- Returns:
|
|
164
|
|
- 字体名称或 None
|
|
165
|
|
- """
|
|
|
130
|
+ """从段落样式中提取字体(当 run 级别没有字体设置时使用,优先 eastAsia)"""
|
|
166
|
131
|
try:
|
|
167
|
132
|
style = para.style
|
|
168
|
133
|
if hasattr(style, 'element'):
|
|
|
@@ -201,14 +166,7 @@ def _get_paragraph_style_font(para):
|
|
201
|
166
|
|
|
202
|
167
|
|
|
203
|
168
|
def _get_paragraph_style_font_recursive(style):
|
|
204
|
|
- """递归查找样式的 eastAsia 字体(用于基础样式查找)
|
|
205
|
|
-
|
|
206
|
|
- Args:
|
|
207
|
|
- style: python-docx Style 对象
|
|
208
|
|
-
|
|
209
|
|
- Returns:
|
|
210
|
|
- eastAsia 字体名称或 None
|
|
211
|
|
- """
|
|
|
169
|
+ """递归查找样式的 eastAsia 字体(用于基础样式查找)"""
|
|
212
|
170
|
try:
|
|
213
|
171
|
if hasattr(style, 'element'):
|
|
214
|
172
|
rPr = style.element.find(qn('w:rPr'))
|
|
|
@@ -229,14 +187,7 @@ def _get_paragraph_style_font_recursive(style):
|
|
229
|
187
|
|
|
230
|
188
|
|
|
231
|
189
|
def _get_style_formatting(style):
|
|
232
|
|
- """从样式中提取格式属性(加粗、斜体、下划线等)
|
|
233
|
|
-
|
|
234
|
|
- Args:
|
|
235
|
|
- style: python-docx Style 对象
|
|
236
|
|
-
|
|
237
|
|
- Returns:
|
|
238
|
|
- 格式属性字典 {'bold': True/False, 'italic': True/False, ...}
|
|
239
|
|
- """
|
|
|
190
|
+ """从样式中提取格式属性(加粗、斜体、下划线等)"""
|
|
240
|
191
|
formatting = {}
|
|
241
|
192
|
|
|
242
|
193
|
if not style or not hasattr(style, 'element'):
|
|
|
@@ -274,14 +225,7 @@ def _get_style_formatting(style):
|
|
274
|
225
|
|
|
275
|
226
|
|
|
276
|
227
|
def parse_word_to_blocks(docx_path: Path) -> list[dict]:
|
|
277
|
|
- """将 Word 文档解析为 Block 列表
|
|
278
|
|
-
|
|
279
|
|
- Args:
|
|
280
|
|
- docx_path: Word 文档路径
|
|
281
|
|
-
|
|
282
|
|
- Returns:
|
|
283
|
|
- Block 列表,每个 Block 包含 id, block_order, type, level, index, content 等字段
|
|
284
|
|
- """
|
|
|
228
|
+ """将 Word 文档解析为 Block 列表"""
|
|
285
|
229
|
global _current_theme_fonts
|
|
286
|
230
|
|
|
287
|
231
|
doc = DocxDocument(str(docx_path))
|
|
|
@@ -513,15 +457,7 @@ def parse_word_to_blocks(docx_path: Path) -> list[dict]:
|
|
513
|
457
|
|
|
514
|
458
|
|
|
515
|
459
|
def _identify_heading_level(para, style_name: str) -> Optional[int]:
|
|
516
|
|
- """识别段落的标题级别
|
|
517
|
|
-
|
|
518
|
|
- Args:
|
|
519
|
|
- para: python-docx 段落对象
|
|
520
|
|
- style_name: 样式名称
|
|
521
|
|
-
|
|
522
|
|
- Returns:
|
|
523
|
|
- 标题级别(1-6)或 None(不是标题)
|
|
524
|
|
- """
|
|
|
460
|
+ """识别段落的标题级别(1-6 或 None)"""
|
|
525
|
461
|
# 方法1:检查样式名称(内置样式)
|
|
526
|
462
|
if style_name.startswith('Heading'):
|
|
527
|
463
|
try:
|
|
|
@@ -559,14 +495,7 @@ def _identify_heading_level(para, style_name: str) -> Optional[int]:
|
|
559
|
495
|
|
|
560
|
496
|
|
|
561
|
497
|
def _extract_paragraph_format(para) -> dict:
|
|
562
|
|
- """提取段落级样式(Block 级别的 style)
|
|
563
|
|
-
|
|
564
|
|
- Args:
|
|
565
|
|
- para: python-docx 段落对象
|
|
566
|
|
-
|
|
567
|
|
- Returns:
|
|
568
|
|
- 段落样式字典(只包含设计文档 5.3 中可支持的属性)
|
|
569
|
|
- """
|
|
|
498
|
+ """提取段落级样式(Block 级别的 style)"""
|
|
570
|
499
|
style = {}
|
|
571
|
500
|
|
|
572
|
501
|
# 对齐方式
|
|
|
@@ -656,16 +585,7 @@ def _extract_paragraph_format(para) -> dict:
|
|
656
|
585
|
|
|
657
|
586
|
|
|
658
|
587
|
def _extract_rich_text(para) -> str | list:
|
|
659
|
|
- """提取段落的富文本内容
|
|
660
|
|
-
|
|
661
|
|
- Args:
|
|
662
|
|
- para: python-docx 段落对象
|
|
663
|
|
-
|
|
664
|
|
- Returns:
|
|
665
|
|
- 纯文本字符串 或 富文本片段列表
|
|
666
|
|
- - 纯文本:所有 runs 样式相同,返回字符串
|
|
667
|
|
- - 富文本:runs 样式不同,返回数组,每个元素包含完整样式
|
|
668
|
|
- """
|
|
|
588
|
+ """提取段落的富文本内容(纯文本字符串或富文本片段列表)"""
|
|
669
|
589
|
text = para.text.strip()
|
|
670
|
590
|
if not text:
|
|
671
|
591
|
return ""
|
|
|
@@ -760,14 +680,7 @@ def _extract_rich_text(para) -> str | list:
|
|
760
|
680
|
|
|
761
|
681
|
|
|
762
|
682
|
def _extract_table(table) -> dict:
|
|
763
|
|
- """提取表格内容
|
|
764
|
|
-
|
|
765
|
|
- Args:
|
|
766
|
|
- table: python-docx 表格对象
|
|
767
|
|
-
|
|
768
|
|
- Returns:
|
|
769
|
|
- 表格数据字典,包含合并单元格和尺寸信息
|
|
770
|
|
- """
|
|
|
683
|
+ """提取表格内容(包含合并单元格和尺寸信息)- 完整修复版"""
|
|
771
|
684
|
rows_data = []
|
|
772
|
685
|
|
|
773
|
686
|
# 提取表格列宽(从 tblGrid)
|
|
|
@@ -781,200 +694,312 @@ def _extract_table(table) -> dict:
|
|
781
|
694
|
# twips 转 pt (1 pt = 20 twips)
|
|
782
|
695
|
col_widths.append(int(width) / 20)
|
|
783
|
696
|
|
|
784
|
|
- # 用于跟踪行合并(vMerge)
|
|
785
|
|
- # col_index -> {start_row, rowspan_count}
|
|
786
|
|
- vmerge_tracking = {}
|
|
|
697
|
+ # 第一遍:从 XML 直接读取,建立列索引到行合并信息的映射
|
|
|
698
|
+ # col_index -> [{start_row, end_row}, ...] # 可能有多个合并区间
|
|
|
699
|
+ vmerge_map = {}
|
|
787
|
700
|
|
|
788
|
|
- for row_idx, row in enumerate(table.rows):
|
|
789
|
|
- cells_data = []
|
|
790
|
|
-
|
|
791
|
|
- # 提取行高
|
|
792
|
|
- row_height = None
|
|
793
|
|
- if row.height:
|
|
794
|
|
- row_height = row.height.pt
|
|
|
701
|
+ trs = tbl_elem.findall(qn('w:tr'))
|
|
|
702
|
+ for row_idx, tr in enumerate(trs):
|
|
|
703
|
+ tcs = tr.findall(qn('w:tc'))
|
|
|
704
|
+ col_offset = 0
|
|
795
|
705
|
|
|
796
|
|
- col_offset = 0 # 当前列偏移(考虑 colspan)
|
|
797
|
|
- seen_cells = set() # 用于去重(基于对象 ID)
|
|
798
|
|
-
|
|
799
|
|
- for cell_idx, cell in enumerate(row.cells):
|
|
800
|
|
- # 去重:跳过重复的单元格对象(合并单元格会返回同一个对象)
|
|
801
|
|
- cell_id = id(cell)
|
|
802
|
|
- if cell_id in seen_cells:
|
|
803
|
|
- continue
|
|
804
|
|
- seen_cells.add(cell_id)
|
|
805
|
|
- # 提取单元格文本
|
|
806
|
|
- cell_text = []
|
|
807
|
|
- for para in cell.paragraphs:
|
|
808
|
|
- para_text = _extract_rich_text(para)
|
|
809
|
|
- if para_text:
|
|
810
|
|
- cell_text.append(para_text if isinstance(para_text, str) else para_text)
|
|
811
|
|
-
|
|
812
|
|
- # 检测单元格样式(从第一个段落的第一个 run)
|
|
813
|
|
- cell_style = {}
|
|
814
|
|
- cell_word_style = None # 单元格的 word_style
|
|
815
|
|
-
|
|
816
|
|
- if cell.paragraphs:
|
|
817
|
|
- first_para = cell.paragraphs[0]
|
|
818
|
|
-
|
|
819
|
|
- # 提取 word_style(段落样式)
|
|
820
|
|
- if first_para.style:
|
|
821
|
|
- cell_word_style = first_para.style.name
|
|
822
|
|
-
|
|
823
|
|
- # 从样式中提取格式(加粗、斜体等)
|
|
824
|
|
- style_formatting = _get_style_formatting(first_para.style)
|
|
825
|
|
- # 将样式中定义的格式作为基础
|
|
826
|
|
- cell_style.update(style_formatting)
|
|
827
|
|
-
|
|
828
|
|
- if first_para.runs:
|
|
829
|
|
- first_run = first_para.runs[0]
|
|
830
|
|
-
|
|
831
|
|
- # 加粗(run 明确设置会覆盖样式)
|
|
832
|
|
- if first_run.bold is True:
|
|
833
|
|
- cell_style['bold'] = True
|
|
834
|
|
- elif first_run.bold is False:
|
|
835
|
|
- # 明确设置为不加粗,移除样式的加粗
|
|
836
|
|
- cell_style.pop('bold', None)
|
|
837
|
|
- # 如果 run.bold 为 None,保持样式中的设置
|
|
838
|
|
-
|
|
839
|
|
- # 斜体(run 明确设置会覆盖样式)
|
|
840
|
|
- if first_run.italic is True:
|
|
841
|
|
- cell_style['italic'] = True
|
|
842
|
|
- elif first_run.italic is False:
|
|
843
|
|
- cell_style.pop('italic', None)
|
|
844
|
|
-
|
|
845
|
|
- # 下划线(run 明确设置会覆盖样式)
|
|
846
|
|
- if first_run.underline:
|
|
847
|
|
- cell_style['underline'] = True
|
|
848
|
|
-
|
|
849
|
|
- # 字体
|
|
850
|
|
- font_name = _get_font_name(first_run)
|
|
851
|
|
- if font_name:
|
|
852
|
|
- cell_style['font_name'] = font_name
|
|
853
|
|
-
|
|
854
|
|
- # 字号
|
|
855
|
|
- if first_run.font.size:
|
|
856
|
|
- cell_style['font_size'] = first_run.font.size.pt
|
|
857
|
|
-
|
|
858
|
|
- # 颜色
|
|
859
|
|
- if first_run.font.color and first_run.font.color.rgb:
|
|
860
|
|
- cell_style['color'] = str(first_run.font.color.rgb)
|
|
861
|
|
-
|
|
862
|
|
- # 对齐方式
|
|
863
|
|
- if first_para.alignment is not None:
|
|
864
|
|
- align_map = {0: 'left', 1: 'center', 2: 'right', 3: 'justify'}
|
|
865
|
|
- cell_style['align'] = align_map.get(first_para.alignment, 'left')
|
|
866
|
|
-
|
|
867
|
|
- # 合并多个段落的文本
|
|
868
|
|
- if len(cell_text) == 1:
|
|
869
|
|
- text_content = cell_text[0]
|
|
870
|
|
- elif len(cell_text) > 1:
|
|
871
|
|
- # 多个段落,用换行符连接
|
|
872
|
|
- text_content = ' '.join(str(t) for t in cell_text)
|
|
873
|
|
- else:
|
|
874
|
|
- text_content = ""
|
|
875
|
|
-
|
|
876
|
|
- # 提取合并信息
|
|
877
|
|
- tc_elem = cell._tc
|
|
878
|
|
- tcPr = tc_elem.find(qn('w:tcPr'))
|
|
|
706
|
+ for tc in tcs:
|
|
|
707
|
+ tcPr = tc.find(qn('w:tcPr'))
|
|
879
|
708
|
|
|
880
|
709
|
colspan = 1
|
|
881
|
|
- rowspan = 1
|
|
882
|
|
- is_vmerge_continue = False
|
|
|
710
|
+ has_vmerge_restart = False
|
|
|
711
|
+ has_vmerge_continue = False
|
|
|
712
|
+ is_empty = False
|
|
883
|
713
|
|
|
884
|
714
|
if tcPr is not None:
|
|
885
|
|
- # 列合并 (gridSpan)
|
|
|
715
|
+ # 列合并
|
|
886
|
716
|
grid_span = tcPr.find(qn('w:gridSpan'))
|
|
887
|
717
|
if grid_span is not None:
|
|
888
|
718
|
colspan = int(grid_span.get(qn('w:val')))
|
|
889
|
719
|
|
|
890
|
|
- # 行合并 (vMerge)
|
|
|
720
|
+ # 行合并
|
|
891
|
721
|
v_merge = tcPr.find(qn('w:vMerge'))
|
|
892
|
722
|
if v_merge is not None:
|
|
893
|
723
|
v_merge_val = v_merge.get(qn('w:val'))
|
|
894
|
724
|
if v_merge_val == 'restart':
|
|
895
|
|
- # 行合并起始
|
|
896
|
|
- vmerge_tracking[col_offset] = {
|
|
897
|
|
- 'start_row': row_idx,
|
|
898
|
|
- 'count': 1
|
|
899
|
|
- }
|
|
900
|
|
- elif v_merge_val is None:
|
|
901
|
|
- # 行合并继续(被合并的单元格)
|
|
902
|
|
- is_vmerge_continue = True
|
|
903
|
|
- if col_offset in vmerge_tracking:
|
|
904
|
|
- vmerge_tracking[col_offset]['count'] += 1
|
|
905
|
|
-
|
|
906
|
|
- # 计算实际的 rowspan
|
|
907
|
|
- if col_offset in vmerge_tracking:
|
|
908
|
|
- if vmerge_tracking[col_offset]['start_row'] == row_idx:
|
|
909
|
|
- # 这是起始行,后续会更新 rowspan
|
|
910
|
|
- rowspan = vmerge_tracking[col_offset]['count']
|
|
911
|
|
- elif is_vmerge_continue:
|
|
912
|
|
- # 这是被合并的单元格,标记为 0(表示被合并)
|
|
913
|
|
- rowspan = 0
|
|
914
|
|
-
|
|
915
|
|
- # 提取单元格宽度
|
|
916
|
|
- cell_width = None
|
|
|
725
|
+ has_vmerge_restart = True
|
|
|
726
|
+ else:
|
|
|
727
|
+ # 'continue' 或 None/空字符串都表示继续合并
|
|
|
728
|
+ has_vmerge_continue = True
|
|
|
729
|
+
|
|
|
730
|
+ # 检查是否为空单元格(用于判断行合并)
|
|
|
731
|
+ paras = tc.findall(qn('w:p'))
|
|
|
732
|
+ text_parts = []
|
|
|
733
|
+ for p in paras:
|
|
|
734
|
+ runs = p.findall(qn('w:r'))
|
|
|
735
|
+ for r in runs:
|
|
|
736
|
+ ts = r.findall(qn('w:t'))
|
|
|
737
|
+ for t in ts:
|
|
|
738
|
+ if t.text and t.text.strip():
|
|
|
739
|
+ text_parts.append(t.text)
|
|
|
740
|
+ is_empty = len(text_parts) == 0
|
|
|
741
|
+
|
|
|
742
|
+ # 处理行合并逻辑 - 记录所有合并区间
|
|
|
743
|
+ if col_offset not in vmerge_map:
|
|
|
744
|
+ vmerge_map[col_offset] = []
|
|
|
745
|
+
|
|
|
746
|
+ merges = vmerge_map[col_offset]
|
|
|
747
|
+
|
|
|
748
|
+ if has_vmerge_restart:
|
|
|
749
|
+ # 开始新的行合并
|
|
|
750
|
+ merges.append({
|
|
|
751
|
+ 'start_row': row_idx,
|
|
|
752
|
+ 'end_row': row_idx # 初始结束行等于开始行,后续会扩展
|
|
|
753
|
+ })
|
|
|
754
|
+ elif has_vmerge_continue:
|
|
|
755
|
+ # 明确标记为 continue - 扩展最后一个合并
|
|
|
756
|
+ if merges:
|
|
|
757
|
+ merges[-1]['end_row'] = row_idx
|
|
|
758
|
+ # 注意:移除了 "is_empty" 的判断,因为空单元格不一定意味着合并
|
|
|
759
|
+
|
|
|
760
|
+ col_offset += colspan
|
|
|
761
|
+
|
|
|
762
|
+ # 第二遍:完全从 XML 提取单元格数据
|
|
|
763
|
+ for row_idx, tr in enumerate(trs):
|
|
|
764
|
+ cells_data = []
|
|
|
765
|
+
|
|
|
766
|
+ # 提取行高(从 python-docx,因为 XML 提取行高比较复杂)
|
|
|
767
|
+ row_height = None
|
|
|
768
|
+ if row_idx < len(table.rows):
|
|
|
769
|
+ row = table.rows[row_idx]
|
|
|
770
|
+ if row.height:
|
|
|
771
|
+ row_height = row.height.pt
|
|
|
772
|
+
|
|
|
773
|
+ # 遍历 XML 的 tc 元素
|
|
|
774
|
+ tcs = tr.findall(qn('w:tc'))
|
|
|
775
|
+ col_offset = 0
|
|
|
776
|
+
|
|
|
777
|
+ for tc in tcs:
|
|
|
778
|
+ # 首先提取 colspan 和 vMerge 信息
|
|
|
779
|
+ tcPr = tc.find(qn('w:tcPr'))
|
|
|
780
|
+
|
|
|
781
|
+ # 提取 colspan
|
|
|
782
|
+ colspan = 1
|
|
917
|
783
|
if tcPr is not None:
|
|
918
|
|
- tcW = tcPr.find(qn('w:tcW'))
|
|
919
|
|
- if tcW is not None:
|
|
920
|
|
- width_val = tcW.get(qn('w:w'))
|
|
921
|
|
- width_type = tcW.get(qn('w:type'))
|
|
922
|
|
- if width_val and width_type != 'pct':
|
|
923
|
|
- # twips 转 pt
|
|
924
|
|
- cell_width = int(width_val) / 20
|
|
925
|
|
-
|
|
926
|
|
- # 如果没有明确宽度,使用列宽
|
|
927
|
|
- if cell_width is None and col_offset < len(col_widths):
|
|
928
|
|
- if colspan == 1:
|
|
929
|
|
- cell_width = col_widths[col_offset]
|
|
930
|
|
- else:
|
|
931
|
|
- # 多列合并,计算总宽度
|
|
932
|
|
- cell_width = sum(col_widths[col_offset:col_offset + colspan])
|
|
933
|
|
-
|
|
934
|
|
- # 构建单元格数据(方案 D:包含 word_style)
|
|
935
|
|
- cell_data = {
|
|
936
|
|
- 'text': text_content,
|
|
937
|
|
- 'rowspan': rowspan,
|
|
938
|
|
- 'colspan': colspan,
|
|
939
|
|
- 'style': cell_style
|
|
940
|
|
- }
|
|
|
784
|
+ grid_span = tcPr.find(qn('w:gridSpan'))
|
|
|
785
|
+ if grid_span is not None:
|
|
|
786
|
+ colspan = int(grid_span.get(qn('w:val')))
|
|
|
787
|
+
|
|
|
788
|
+ # 检查 vMerge - 如果是 continue,跳过这个单元格
|
|
|
789
|
+ is_vmerge_continue = False
|
|
|
790
|
+ if tcPr is not None:
|
|
|
791
|
+ v_merge = tcPr.find(qn('w:vMerge'))
|
|
|
792
|
+ if v_merge is not None:
|
|
|
793
|
+ v_merge_val = v_merge.get(qn('w:val'))
|
|
|
794
|
+ # 'continue' 或 None/空字符串都表示继续合并
|
|
|
795
|
+ if v_merge_val != 'restart':
|
|
|
796
|
+ is_vmerge_continue = True
|
|
|
797
|
+
|
|
|
798
|
+ if is_vmerge_continue:
|
|
|
799
|
+ # 这是被合并的单元格,跳过
|
|
|
800
|
+ col_offset += colspan
|
|
|
801
|
+ continue
|
|
|
802
|
+
|
|
|
803
|
+ # 不需要跳过被占用的列,因为 XML 中已经包含了占位符
|
|
|
804
|
+ # (上面的 is_vmerge_continue 检查已经处理了)
|
|
941
|
805
|
|
|
942
|
|
- # 添加 word_style
|
|
943
|
|
- if cell_word_style:
|
|
944
|
|
- cell_data['word_style'] = cell_word_style
|
|
|
806
|
+ # 检查是否为空单元格
|
|
|
807
|
+ is_empty = True
|
|
|
808
|
+ paras = tc.findall(qn('w:p'))
|
|
|
809
|
+ text_parts = []
|
|
|
810
|
+ for p in paras:
|
|
|
811
|
+ runs = p.findall(qn('w:r'))
|
|
|
812
|
+ for r in runs:
|
|
|
813
|
+ ts = r.findall(qn('w:t'))
|
|
|
814
|
+ for t in ts:
|
|
|
815
|
+ if t.text and t.text.strip():
|
|
|
816
|
+ is_empty = False
|
|
|
817
|
+ text_parts.append(t.text)
|
|
945
|
818
|
|
|
946
|
|
- # 添加尺寸信息
|
|
947
|
|
- if cell_width is not None:
|
|
948
|
|
- cell_data['width'] = round(cell_width, 2)
|
|
|
819
|
+ # 判断是否应该提取此单元格
|
|
|
820
|
+ should_extract = True
|
|
|
821
|
+ rowspan = 1
|
|
|
822
|
+
|
|
|
823
|
+ # 查找该列该行所在的合并区间
|
|
|
824
|
+ if col_offset in vmerge_map:
|
|
|
825
|
+ merges = vmerge_map[col_offset]
|
|
|
826
|
+ for merge in merges:
|
|
|
827
|
+ if merge['start_row'] == row_idx:
|
|
|
828
|
+ # 这是合并的起始行
|
|
|
829
|
+ rowspan = merge['end_row'] - merge['start_row'] + 1
|
|
|
830
|
+ break
|
|
|
831
|
+ elif row_idx > merge['start_row'] and row_idx <= merge['end_row']:
|
|
|
832
|
+ # 这是被合并的行
|
|
|
833
|
+ should_extract = False # 跳过被合并的单元格
|
|
|
834
|
+ break
|
|
949
|
835
|
|
|
950
|
|
- cells_data.append(cell_data)
|
|
|
836
|
+ if should_extract:
|
|
|
837
|
+ # 从 XML 提取文本(支持富文本)
|
|
|
838
|
+ cell_text_segments = []
|
|
|
839
|
+ for p in paras:
|
|
|
840
|
+ para_segments = []
|
|
|
841
|
+ runs = p.findall(qn('w:r'))
|
|
|
842
|
+
|
|
|
843
|
+ for r in runs:
|
|
|
844
|
+ # 提取文本
|
|
|
845
|
+ run_text = []
|
|
|
846
|
+ for t in r.findall(qn('w:t')):
|
|
|
847
|
+ if t.text:
|
|
|
848
|
+ run_text.append(t.text)
|
|
|
849
|
+
|
|
|
850
|
+ if run_text:
|
|
|
851
|
+ # 提取 run 级样式
|
|
|
852
|
+ run_style = {}
|
|
|
853
|
+ rPr = r.find(qn('w:rPr'))
|
|
|
854
|
+ if rPr is not None:
|
|
|
855
|
+ # 加粗
|
|
|
856
|
+ if rPr.find(qn('w:b')) is not None:
|
|
|
857
|
+ run_style['bold'] = True
|
|
|
858
|
+
|
|
|
859
|
+ # 斜体
|
|
|
860
|
+ if rPr.find(qn('w:i')) is not None:
|
|
|
861
|
+ run_style['italic'] = True
|
|
|
862
|
+
|
|
|
863
|
+ # 下划线
|
|
|
864
|
+ if rPr.find(qn('w:u')) is not None:
|
|
|
865
|
+ run_style['underline'] = True
|
|
|
866
|
+
|
|
|
867
|
+ # 字号
|
|
|
868
|
+ sz = rPr.find(qn('w:sz'))
|
|
|
869
|
+ if sz is not None:
|
|
|
870
|
+ size_val = sz.get(qn('w:val'))
|
|
|
871
|
+ if size_val:
|
|
|
872
|
+ run_style['font_size'] = int(size_val) / 2 # 半磅转磅
|
|
|
873
|
+
|
|
|
874
|
+ # 颜色
|
|
|
875
|
+ color = rPr.find(qn('w:color'))
|
|
|
876
|
+ if color is not None:
|
|
|
877
|
+ color_val = color.get(qn('w:val'))
|
|
|
878
|
+ if color_val and color_val != 'auto':
|
|
|
879
|
+ run_style['color'] = color_val
|
|
|
880
|
+
|
|
|
881
|
+ para_segments.append({
|
|
|
882
|
+ 'text': ''.join(run_text),
|
|
|
883
|
+ 'style': run_style
|
|
|
884
|
+ })
|
|
|
885
|
+
|
|
|
886
|
+ if para_segments:
|
|
|
887
|
+ cell_text_segments.extend(para_segments)
|
|
|
888
|
+
|
|
|
889
|
+ # 合并文本
|
|
|
890
|
+ if len(cell_text_segments) == 0:
|
|
|
891
|
+ text_content = ""
|
|
|
892
|
+ elif len(cell_text_segments) == 1 and not cell_text_segments[0]['style']:
|
|
|
893
|
+ # 纯文本
|
|
|
894
|
+ text_content = cell_text_segments[0]['text']
|
|
|
895
|
+ else:
|
|
|
896
|
+ # 富文本或多个片段 - 简化处理:合并为纯文本
|
|
|
897
|
+ text_content = ''.join(seg['text'] for seg in cell_text_segments)
|
|
|
898
|
+
|
|
|
899
|
+ # 提取单元格样式(从第一个段落的第一个 run)
|
|
|
900
|
+ cell_style = {}
|
|
|
901
|
+ cell_word_style = None
|
|
|
902
|
+
|
|
|
903
|
+ if paras:
|
|
|
904
|
+ first_p = paras[0]
|
|
|
905
|
+ pPr = first_p.find(qn('w:pPr'))
|
|
|
906
|
+
|
|
|
907
|
+ if pPr is not None:
|
|
|
908
|
+ # 段落样式名称
|
|
|
909
|
+ pStyle = pPr.find(qn('w:pStyle'))
|
|
|
910
|
+ if pStyle is not None:
|
|
|
911
|
+ cell_word_style = pStyle.get(qn('w:val'))
|
|
|
912
|
+
|
|
|
913
|
+ # 对齐方式
|
|
|
914
|
+ jc = pPr.find(qn('w:jc'))
|
|
|
915
|
+ if jc is not None:
|
|
|
916
|
+ align_val = jc.get(qn('w:val'))
|
|
|
917
|
+ align_map = {'left': 'left', 'center': 'center', 'right': 'right', 'both': 'justify'}
|
|
|
918
|
+ cell_style['align'] = align_map.get(align_val, 'left')
|
|
|
919
|
+
|
|
|
920
|
+ # 从第一个 run 提取样式
|
|
|
921
|
+ runs = first_p.findall(qn('w:r'))
|
|
|
922
|
+ if runs:
|
|
|
923
|
+ first_r = runs[0]
|
|
|
924
|
+ rPr = first_r.find(qn('w:rPr'))
|
|
|
925
|
+ if rPr is not None:
|
|
|
926
|
+ # 加粗
|
|
|
927
|
+ if rPr.find(qn('w:b')) is not None:
|
|
|
928
|
+ cell_style['bold'] = True
|
|
|
929
|
+
|
|
|
930
|
+ # 斜体
|
|
|
931
|
+ if rPr.find(qn('w:i')) is not None:
|
|
|
932
|
+ cell_style['italic'] = True
|
|
|
933
|
+
|
|
|
934
|
+ # 下划线
|
|
|
935
|
+ if rPr.find(qn('w:u')) is not None:
|
|
|
936
|
+ cell_style['underline'] = True
|
|
|
937
|
+
|
|
|
938
|
+ # 字号
|
|
|
939
|
+ sz = rPr.find(qn('w:sz'))
|
|
|
940
|
+ if sz is not None:
|
|
|
941
|
+ size_val = sz.get(qn('w:val'))
|
|
|
942
|
+ if size_val:
|
|
|
943
|
+ cell_style['font_size'] = int(size_val) / 2
|
|
|
944
|
+
|
|
|
945
|
+ # 颜色
|
|
|
946
|
+ color = rPr.find(qn('w:color'))
|
|
|
947
|
+ if color is not None:
|
|
|
948
|
+ color_val = color.get(qn('w:val'))
|
|
|
949
|
+ if color_val and color_val != 'auto':
|
|
|
950
|
+ cell_style['color'] = color_val
|
|
|
951
|
+
|
|
|
952
|
+ # 字体(复杂,需要处理主题字体)
|
|
|
953
|
+ rFonts = rPr.find(qn('w:rFonts'))
|
|
|
954
|
+ if rFonts is not None:
|
|
|
955
|
+ font_name = (rFonts.get(qn('w:eastAsia')) or
|
|
|
956
|
+ rFonts.get(qn('w:ascii')) or
|
|
|
957
|
+ rFonts.get(qn('w:hAnsi')))
|
|
|
958
|
+ if font_name:
|
|
|
959
|
+ cell_style['font_name'] = font_name
|
|
|
960
|
+
|
|
|
961
|
+ # 提取单元格宽度
|
|
|
962
|
+ cell_width = None
|
|
|
963
|
+ if tcPr is not None:
|
|
|
964
|
+ tcW = tcPr.find(qn('w:tcW'))
|
|
|
965
|
+ if tcW is not None:
|
|
|
966
|
+ width_val = tcW.get(qn('w:w'))
|
|
|
967
|
+ width_type = tcW.get(qn('w:type'))
|
|
|
968
|
+ if width_val and width_type != 'pct':
|
|
|
969
|
+ cell_width = int(width_val) / 20
|
|
|
970
|
+
|
|
|
971
|
+ if cell_width is None and col_offset < len(col_widths):
|
|
|
972
|
+ if colspan == 1:
|
|
|
973
|
+ cell_width = col_widths[col_offset]
|
|
|
974
|
+ else:
|
|
|
975
|
+ cell_width = sum(col_widths[col_offset:col_offset + colspan])
|
|
|
976
|
+
|
|
|
977
|
+ # 构建单元格数据
|
|
|
978
|
+ cell_data = {
|
|
|
979
|
+ 'text': text_content,
|
|
|
980
|
+ 'rowspan': rowspan,
|
|
|
981
|
+ 'colspan': colspan,
|
|
|
982
|
+ 'col_index': col_offset, # 记录该单元格的绝对列位置
|
|
|
983
|
+ 'style': cell_style
|
|
|
984
|
+ }
|
|
|
985
|
+
|
|
|
986
|
+ if cell_word_style:
|
|
|
987
|
+ cell_data['word_style'] = cell_word_style
|
|
|
988
|
+
|
|
|
989
|
+ if cell_width is not None:
|
|
|
990
|
+ cell_data['width'] = round(cell_width, 2)
|
|
|
991
|
+
|
|
|
992
|
+ cells_data.append(cell_data)
|
|
951
|
993
|
|
|
952
|
|
- # 更新列偏移
|
|
953
|
994
|
col_offset += colspan
|
|
954
|
995
|
|
|
955
|
996
|
# 构建行数据
|
|
956
|
|
- row_data = {
|
|
957
|
|
- 'cells': cells_data
|
|
958
|
|
- }
|
|
959
|
|
-
|
|
960
|
|
- # 添加行高
|
|
|
997
|
+ row_data = {'cells': cells_data}
|
|
961
|
998
|
if row_height is not None:
|
|
962
|
999
|
row_data['height'] = round(row_height, 2)
|
|
963
|
1000
|
|
|
964
|
1001
|
rows_data.append(row_data)
|
|
965
|
1002
|
|
|
966
|
|
- # 第二遍:更新 rowspan 值
|
|
967
|
|
- for col_idx, info in vmerge_tracking.items():
|
|
968
|
|
- start_row = info['start_row']
|
|
969
|
|
- count = info['count']
|
|
970
|
|
- # 找到起始行的单元格并更新 rowspan
|
|
971
|
|
- if start_row < len(rows_data):
|
|
972
|
|
- for cell in rows_data[start_row]['cells']:
|
|
973
|
|
- # 简化:假设 col_idx 对应 cells 索引(实际可能需要考虑 colspan)
|
|
974
|
|
- if 'rowspan' in cell and cell['rowspan'] > 0:
|
|
975
|
|
- cell['rowspan'] = count
|
|
976
|
|
- break
|
|
977
|
|
-
|
|
978
|
1003
|
return {
|
|
979
|
1004
|
'rows': rows_data,
|
|
980
|
1005
|
'col_widths': [round(w, 2) for w in col_widths] if col_widths else None
|