#!/usr/bin/env python3 """PPTX 文件解析模块,提供三种解析方法。""" import re import xml.etree.ElementTree as ET import zipfile from typing import Any, List, Optional, Tuple from common import ( build_markdown_table, flush_list_stack, parse_with_docling, parse_with_markitdown, ) def parse_pptx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 docling 库解析 PPTX 文件""" return parse_with_docling(file_path) def parse_pptx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 MarkItDown 库解析 PPTX 文件""" return parse_with_markitdown(file_path) def extract_formatted_text_pptx(runs: List[Any]) -> str: """从 PPTX 文本运行中提取带有格式的文本""" result = [] for run in runs: if not run.text: continue text = run.text font = run.font is_bold = getattr(font, "bold", False) or False is_italic = getattr(font, "italic", False) or False if is_bold and is_italic: text = f"***{text}***" elif is_bold: text = f"**{text}**" elif is_italic: text = f"*{text}*" result.append(text) return "".join(result).strip() def convert_table_to_md_pptx(table: Any) -> str: """将 PPTX 表格转换为 Markdown 格式""" rows_data = [] for row in table.rows: row_data = [] for cell in row.cells: cell_content = [] for para in cell.text_frame.paragraphs: text = extract_formatted_text_pptx(para.runs) if text: cell_content.append(text) cell_text = " ".join(cell_content).strip() row_data.append(cell_text if cell_text else "") rows_data.append(row_data) return build_markdown_table(rows_data) def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 python-pptx 库解析 PPTX 文件""" try: from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE except ImportError: return None, "python-pptx 库未安装" try: prs = Presentation(file_path) md_content = [] for slide_num, slide in enumerate(prs.slides, 1): md_content.append(f"\n## Slide {slide_num}\n") list_stack = [] for shape in slide.shapes: if shape.shape_type == MSO_SHAPE_TYPE.PICTURE: continue if hasattr(shape, "has_table") and shape.has_table: if list_stack: md_content.append( "\n" + "\n".join([x for x in list_stack if x]) + "\n" ) list_stack.clear() table_md = convert_table_to_md_pptx(shape.table) md_content.append(table_md) if hasattr(shape, "text_frame"): for para in shape.text_frame.paragraphs: pPr = para._element.pPr is_list = False if pPr is not None: is_list = ( para.level > 0 or pPr.find( ".//a:buChar", namespaces={ "a": "http://schemas.openxmlformats.org/drawingml/2006/main" }, ) is not None or pPr.find( ".//a:buAutoNum", namespaces={ "a": "http://schemas.openxmlformats.org/drawingml/2006/main" }, ) is not None ) if is_list: level = para.level while len(list_stack) <= level: list_stack.append("") text = extract_formatted_text_pptx(para.runs) if text: pPr = para._element.pPr is_ordered = ( pPr is not None and pPr.find( ".//a:buAutoNum", namespaces={ "a": "http://schemas.openxmlformats.org/drawingml/2006/main" }, ) is not None ) marker = "1. " if is_ordered else "- " indent = " " * level list_stack[level] = f"{indent}{marker}{text}" for i in range(len(list_stack)): if list_stack[i]: md_content.append(list_stack[i] + "\n") list_stack[i] = "" else: if list_stack: md_content.append( "\n" + "\n".join([x for x in list_stack if x]) + "\n" ) list_stack.clear() text = extract_formatted_text_pptx(para.runs) if text: md_content.append(f"{text}\n") if list_stack: md_content.append("\n" + "\n".join([x for x in list_stack if x]) + "\n") list_stack.clear() md_content.append("---\n") content = "\n".join(md_content) if not content.strip(): return None, "文档为空" return content, None except Exception as e: return None, f"python-pptx 解析失败: {str(e)}" def parse_pptx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 XML 原生解析 PPTX 文件""" pptx_namespace = { "a": "http://schemas.openxmlformats.org/drawingml/2006/main", "p": "http://schemas.openxmlformats.org/presentationml/2006/main", "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", } def extract_text_with_formatting_xml(text_elem: Any, namespaces: dict) -> str: result = [] runs = text_elem.findall(".//a:r", namespaces=namespaces) for run in runs: t_elem = run.find(".//a:t", namespaces=namespaces) if t_elem is None or not t_elem.text: continue text = t_elem.text rPr = run.find(".//a:rPr", namespaces=namespaces) is_bold = False is_italic = False if rPr is not None: is_bold = rPr.find(".//a:b", namespaces=namespaces) is not None is_italic = rPr.find(".//a:i", namespaces=namespaces) is not None if is_bold and is_italic: text = f"***{text}***" elif is_bold: text = f"**{text}**" elif is_italic: text = f"*{text}*" result.append(text) return "".join(result).strip() if result else "" def convert_table_to_md_xml(table_elem: Any, namespaces: dict) -> str: rows = table_elem.findall(".//a:tr", namespaces=namespaces) if not rows: return "" rows_data = [] for row in rows: cells = row.findall(".//a:tc", namespaces=namespaces) row_data = [] for cell in cells: cell_text = extract_text_with_formatting_xml(cell, namespaces) if cell_text: cell_text = cell_text.replace("\n", " ").replace("\r", "") row_data.append(cell_text if cell_text else "") rows_data.append(row_data) return build_markdown_table(rows_data) def is_list_item_xml(p_elem: Any, namespaces: dict) -> Tuple[bool, bool]: if p_elem is None: return False, False pPr = p_elem.find(".//a:pPr", namespaces=namespaces) if pPr is None: return False, False buChar = pPr.find(".//a:buChar", namespaces=namespaces) if buChar is not None: return True, False buAutoNum = pPr.find(".//a:buAutoNum", namespaces=namespaces) if buAutoNum is not None: return True, True return False, False def get_indent_level_xml(p_elem: Any, namespaces: dict) -> int: if p_elem is None: return 0 pPr = p_elem.find(".//a:pPr", namespaces=namespaces) if pPr is None: return 0 lvl = pPr.get("lvl") return int(lvl) if lvl else 0 try: md_content = [] with zipfile.ZipFile(file_path) as zip_file: slide_files = [ f for f in zip_file.namelist() if re.match(r"ppt/slides/slide\d+\.xml$", f) ] slide_files.sort( key=lambda f: int(re.search(r"slide(\d+)\.xml$", f).group(1)) ) for slide_idx, slide_file in enumerate(slide_files, 1): md_content.append("\n## Slide {}\n".format(slide_idx)) with zip_file.open(slide_file) as slide_xml: slide_root = ET.parse(slide_xml).getroot() tx_bodies = slide_root.findall( ".//p:sp/p:txBody", namespaces=pptx_namespace ) tables = slide_root.findall(".//a:tbl", namespaces=pptx_namespace) for table in tables: table_md = convert_table_to_md_xml(table, pptx_namespace) if table_md: md_content.append(table_md) for tx_body in tx_bodies: paragraphs = tx_body.findall( ".//a:p", namespaces=pptx_namespace ) list_stack = [] for para in paragraphs: is_list, is_ordered = is_list_item_xml(para, pptx_namespace) if is_list: level = get_indent_level_xml(para, pptx_namespace) while len(list_stack) <= level: list_stack.append("") text = extract_text_with_formatting_xml( para, pptx_namespace ) if text: marker = "1. " if is_ordered else "- " indent = " " * level list_stack[level] = f"{indent}{marker}{text}" for i in range(len(list_stack)): if list_stack[i]: md_content.append(list_stack[i] + "\n") list_stack[i] = "" else: if list_stack: flush_list_stack(list_stack, md_content) text = extract_text_with_formatting_xml( para, pptx_namespace ) if text: md_content.append(f"{text}\n") if list_stack: flush_list_stack(list_stack, md_content) md_content.append("---\n") content = "\n".join(md_content) if not content.strip(): return None, "文档为空" return content, None except Exception as e: return None, f"XML 解析失败: {str(e)}"