- 创建 scripts/ 目录作为核心代码根目录 - 移动 core/, readers/, utils/ 到 scripts/ 下 - 移动 config.py, lyxy_document_reader.py 到 scripts/ - 移动 encoding_detection.py 到 scripts/utils/ - 更新 pyproject.toml 中的入口点路径和 pytest 配置 - 更新所有内部导入语句为 scripts.* 模块 - 更新 README.md 目录结构说明 - 更新 openspec/config.yaml 添加目录结构说明 - 删除无用的 main.py 此变更使项目结构更清晰,便于区分核心代码与测试、文档等支撑文件。
128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
"""使用 python-pptx 库解析 PPTX 文件"""
|
|
|
|
from typing import Any, List, Optional, Tuple
|
|
|
|
from scripts.core import build_markdown_table, flush_list_stack
|
|
|
|
|
|
def parse(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 库未安装"
|
|
|
|
_A_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
|
|
|
def extract_formatted_text(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(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(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)
|
|
|
|
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:
|
|
flush_list_stack(list_stack, md_content)
|
|
|
|
table_md = convert_table_to_md(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_NS) is not None
|
|
or pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None
|
|
)
|
|
|
|
if is_list:
|
|
level = para.level
|
|
|
|
while len(list_stack) <= level:
|
|
list_stack.append("")
|
|
|
|
text = extract_formatted_text(para.runs)
|
|
if text:
|
|
is_ordered = (
|
|
pPr is not None
|
|
and pPr.find(".//a:buAutoNum", namespaces=_A_NS) 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:
|
|
flush_list_stack(list_stack, md_content)
|
|
|
|
text = extract_formatted_text(para.runs)
|
|
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"python-pptx 解析失败: {str(e)}"
|