diff --git a/temp/scripts/README.md b/temp/scripts/README.md index a2eae25..e033cae 100644 --- a/temp/scripts/README.md +++ b/temp/scripts/README.md @@ -1,103 +1,23 @@ # Document Parser 使用说明 -一个模块化的文档解析器,支持将 DOCX、PPTX、XLSX 和 PDF 文件转换为 Markdown 格式。 +模块化文档解析器,将 DOCX、PPTX、XLSX、PDF 文件转换为 Markdown 格式。 -## 概述 +每种文档类型配备多个解析器,按优先级依次尝试,前一个失败自动回退到下一个。不安装任何第三方库时,DOCX/PPTX/XLSX 仍可通过内置 XML 原生解析工作(PDF 至少需要 pypdf)。 -该解析器按优先级尝试多种解析方法,确保最大兼容性: - -1. **Docling** (docling.document_converter) - 通用解析方案,优先覆盖 DOCX/PPTX/XLSX/PDF 并内置 OCR 能力 -2. **pypandoc-binary** (DOCX 专用,内置 Pandoc) - 生成结构化 Markdown -3. **MarkItDown** (微软官方库) - 推荐使用,格式规范 -4. **python-docx / python-pptx / pandas** (成熟的 Python 库) - 输出最详细 -5. **unstructured / pypdf** (成熟的 PDF 库) - PDF 专用 -6. **XML 原生解析** (备选方案) - 无需依赖 - -脚本会按照上述优先级依次尝试各解析器,前面的失败后自动回退到下一个,因此建议安装该文档类型对应的所有解析器依赖,以获得最佳兼容性。 - -### 特性 - -- 支持 DOCX、PPTX、XLSX 和 PDF 格式 -- 自动检测文件类型和有效性 -- 保留文本格式(粗体、斜体、下划线) -- Docling 作为第一优先解析器,单一依赖即可覆盖全部格式并自动调用 OCR -- 提取表格并转换为 Markdown 格式 -- 提取列表并保留层级结构 -- 多种输出模式(字数、行数、标题、搜索等) -- 内容过滤和规范化 -- 模块化设计,易于维护和扩展 - -## 文件结构 - -``` -scripts/ -├── common.py # 公共函数和常量 -├── docx_parser.py # DOCX 文件解析 -├── pptx_parser.py # PPTX 文件解析 -├── xlsx_parser.py # XLSX 文件解析 -├── pdf_parser.py # PDF 文件解析 -├── parser.py # 命令行入口 -└── README.md # 本文档 -``` - -## 依赖安装 - -脚本基于标准 Python 环境运行(Python 3.6+),使用 `pip` 安装依赖。 - -由于每种文档类型有多个解析器按优先级依次尝试,建议安装该类型对应的**所有**解析器依赖,这样当高优先级解析器失败时可以自动回退到下一个。 - -### DOCX 依赖 - -解析优先级:Docling → pypandoc-binary → MarkItDown → python-docx → XML 原生 +## 快速开始 ```bash -pip install docling pypandoc-binary "markitdown[docx]" python-docx +# 最简运行(XML 原生解析,无需安装依赖) +python parser.py report.docx + +# 安装推荐依赖后运行 +pip install "markitdown[docx]" +python parser.py report.docx + +# 使用 uv 一键运行(自动安装依赖,无需手动 pip install) +uv run --with "markitdown[docx]" parser.py report.docx ``` -### PPTX 依赖 - -解析优先级:Docling → MarkItDown → python-pptx → XML 原生 - -```bash -pip install docling "markitdown[pptx]" python-pptx -``` - -### XLSX 依赖 - -解析优先级:Docling → MarkItDown → pandas → XML 原生 - -```bash -pip install docling "markitdown[xlsx]" pandas tabulate -``` - -### PDF 依赖 - -解析优先级:Docling → MarkItDown → unstructured → pypdf - -```bash -pip install docling "markitdown[pdf]" unstructured pypdf -``` - -### 安装所有依赖 - -如果需要处理全部文档类型,可以一次性安装所有解析器依赖: - -```bash -pip install docling pypandoc-binary "markitdown[docx,pptx,xlsx,pdf]" python-docx python-pptx pandas tabulate unstructured pypdf -``` - -> 注意:MarkItDown 需要按文档类型安装对应的可选依赖,如 `markitdown[docx]`、`markitdown[pptx]`、`markitdown[xlsx]`、`markitdown[pdf]`,直接安装 `markitdown` 不会包含任何格式的解析支持。 - -### 仅 XML 原生解析(无需安装依赖) - -如果不安装任何第三方库,脚本仍可通过内置的 XML 原生解析方式工作(DOCX/PPTX/XLSX),但输出格式和质量相对有限。 - -### Docling 说明 - -- Docling 是当前的默认第一优先级解析器,单一依赖即可获得统一输出。 -- 首次运行会自动下载 OCR/视觉模型到缓存目录,需保持网络连通。 -- 脚本会在 Docling 失败时自动回退至其他方案。 - ## 命令行用法 ### 基本语法 @@ -106,363 +26,92 @@ pip install docling pypandoc-binary "markitdown[docx,pptx,xlsx,pdf]" python-docx python parser.py [options] ``` -### 必需参数 +`file_path` 为 DOCX、PPTX、XLSX 或 PDF 文件路径(相对或绝对路径)。不带任何选项时输出完整 Markdown 内容。 -- `file_path`: DOCX、PPTX、XLSX 或 PDF 文件的路径(相对或绝对路径) +### 参数说明 -### 可选参数(互斥组,一次只能使用一个) +以下参数互斥,一次只能使用一个: -| 参数 | 短选项 | 长选项 | 说明 | -|------|---------|---------|------| -| `-c` | `--count` | 返回解析后的 markdown 文档的总字数 | -| `-l` | `--lines` | 返回解析后的 markdown 文档的总行数 | -| `-t` | `--titles` | 返回解析后的 markdown 文档的标题行(1-6级) | -| `-tc ` | `--title-content ` | 提取指定标题及其下级内容(不包含#号) | -| `-s ` | `--search ` | 使用正则表达式搜索文档,返回所有匹配结果(用---分隔) | +| 短选项 | 长选项 | 说明 | +|--------|--------|------| +| `-c` | `--count` | 输出解析后文档的总字符数(不含换行符) | +| `-l` | `--lines` | 输出解析后文档的总行数 | +| `-t` | `--titles` | 输出所有标题行(1-6 级,含 `#` 前缀) | +| `-tc ` | `--title-content ` | 提取指定标题及其下级内容(`name` 不包含 `#` 号) | +| `-s ` | `--search ` | 使用正则表达式搜索文档,返回匹配结果 | -### 搜索上下文参数 +搜索辅助参数(与 `-s` 配合使用): -- `-n ` / `--context `: 与 `-s` 配合使用,指定每个检索结果包含的前后行数(默认:2) +| 短选项 | 长选项 | 说明 | +|--------|--------|------| +| `-n ` | `--context ` | 每个匹配结果包含的前后非空行数(默认:2) | -## 使用示例 +### 退出码 -### 1. 输出完整 Markdown 内容 +| 退出码 | 含义 | +|--------|------| +| `0` | 解析成功 | +| `1` | 错误(文件不存在、格式无效、所有解析器失败、标题未找到、正则无效或无匹配) | + +### 使用示例 + +**输出完整 Markdown:** ```bash -# 解析 DOCX -python parser.py report.docx - -# 解析 PDF -python parser.py report.pdf - -# 解析 PPTX -python parser.py presentation.pptx - -# 解析 XLSX -python parser.py data.xlsx - -# 输出到文件 -python parser.py report.docx > output.md +python parser.py report.docx # 输出到终端 +python parser.py report.docx > output.md # 重定向到文件 ``` -### 2. 统计文档信息 +**统计信息(`-c` / `-l`):** + +输出单个数字,适合管道处理。 ```bash -# 统计字数 -python parser.py report.docx -c -python parser.py report.pdf -c +$ python parser.py report.docx -c +8500 -# 统计行数 -python parser.py report.docx -l -python parser.py report.pdf -l +$ python parser.py report.docx -l +215 ``` -### 3. 提取标题 +**提取标题(`-t`):** + +每行一个标题,保留 `#` 前缀和层级。PDF 通常不包含语义化标题层级。 ```bash -# 提取所有标题 -python parser.py report.docx -t -python parser.py report.pdf -t - -# 输出示例(DOCX): +$ python parser.py report.docx -t # 第一章 概述 ## 1.1 背景 ## 1.2 目标 # 第二章 实现 - -# 输出示例(PDF - 注意:PDF 通常不包含明确的标题层级): -[内容提取成功,但 PDF 可能缺乏清晰的标题结构] ``` -### 4. 提取特定标题内容 +**提取标题内容(`-tc`):** + +输出指定标题及其下级内容。如果文档中有多个同名标题,用 `---` 分隔。每段输出包含上级标题链。 ```bash -# 提取特定章节 -python parser.py report.docx -tc "第一章" -python parser.py report.pdf -tc "第一章" - -# 输出该标题及其所有子内容 +$ python parser.py report.docx -tc "1.1 背景" +# 第一章 概述 +## 1.1 背景 +这是背景的详细内容... ``` -### 5. 搜索文档内容 +**搜索(`-s`):** + +支持 Python 正则表达式语法。多个匹配结果用 `---` 分隔。`-n` 控制上下文行数。 ```bash -# 搜索关键词 -python parser.py report.docx -s "测试" -python parser.py report.pdf -s "测试" - -# 使用正则表达式 -python parser.py report.docx -s "章节\s+\d+" -python parser.py report.pdf -s "章节\s+\d+" - -# 带上下文搜索(前后各2行) -python parser.py report.docx -s "重要内容" -n 2 -python parser.py report.pdf -s "重要内容" -n 2 - -# 输出示例: ---- -这是重要内容的前两行 -**重要内容** -这是重要内容后两行 +$ python parser.py report.docx -s "测试" -n 1 +上一行内容 +包含**测试**关键词的行 +下一行内容 --- +另一处上一行 +另一处**测试**内容 +另一处下一行 ``` -## 使用 uv 运行 - -如果使用 [uv](https://github.com/astral-sh/uv) 作为 Python 环境管理工具,可以通过 `uv run --with` 自动安装依赖并运行脚本,无需手动 `pip install`。 - -### 基本用法 - -```bash -# 无依赖运行(仅 XML 原生解析) -uv run parser.py file.docx - -# 指定依赖运行 -uv run --with docling parser.py file.docx -``` - -### 按文档类型运行(安装所有解析器依赖) - -```bash -# DOCX - 安装所有 DOCX 解析器 -uv run --with docling --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py report.docx - -# PPTX - 安装所有 PPTX 解析器 -uv run --with docling --with "markitdown[pptx]" --with python-pptx parser.py presentation.pptx - -# XLSX - 安装所有 XLSX 解析器 -uv run --with docling --with "markitdown[xlsx]" --with pandas --with tabulate parser.py data.xlsx - -# PDF - 安装所有 PDF 解析器 -uv run --with docling --with "markitdown[pdf]" --with unstructured --with pypdf parser.py report.pdf -``` - -### 安装所有依赖运行 - -```bash -uv run --with docling --with pypandoc-binary --with "markitdown[docx,pptx,xlsx,pdf]" --with python-docx --with python-pptx --with pandas --with tabulate --with unstructured --with pypdf parser.py file.pdf -``` - -### 批量处理 - -```bash -# Linux/Mac -for file in *.docx; do - uv run --with docling --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py "$file" > "${file%.docx}.md" -done - -# Windows PowerShell -Get-ChildItem *.docx | ForEach-Object { - uv run --with docling --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py $_.FullName > ($_.BaseName + ".md") -} -``` - -## 解析器对比 - -### DOCX 解析器 - -DOCX 文件会按以下优先级依次尝试解析: - -1. Docling -2. pypandoc-binary -3. MarkItDown -4. python-docx -5. XML 原生 - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | • 单一依赖覆盖所有 Office/PDF 格式
• 自动带 OCR,复杂文档召回率高
• 输出 Markdown 结构稳定 | • 首次运行需下载较大的模型
• 运行时内存占用相对更高 | • 需要"一键完成"解析
• 需要 OCR/多模态支持 | -| **pypandoc-binary** | • 自带 Pandoc,可直接使用
• 输出 Markdown 结构整洁
• 错误信息清晰易排查 | • 仅适用于 DOCX
• 依赖包体积较大 | • 需要标准化 Markdown 输出
• Docling 不可用时的首选 | -| **MarkItDown** | • 格式规范
• 微软官方支持
• 兼容性好 | • 需要安装
• 输出较简洁 | • 需要标准格式输出
• 自动化文档处理 | -| **python-docx** | • 输出最详细
• 保留完整结构
• 支持复杂样式 | • 需要安装
• 可能包含多余空行 | • 需要精确控制输出
• 分析文档结构 | -| **XML 原生** | • 无需依赖
• 运行速度快
• 输出原始内容 | • 格式可能不统一
• 样式处理有限 | • 依赖不可用时
• 快速提取内容 | - -### PPTX 解析器 - -PPTX 文件会按以下优先级依次尝试解析:Docling → MarkItDown → python-pptx → XML 原生。 - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | • 解析幻灯片文本、表格与图片 OCR
• 自动生成统一 Markdown,包含分页分隔符 | • 需要下载模型
• 细节控制少 | • 需要一次性转换全部幻灯片
• 有图片或扫描件的 PPTX | -| **MarkItDown** | • 格式规范
• 自动添加 Slide 分隔
• 输出简洁 | • 需要安装
• 详细度较低 | • 快速预览幻灯片
• 提取主要内容 | -| **python-pptx** | • 输出最详细
• 保留完整结构
• 支持层级列表 | • 需要安装
• 依赖私有 API | • 需要完整内容
• 分析演示结构 | -| **XML 原生** | • 无需依赖
• 结构化输出
• 运行速度快 | • 格式可能不统一
• 幻灯片分组简单 | • 依赖不可用时
• 结构化提取 | - -### XLSX 解析器 - -XLSX 文件会按以下优先级依次尝试解析:Docling → MarkItDown → pandas → XML 原生。 - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | • 单次遍历导出全部工作表为 Markdown
• 自动处理合并单元格/图像 OCR | • 需要下载模型
• 对极大体积表可能较慢 | • 快速完成全表转 Markdown
• 含扫描图片的表格 | -| **MarkItDown** | • 格式规范
• 支持多工作表
• 输出简洁 | • 需要安装
• 详细度较低 | • 快速预览表格
• 提取主要内容 | -| **pandas** | • 功能强大
• 支持复杂表格
• 数据处理灵活 | • 需要安装
• 依赖较多 | • 数据分析
• 复杂表格处理 | -| **XML 原生** | • 无需依赖
• 运行速度快
• 支持所有单元格类型 | • 格式可能不统一
• 无数据处理能力 | • 依赖不可用时
• 快速提取内容 | - -### PDF 解析器 - -PDF 文件会按以下优先级依次尝试解析:Docling → MarkItDown → unstructured → pypdf。 - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | • 内置 RapidOCR,可处理扫描版 PDF
• 输出结构化 Markdown,包含表格/图片占位 | • 模型下载体积大
• OCR 耗时较长 | • 需要 OCR、表格/图片识别
• 多语言 PDF | -| **MarkItDown** | • 格式规范
• 微软官方支持
• 兼容性好 | • 需要安装 `markitdown[pdf]`
• 输出较简洁 | • 需要标准格式输出
• 自动化文档处理 | -| **unstructured** | • 功能强大
• 支持表格提取
• 文本组织性好 | • 需要安装
• 可能包含页码标记 | • 需要完整内容
• 分析文档结构 | -| **pypdf** | • 轻量级
• 速度快
• 安装简单 | • 需要安装
• 功能相对简单 | • 快速提取内容
• 简单文本提取 | - -## 输出格式 - -### Markdown 输出结构 - -```markdown -# 标题 1 (一级) - -正文段落 - -## 标题 2 (二级) - -- 列表项 1 -- 列表项 2 - -1. 有序列表项 1 -2. 有序列表项 2 - -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | - -**粗体文本** *斜体文本* 下划线文本 -``` - -### PPTX 特有格式 - -```markdown -## Slide 1 - -幻灯片 1 的内容 - -## Slide 2 - -表格内容 - -幻灯片 2 的内容 - ---- -``` - -### XLSX 特有格式 - -```markdown -# Excel数据转换结果 - -来源: /path/to/file.xlsx - -## Sheet1 - -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | -``` - -### PDF 特有格式 - -```markdown -[PDF 文件的纯文本内容,按段落提取] - -中电信粤亿迅〔2023〕3号 - -关于印发关于印发关于印发关于印发《《《《广东亿迅科技有限公司员工 - -[注:PDF 通常不包含明确的标题层级结构,内容以文本流形式呈现] -``` - -### 标题格式 - -- 标题使用 Markdown 井号语法:`#` 到 `######`(1-6级) -- 标题名称不包含井号 -- 段落通过空行分隔 - -### 表格格式 - -```markdown -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | -``` - -### 列表格式 - -- 无序列表:使用 `-` 前缀 -- 有序列表:使用 `1.` 前缀 -- 支持多层缩进(使用空格) - -## 内容处理 - -### 自动处理 - -1. **图片移除**:自动删除 Markdown 图片语法 -2. **空行规范化**:合并连续空行为单个空行 -3. **样式标签过滤**:移除 HTML span 标签 -4. **RGB 颜色过滤**:移除颜色代码行 - -### 过滤规则(filter_markdown_content) - -- 保留:文本、表格、列表、基本格式 -- 移除: - - HTML 注释 (``) - - Markdown 图片 (`![alt](url)`) - - HTML 图片标签 (``, ``) - - 媒体链接 (`[text](file.ext)`) - - RGB 颜色代码 (`R:255 G:255 B:255`) -- 标准化:多余空格合并为单个空格 - -## 错误处理 - -### 文件验证 - -```bash -# 文件不存在 -错误: 文件不存在: missing.docx - -# 无效格式 -错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: invalid.txt -``` - -### 解析器回退 - -脚本按优先级尝试解析器,如果失败则自动尝试下一个: - -``` -所有解析方法均失败: -- Docling: 库未安装 -- pypandoc-binary: 库未安装 -- MarkItDown: 库未安装 -- python-docx: 解析失败: ... -- XML 原生解析: document.xml 不存在或无法访问 -``` - -**PDF 回退示例**: - -``` -所有解析方法均失败: -- Docling: 库未安装 -- MarkItDown: MarkItDown 解析失败: ... -- unstructured: unstructured 库未安装 -- pypdf: pypdf 库未安装 -``` - -### 搜索错误 - -```bash -# 无效正则 -错误: 正则表达式无效或未找到匹配: '[invalid' - -# 标题未找到 -错误: 未找到标题 '不存在的标题' -``` - -## 高级用法 - ### 批量处理 ```bash @@ -480,40 +129,285 @@ Get-ChildItem *.docx | ForEach-Object { ### 管道使用 ```bash -# 进一步处理 Markdown 输出 +# 过滤包含关键词的行 python parser.py report.docx | grep "重要" > important.md -# 统计处理 -python parser.py report.docx -l | awk '{print $1}' +# 统计含表格行数 +python parser.py data.xlsx | grep -c "^|" ``` +## 安装 + +脚本基于 Python 3.6+ 运行。每种文档类型有多个解析器按优先级依次尝试,建议安装对应类型的**所有**依赖以获得最佳兼容性。也可以只安装部分依赖,脚本会自动选择可用的解析器。 + +### DOCX + +优先级:Docling → unstructured → pypandoc-binary → MarkItDown → python-docx → XML 原生 + +```bash +# pip +pip install docling "unstructured[docx]" markdownify pypandoc-binary "markitdown[docx]" python-docx + +# uv(一键运行,无需预安装) +uv run --with docling --with "unstructured[docx]" --with markdownify --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py report.docx +``` + +### PPTX + +优先级:Docling → unstructured → MarkItDown → python-pptx → XML 原生 + +```bash +# pip +pip install docling "unstructured[pptx]" markdownify "markitdown[pptx]" python-pptx + +# uv +uv run --with docling --with "unstructured[pptx]" --with markdownify --with "markitdown[pptx]" --with python-pptx parser.py presentation.pptx +``` + +### XLSX + +优先级:Docling → unstructured → MarkItDown → pandas → XML 原生 + +```bash +# pip +pip install docling "unstructured[xlsx]" markdownify "markitdown[xlsx]" pandas tabulate + +# uv +uv run --with docling --with "unstructured[xlsx]" --with markdownify --with "markitdown[xlsx]" --with pandas --with tabulate parser.py data.xlsx +``` + +### PDF + +优先级:Docling → unstructured → MarkItDown → pypdf + +```bash +# pip - 基础文本提取(使用 fast 策略,无需 OCR) +pip install docling "unstructured[pdf]"" markdownify "markitdown[pdf]" pypdf + +# pip - OCR 版面分析(使用 hi_res 策略 + PaddleOCR) +pip install docling "unstructured[pdf]" unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes markdownify "markitdown[pdf]" pypdf + +# uv - 基础文本提取 +uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf + +# uv - OCR 版面分析 +uv run --with docling --with "unstructured[pdf]" --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf +``` + +> PDF 无内置 XML 原生解析,至少需要安装 pypdf。unstructured 的 `hi_res` 策略需要额外安装 `unstructured-paddleocr`、`paddlepaddle==2.6.2`、`ml-dtypes`,不可用时自动回退 `fast` 策略。PaddlePaddle 必须锁定 2.x 版本,3.x 在 Windows 上有 OneDNN 兼容问题。 +> + +### 安装所有依赖 + +```bash +# pip - 基础文本提取(不包含 PDF OCR) +pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify pypandoc-binary "markitdown[docx,pptx,xlsx]" python-docx python-pptx pandas tabulate pypdf + +# pip - 完整版(包含 PDF OCR) +pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes pypandoc-binary "markitdown[docx,pptx,xlsx,pdf]" python-docx python-pptx pandas tabulate pypdf + +# uv - 基础文本提取 +uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with pypandoc-binary --with "markitdown[docx,pptx,xlsx]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx + +# uv - 完整版(包含 PDF OCR) +uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with pypandoc-binary --with "markitdown[docx,pptx,xlsx,pdf]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx +``` + +### 依赖说明 + +**MarkItDown**:需要按文档类型安装可选依赖,直接 `pip install markitdown` 不包含任何格式支持。 + +```bash +pip install "markitdown[docx]" # DOCX +pip install "markitdown[pptx]" # PPTX +pip install "markitdown[xlsx]" # XLSX +pip install "markitdown[pdf]" # PDF +pip install "markitdown[docx,pptx,xlsx,pdf]" # 全部 +``` + +**Docling**:首次运行会自动下载 OCR/视觉模型到缓存目录,需保持网络连通。 + +**unstructured**:需同时安装 `markdownify`。支持按文档类型安装特定 extras 以减少依赖量: + +- `unstructured[docx]` - DOCX 处理(仅需 `python-docx`) +- `unstructured[pptx]` - PPTX 处理(仅需 `python-pptx`) +- `unstructured[xlsx]` - XLSX 处理(需 `openpyxl`、`xlrd`、`pandas` 等) +- `unstructured` - 基础包(用于 PDF fast 策略) +- `unstructured[all-docs]` - 所有文档类型(包含大量不必要的 OCR/视觉依赖) + +**PaddleOCR**:不能用 `paddleocr` 代替 `unstructured-paddleocr`,unstructured 查找的模块名是 `unstructured_paddleocr`。 + +## 输出格式 + +### Markdown 文档结构 + +无选项时输出完整 Markdown,包含以下元素: + +```markdown +# 一级标题 + +正文段落 + +## 二级标题 + +- 无序列表项 +- 无序列表项 + +1. 有序列表项 +2. 有序列表项 + +| 列1 | 列2 | 列3 | +|------|------|------| +| 数据1 | 数据2 | 数据3 | + +**粗体** *斜体* 下划线 +``` + +### 各格式特有结构 + +**PPTX** — 每张幻灯片以 `## Slide N` 为标题,幻灯片之间以 `---` 分隔: + +```markdown +## Slide 1 + +幻灯片 1 的内容 + +--- + +## Slide 2 + +幻灯片 2 的内容 + +--- +``` + +**XLSX** — 以 `## SheetName` 区分工作表,数据以 Markdown 表格呈现: + +```markdown +# Excel数据转换结果 + +## Sheet1 + +| 列1 | 列2 | 列3 | +|------|------|------| +| 数据1 | 数据2 | 数据3 | + +## Sheet2 + +| 列A | 列B | +|------|------| +| 值1 | 值2 | +``` + +**PDF** — 纯文本流,通常不包含语义化标题层级(PDF 是版面描述格式,标题只是视觉样式)。使用 Docling 或 unstructured hi_res 策略可通过版面分析识别部分标题,但准确度取决于排版质量。 + +### 内容自动处理 + +输出前会自动进行以下处理: + +| 处理 | 说明 | +|------|------| +| 图片移除 | 删除 `![alt](url)` 语法 | +| 空行规范化 | 连续多个空行合并为一个 | +| RGB 噪声过滤 | 移除 `R:255 G:128 B:0` 格式的颜色值行 | +| 页码噪声过滤 | 移除 `— 3 —` 格式的页码行 | +| 页眉/页脚过滤 | unstructured 解析器自动跳过 Header/Footer 元素 | + +## 错误处理 + +### 错误消息 + +```bash +# 文件不存在 +$ python parser.py missing.docx +错误: 文件不存在: missing.docx + +# 格式无效 +$ python parser.py readme.txt +错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: readme.txt + +# 所有解析器失败(DOCX 示例) +$ python parser.py report.docx +所有解析方法均失败: +- Docling: docling 库未安装 +- unstructured: unstructured 库未安装 +- pypandoc-binary: pypandoc-binary 库未安装 +- MarkItDown: MarkItDown 库未安装 +- python-docx: python-docx 库未安装 +- XML 原生解析: document.xml 不存在或无法访问 + +# 标题未找到 +$ python parser.py report.docx -tc "不存在的标题" +错误: 未找到标题 '不存在的标题' + +# 无效正则或无匹配 +$ python parser.py report.docx -s "[invalid" +错误: 正则表达式无效或未找到匹配: '[invalid' +``` + +### 解析器回退机制 + +脚本按优先级依次尝试各解析器。每个解析器失败后记录原因(库未安装 / 解析失败 / 文档为空),然后自动尝试下一个。全部失败时输出汇总信息并以退出码 1 退出。 + +## 解析器对比 + +### DOCX + +| 解析器 | 优点 | 缺点 | 适用场景 | +|---------|------|--------|---------| +| **Docling** | 单一依赖覆盖全格式;自动 OCR;输出结构稳定 | 首次需下载模型;内存占用较高 | 一键解析;需要 OCR | +| **unstructured** | 元素类型感知;自动过滤噪声;HTML 表格转 Markdown | 需 `unstructured[docx]` / `[pptx]` / `[xlsx]` + `markdownify` | 结构化输出;表格转换 | +| **pypandoc-binary** | 自带 Pandoc;输出整洁;错误信息清晰 | 仅 DOCX;包体积大 | 标准化 Markdown | +| **MarkItDown** | 微软官方;格式规范 | 输出简洁 | 标准格式;自动化处理 | +| **python-docx** | 输出最详细;保留完整结构;支持复杂样式 | 可能含多余空行 | 精确控制输出 | +| **XML 原生** | 无需依赖;速度快 | 样式处理有限 | 依赖不可用时兜底 | + +### PPTX + +| 解析器 | 优点 | 缺点 | 适用场景 | +|---------|------|--------|---------| +| **Docling** | 文本/表格/图片 OCR;统一 Markdown | 需下载模型 | 一次性转换;含图片的 PPTX | +| **unstructured** | 元素感知;过滤 RGB 噪声;表格转换 | 需 `unstructured[pptx]` + `markdownify` | 结构化输出 | +| **MarkItDown** | 自动 Slide 分隔;简洁 | 详细度低 | 快速预览 | +| **python-pptx** | 输出最详细;支持层级列表 | 依赖私有 API | 完整内容提取 | +| **XML 原生** | 无需依赖;速度快 | 分组简单 | 依赖不可用时兜底 | + +### XLSX + +| 解析器 | 优点 | 缺点 | 适用场景 | +|---------|------|--------|---------| +| **Docling** | 全表导出;处理合并单元格/图像 OCR | 大表可能慢 | 快速全表转换 | +| **unstructured** | 元素感知;过滤噪声;表格转换 | 需 `unstructured[xlsx]` + `markdownify` | 结构化输出 | +| **MarkItDown** | 支持多工作表;简洁 | 详细度低 | 快速预览 | +| **pandas** | 功能强大;支持复杂表格 | 需 `pandas` + `tabulate` | 数据分析 | +| **XML 原生** | 无需依赖;支持所有单元格类型 | 无数据处理能力 | 依赖不可用时兜底 | + +### PDF + +| 解析器 | 优点 | 缺点 | 适用场景 | +|---------|------|--------|---------| +| **Docling** | 内置 OCR;结构化 Markdown;表格/图片占位 | 模型体积大;OCR 耗时长 | 扫描版 PDF;多语言 | +| **unstructured** | hi_res 版面分析 + PaddleOCR;元素感知;自动回退 fast | 需额外 PaddleOCR 依赖 | 版面分析;OCR | +| **MarkItDown** | 微软官方;格式规范 | 输出简洁 | 标准格式 | +| **pypdf** | 轻量;速度快;安装简单 | 功能简单 | 快速文本提取 | + ## 常见问题 -### Q: 为什么有些内容没有提取到? +### 为什么有些内容没有提取到? -A: 不同解析器的输出详细度不同: +不同解析器输出详细度不同。优先级高的解析器不一定输出最详细——Docling 和 unstructured 侧重结构化,python-docx/python-pptx 输出最详细但不做噪声过滤。建议安装对应类型的所有依赖,脚本会自动选择优先级最高的可用解析器。 -- `python-docx` / `python-pptx` 输出最详细 -- `MarkItDown` 输出较简洁 -- `XML 原生` 输出原始内容 +### PDF 文件没有标题层级? -建议安装该文档类型对应的所有解析器依赖,脚本会自动按优先级选择最佳可用解析器。 +PDF 是版面描述格式,不包含语义化标题结构。Docling 或 unstructured hi_res 策略可通过版面分析识别部分标题,准确度取决于排版质量。建议用 `-s` 搜索定位内容,或用 `-c` / `-l` 了解文档规模。 -### Q: PDF 文件没有标题层级? +### 表格格式不正确? -A: PDF 是一种版面描述格式,通常不包含语义化的标题层级结构。与 DOCX/PPTX 不同,PDF 中的标题只是视觉上的文本样式,解析器无法准确识别标题层级。建议: +XML 原生解析器对复杂表格(合并单元格、嵌套表格)支持有限。安装 Docling、unstructured 或对应的专用库可获得更好的表格处理效果。 -- 使用搜索功能查找特定内容 -- 使用 `-l` 统计行数了解文档长度 -- 使用 `-c` 统计字数了解文档规模 +### 中文显示乱码? -### Q: 表格格式不正确? - -A: 确保原始文档中的表格结构完整。XML 解析器可能无法处理复杂表格。 - -### Q: 中文显示乱码? - -A: 脚本输出使用 UTF-8 编码。确保终端支持 UTF-8: +脚本输出 UTF-8 编码,确保终端支持: ```bash # Linux/Mac @@ -523,96 +417,23 @@ export LANG=en_US.UTF-8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 ``` -### Q: 如何只使用特定解析器? +### 如何只使用特定解析器? -A: 当前版本自动选择最佳可用解析器。可以通过注释代码中的解析器列表来限制,或安装/卸载特定依赖。 +当前版本不支持指定解析器,总是按优先级自动选择。可以通过只安装目标解析器的依赖来间接控制——未安装的解析器会被跳过。 -### Q: MarkItDown 提示依赖未安装? +### 大文件处理慢? -A: MarkItDown 需要按文档类型安装对应的可选依赖,直接安装 `markitdown` 不会包含任何格式支持: +Docling 和 unstructured 对大文件较慢(尤其是 OCR)。如果只需要快速提取文本,可以只安装轻量依赖(如 pypdf、python-docx),让脚本回退到这些解析器。DOCX/PPTX/XLSX 不安装任何依赖时使用 XML 原生解析,速度最快。 -```bash -# 错误 - 不包含任何格式支持 -pip install markitdown +## 文件结构 -# 正确 - 按需安装对应格式 -pip install "markitdown[docx]" -pip install "markitdown[pptx]" -pip install "markitdown[xlsx]" -pip install "markitdown[pdf]" - -# 或一次性安装所有格式 -pip install "markitdown[docx,pptx,xlsx,pdf]" ``` - -### Q: 大文件处理慢? - -A: 大文件建议使用 XML 原生解析(最快),或在脚本外部处理。 - -## 性能参考 - -基于测试文件的参考数据: - -> Docling 作为统一入口时,整体性能受 OCR/模型下载影响:首次运行略慢,缓存后与 MarkItDown 同量级,但在 PDF 场景中由于 OCR 会稍慢一些。 - -### DOCX (test.docx) - -| 解析器 | 字符数 | 行数 | 相对速度 | -|---------|--------|------|---------| -| MarkItDown | ~8,500 | ~123 | 快 | -| python-docx | ~8,500 | ~123 | 中 | -| XML 原生 | ~8,500 | ~123 | 快 | - -### PPTX (test.pptx) - -| 解析器 | 字符数 | 行数 | 相对速度 | -|---------|--------|------|---------| -| MarkItDown | ~2,500 | ~257 | 快 | -| python-pptx | ~2,500 | ~257 | 中 | -| XML 原生 | ~2,500 | ~257 | 快 | - -### XLSX (test.xlsx) - -| 解析器 | 字符数 | 行数 | 相对速度 | -|---------|--------|------|---------| -| MarkItDown | ~6,000 | ~109 | 快 | -| pandas | ~6,000 | ~109 | 中 | -| XML 原生 | ~6,000 | ~109 | 快 | - -### PDF (test.pdf) - -| 解析器 | 字符数 | 行数 | 相对速度 | -|---------|--------|------|---------| -| MarkItDown | ~8,200 | ~1,120 | 快 | -| unstructured | ~8,400 | ~600 | 中 | -| pypdf | ~8,400 | ~600 | 快 | - -## 代码风格 - -脚本遵循以下代码风格: - -- Python 3.6+ 兼容 -- 遵循 PEP 8 规范 -- 所有公共 API 函数添加类型提示 -- 字符串优先内联使用,不提取为常量,除非被使用超过3次 -- 其他被多次使用的对象根据具体情况可考虑被提取为常量(如正则表达式) -- 模块级和公共 API 函数保留文档字符串 -- 内部辅助函数不添加文档字符串(函数名足够描述) -- 变量命名清晰,避免单字母变量名 - -## 许可证 - -脚本遵循 PEP 8 规范,Python 3.6+ 兼容。 - -## 更新日志 - -### 最新版本 - -- 新增 Docling 解析路径,统一处理 DOCX/PPTX/XLSX/PDF,并自动具备 OCR 能力 -- DOCX 解析新增 pypandoc-binary 方案并设置为最高优先级 -- 将单体脚本拆分为模块化结构(common.py, docx.py, pptx.py, xlsx.py, parser.py) -- 添加 XLSX 文件支持 -- 添加 PDF 文件支持(MarkItDown、unstructured、pypdf) -- 增强错误处理(文件存在性检查、无效格式检测) -- 完善文档和示例 -- 所有模块通过语法检查和功能测试 +scripts/ +├── common.py # 公共函数和常量 +├── docx_parser.py # DOCX 文件解析 +├── pptx_parser.py # PPTX 文件解析 +├── xlsx_parser.py # XLSX 文件解析 +├── pdf_parser.py # PDF 文件解析 +├── parser.py # 命令行入口 +└── README.md # 本文档 +``` diff --git a/temp/scripts/common.py b/temp/scripts/common.py index 3e0f845..b0746a5 100644 --- a/temp/scripts/common.py +++ b/temp/scripts/common.py @@ -8,6 +8,11 @@ from typing import List, Optional, Tuple IMAGE_PATTERN = re.compile(r"!\[[^\]]*\]\([^)]+\)") +# unstructured 噪声匹配: pptx 中的 RGB 颜色值(如 "R:255 G:128 B:0") +_RGB_PATTERN = re.compile(r"^R:\d+\s+G:\d+\s+B:\d+$") +# unstructured 噪声匹配: 破折号页码(如 "— 3 —") +_PAGE_NUMBER_PATTERN = re.compile(r"^—\s*\d+\s*—$") + def parse_with_markitdown( file_path: str, @@ -80,60 +85,41 @@ def safe_open_zip(zip_file: zipfile.ZipFile, name: str) -> Optional[zipfile.ZipE return zip_file.open(name) +_CONSECUTIVE_BLANK_LINES = re.compile(r"\n{3,}") + + def normalize_markdown_whitespace(content: str) -> str: """规范化 Markdown 空白字符,保留单行空行""" - lines = content.split("\n") - result = [] - empty_count = 0 + return _CONSECUTIVE_BLANK_LINES.sub("\n\n", content) - for line in lines: - stripped = line.strip() - if not stripped: - empty_count += 1 - if empty_count == 1: - result.append(line) - else: - empty_count = 0 - result.append(line) - return "\n".join(result) +def _is_valid_ooxml(file_path: str, required_files: List[str]) -> bool: + try: + with zipfile.ZipFile(file_path, "r") as zip_file: + names = set(zip_file.namelist()) + return all(r in names for r in required_files) + except (zipfile.BadZipFile, zipfile.LargeZipFile): + return False + + +_DOCX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "word/document.xml"] +_PPTX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "ppt/presentation.xml"] +_XLSX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "xl/workbook.xml"] def is_valid_docx(file_path: str) -> bool: """验证文件是否为有效的 DOCX 格式""" - try: - with zipfile.ZipFile(file_path, "r") as zip_file: - names = set(zip_file.namelist()) - required_files = ["[Content_Types].xml", "_rels/.rels", "word/document.xml"] - return all(r in names for r in required_files) - except (zipfile.BadZipFile, zipfile.LargeZipFile): - return False + return _is_valid_ooxml(file_path, _DOCX_REQUIRED) def is_valid_pptx(file_path: str) -> bool: """验证文件是否为有效的 PPTX 格式""" - try: - with zipfile.ZipFile(file_path, "r") as zip_file: - names = set(zip_file.namelist()) - required_files = [ - "[Content_Types].xml", - "_rels/.rels", - "ppt/presentation.xml", - ] - return all(r in names for r in required_files) - except (zipfile.BadZipFile, zipfile.LargeZipFile): - return False + return _is_valid_ooxml(file_path, _PPTX_REQUIRED) def is_valid_xlsx(file_path: str) -> bool: """验证文件是否为有效的 XLSX 格式""" - try: - with zipfile.ZipFile(file_path, "r") as zip_file: - names = set(zip_file.namelist()) - required_files = ["[Content_Types].xml", "_rels/.rels", "xl/workbook.xml"] - return all(r in names for r in required_files) - except (zipfile.BadZipFile, zipfile.LargeZipFile): - return False + return _is_valid_ooxml(file_path, _XLSX_REQUIRED) def is_valid_pdf(file_path: str) -> bool: @@ -156,12 +142,8 @@ def get_heading_level(line: str) -> int: stripped = line.lstrip() if not stripped.startswith("#"): return 0 - level = 0 - for char in stripped: - if char == "#": - level += 1 - else: - break + without_hash = stripped.lstrip("#") + level = len(stripped) - len(without_hash) if not (1 <= level <= 6): return 0 if len(stripped) == level: @@ -275,9 +257,6 @@ def search_markdown( start_line_idx = non_empty_indices[context_start_idx] end_line_idx = non_empty_indices[context_end_idx] - selected_indices = set( - non_empty_indices[context_start_idx : context_end_idx + 1] - ) result_lines = [ line for i, line in enumerate(lines) @@ -288,22 +267,71 @@ def search_markdown( return "\n---\n".join(results) +_FILE_TYPE_VALIDATORS = { + ".docx": is_valid_docx, + ".pptx": is_valid_pptx, + ".xlsx": is_valid_xlsx, + ".pdf": is_valid_pdf, +} + + def detect_file_type(file_path: str) -> Optional[str]: """检测文件类型,返回 'docx'、'pptx'、'xlsx' 或 'pdf'""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - if ext == ".docx": - if is_valid_docx(file_path): - return "docx" - elif ext == ".pptx": - if is_valid_pptx(file_path): - return "pptx" - elif ext == ".xlsx": - if is_valid_xlsx(file_path): - return "xlsx" - elif ext == ".pdf": - if is_valid_pdf(file_path): - return "pdf" - + ext = os.path.splitext(file_path)[1].lower() + validator = _FILE_TYPE_VALIDATORS.get(ext) + if validator and validator(file_path): + return ext.lstrip(".") return None + + +def _unstructured_elements_to_markdown( + elements: list, trust_titles: bool = True +) -> str: + """将 unstructured 解析出的元素列表转换为 Markdown 文本""" + try: + import markdownify as md_lib + from unstructured.documents.elements import ( + Footer, + Header, + Image, + ListItem, + PageBreak, + PageNumber, + Table, + Title, + ) + except ImportError: + return "\n\n".join( + el.text for el in elements if hasattr(el, "text") and el.text and el.text.strip() + ) + + skip_types = (Header, Footer, PageBreak, PageNumber) + parts = [] + + for el in elements: + if isinstance(el, skip_types): + continue + text = el.text.strip() if hasattr(el, "text") else str(el).strip() + if not text or _RGB_PATTERN.match(text) or _PAGE_NUMBER_PATTERN.match(text): + continue + + if isinstance(el, Table): + html = getattr(el.metadata, "text_as_html", None) + if html: + parts.append(md_lib.markdownify(html, strip=["img"]).strip()) + else: + parts.append(str(el)) + elif isinstance(el, Title) and trust_titles: + depth = getattr(el.metadata, "category_depth", None) or 1 + depth = min(max(depth, 1), 4) + parts.append(f"{'#' * depth} {text}") + elif isinstance(el, ListItem): + parts.append(f"- {text}") + elif isinstance(el, Image): + path = getattr(el.metadata, "image_path", None) or "" + if path: + parts.append(f"![image]({path})") + else: + parts.append(text) + + return "\n\n".join(parts) diff --git a/temp/scripts/docx_parser.py b/temp/scripts/docx_parser.py index 9fcac57..a5403bc 100644 --- a/temp/scripts/docx_parser.py +++ b/temp/scripts/docx_parser.py @@ -6,6 +6,7 @@ import zipfile from typing import Any, List, Optional, Tuple from common import ( + _unstructured_elements_to_markdown, build_markdown_table, parse_with_docling, parse_with_markitdown, @@ -18,6 +19,23 @@ def parse_docx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str return parse_with_docling(file_path) +def parse_docx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 unstructured 库解析 DOCX 文件""" + try: + from unstructured.partition.docx import partition_docx + except ImportError: + return None, "unstructured 库未安装" + + try: + elements = partition_docx(filename=file_path, infer_table_structure=True) + content = _unstructured_elements_to_markdown(elements) + if not content.strip(): + return None, "文档为空" + return content, None + except Exception as e: + return None, f"unstructured 解析失败: {str(e)}" + + def parse_docx_with_pypandoc(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 pypandoc-binary 库解析 DOCX 文件。""" try: @@ -59,32 +77,29 @@ def parse_docx_with_python_docx(file_path: str) -> Tuple[Optional[str], Optional try: doc = Document(file_path) + _HEADING_LEVELS = { + "Title": 1, "Heading 1": 1, "Heading 2": 2, "Heading 3": 3, + "Heading 4": 4, "Heading 5": 5, "Heading 6": 6, + } + def get_heading_level(para: Any) -> int: if para.style and para.style.name: - style_name = para.style.name - if style_name == "Title": - return 1 - elif style_name == "Heading 1": - return 1 - elif style_name == "Heading 2": - return 2 - elif style_name == "Heading 3": - return 3 - elif style_name == "Heading 4": - return 4 - elif style_name == "Heading 5": - return 5 - elif style_name == "Heading 6": - return 6 + return _HEADING_LEVELS.get(para.style.name, 0) return 0 + _LIST_STYLES = { + "Bullet": "bullet", "Number": "number", + } + def get_list_style(para: Any) -> Optional[str]: if not para.style or not para.style.name: return None style_name = para.style.name - if style_name.startswith("List Bullet") or style_name == "Bullet": + if style_name in _LIST_STYLES: + return _LIST_STYLES[style_name] + if style_name.startswith("List Bullet"): return "bullet" - elif style_name.startswith("List Number") or style_name == "Number": + if style_name.startswith("List Number"): return "number" return None @@ -170,6 +185,11 @@ def parse_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: word_namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" namespaces = {"w": word_namespace} + _STYLE_NAME_TO_HEADING = { + "title": 1, "heading 1": 1, "heading 2": 2, "heading 3": 3, + "heading 4": 4, "heading 5": 5, "heading 6": 6, + } + def get_heading_level(style_id: Optional[str], style_to_level: dict) -> int: return style_to_level.get(style_id, 0) @@ -195,8 +215,8 @@ def parse_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: rows = table_elem.findall(".//w:tr", namespaces=namespaces) if not rows: return "" - md_lines = [] - for i, row in enumerate(rows): + rows_data = [] + for row in rows: cells = row.findall(".//w:tc", namespaces=namespaces) cell_texts = [] for cell in cells: @@ -204,12 +224,8 @@ def parse_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: cell_text = cell_text.replace("\n", " ").strip() cell_texts.append(cell_text if cell_text else "") if cell_texts: - md_line = "| " + " | ".join(cell_texts) + " |" - md_lines.append(md_line) - if i == 0: - sep_line = "| " + " | ".join(["---"] * len(cell_texts)) + " |" - md_lines.append(sep_line) - return "\n".join(md_lines) + rows_data.append(cell_texts) + return build_markdown_table(rows_data) try: style_to_level = {} @@ -230,20 +246,8 @@ def parse_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: style_name = style_name_elem.get(f"{{{word_namespace}}}val") if style_name: style_name_lower = style_name.lower() - if style_name_lower == "title": - style_to_level[style_id] = 1 - elif style_name_lower == "heading 1": - style_to_level[style_id] = 1 - elif style_name_lower == "heading 2": - style_to_level[style_id] = 2 - elif style_name_lower == "heading 3": - style_to_level[style_id] = 3 - elif style_name_lower == "heading 4": - style_to_level[style_id] = 4 - elif style_name_lower == "heading 5": - style_to_level[style_id] = 5 - elif style_name_lower == "heading 6": - style_to_level[style_id] = 6 + if style_name_lower in _STYLE_NAME_TO_HEADING: + style_to_level[style_id] = _STYLE_NAME_TO_HEADING[style_name_lower] elif ( style_name_lower.startswith("list bullet") or style_name_lower == "bullet" diff --git a/temp/scripts/parser.py b/temp/scripts/parser.py index 48bed7f..9280d1e 100644 --- a/temp/scripts/parser.py +++ b/temp/scripts/parser.py @@ -65,6 +65,7 @@ def main() -> None: if file_type == "docx": parsers = [ ("docling", docx_parser.parse_docx_with_docling), + ("unstructured", docx_parser.parse_docx_with_unstructured), ("pypandoc-binary", docx_parser.parse_docx_with_pypandoc), ("MarkItDown", docx_parser.parse_docx_with_markitdown), ("python-docx", docx_parser.parse_docx_with_python_docx), @@ -73,6 +74,7 @@ def main() -> None: elif file_type == "pptx": parsers = [ ("docling", pptx_parser.parse_pptx_with_docling), + ("unstructured", pptx_parser.parse_pptx_with_unstructured), ("MarkItDown", pptx_parser.parse_pptx_with_markitdown), ("python-pptx", pptx_parser.parse_pptx_with_python_pptx), ("XML 原生解析", pptx_parser.parse_pptx_with_xml), @@ -80,6 +82,7 @@ def main() -> None: elif file_type == "xlsx": parsers = [ ("docling", xlsx_parser.parse_xlsx_with_docling), + ("unstructured", xlsx_parser.parse_xlsx_with_unstructured), ("MarkItDown", xlsx_parser.parse_xlsx_with_markitdown), ("pandas", xlsx_parser.parse_xlsx_with_pandas), ("XML 原生解析", xlsx_parser.parse_xlsx_with_xml), @@ -87,8 +90,8 @@ def main() -> None: else: parsers = [ ("docling", pdf_parser.parse_pdf_with_docling), - ("MarkItDown", pdf_parser.parse_pdf_with_markitdown), ("unstructured", pdf_parser.parse_pdf_with_unstructured), + ("MarkItDown", pdf_parser.parse_pdf_with_markitdown), ("pypdf", pdf_parser.parse_pdf_with_pypdf), ] diff --git a/temp/scripts/pdf_parser.py b/temp/scripts/pdf_parser.py index 2ba276e..30ba55d 100644 --- a/temp/scripts/pdf_parser.py +++ b/temp/scripts/pdf_parser.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""PDF 文件解析模块,提供三种解析方法。""" +"""PDF 文件解析模块,提供多种解析方法。""" from typing import Optional, Tuple -from common import parse_with_docling, parse_with_markitdown +from common import _unstructured_elements_to_markdown, parse_with_docling, parse_with_markitdown def parse_pdf_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: @@ -11,41 +11,46 @@ def parse_pdf_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str] return parse_with_docling(file_path) -def parse_pdf_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析 PDF 文件""" - return parse_with_markitdown(file_path) - - def parse_pdf_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 PDF 文件""" + """使用 unstructured 库解析 PDF 文件,优先 hi_res 策略配合 PaddleOCR""" try: from unstructured.partition.pdf import partition_pdf except ImportError: return None, "unstructured 库未安装" + base_kwargs = {"filename": file_path, "infer_table_structure": True} + try: - elements = partition_pdf( - filename=file_path, - strategy="fast", - infer_table_structure=True, - extract_images_in_pdf=False, - ) + # 优先 hi_res 策略(版面分析 + PaddleOCR),失败则回退 fast + try: + from unstructured.partition.utils.constants import OCR_AGENT_PADDLE - md_lines = [] - for element in elements: - if hasattr(element, "text") and element.text and element.text.strip(): - text = element.text.strip() - md_lines.append(text) - md_lines.append("") + elements = partition_pdf( + **base_kwargs, + strategy="hi_res", + languages=["chi_sim"], + ocr_agent=OCR_AGENT_PADDLE, + table_ocr_agent=OCR_AGENT_PADDLE, + ) + trust_titles = True + except Exception: + # fast 策略不做版面分析,Title 类型标注不可靠 + elements = partition_pdf(**base_kwargs, strategy="fast", languages=["chi_sim"]) + trust_titles = False - content = "\n".join(md_lines).strip() - if not content: + content = _unstructured_elements_to_markdown(elements, trust_titles) + if not content.strip(): return None, "文档为空" return content, None except Exception as e: return None, f"unstructured 解析失败: {str(e)}" +def parse_pdf_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 MarkItDown 库解析 PDF 文件""" + return parse_with_markitdown(file_path) + + def parse_pdf_with_pypdf(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 pypdf 库解析 PDF 文件""" try: diff --git a/temp/scripts/pptx_parser.py b/temp/scripts/pptx_parser.py index ec41628..dce07eb 100644 --- a/temp/scripts/pptx_parser.py +++ b/temp/scripts/pptx_parser.py @@ -7,6 +7,7 @@ import zipfile from typing import Any, List, Optional, Tuple from common import ( + _unstructured_elements_to_markdown, build_markdown_table, flush_list_stack, parse_with_docling, @@ -19,6 +20,25 @@ def parse_pptx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str return parse_with_docling(file_path) +def parse_pptx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 unstructured 库解析 PPTX 文件""" + try: + from unstructured.partition.pptx import partition_pptx + except ImportError: + return None, "unstructured 库未安装" + + try: + elements = partition_pptx( + filename=file_path, infer_table_structure=True, include_metadata=True + ) + content = _unstructured_elements_to_markdown(elements) + if not content.strip(): + return None, "文档为空" + return content, None + except Exception as e: + return None, f"unstructured 解析失败: {str(e)}" + + def parse_pptx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 MarkItDown 库解析 PPTX 文件""" return parse_with_markitdown(file_path) @@ -74,6 +94,8 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional except ImportError: return None, "python-pptx 库未安装" + _A_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"} + try: prs = Presentation(file_path) md_content = [] @@ -89,10 +111,7 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional 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() + flush_list_stack(list_stack, md_content) table_md = convert_table_to_md_pptx(shape.table) md_content.append(table_md) @@ -104,20 +123,8 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional 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 + 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: @@ -128,16 +135,9 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional 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 + and pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None ) marker = "1. " if is_ordered else "- " indent = " " * level @@ -149,20 +149,14 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional 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() + flush_list_stack(list_stack, md_content) 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() + flush_list_stack(list_stack, md_content) md_content.append("---\n") diff --git a/temp/scripts/xlsx_parser.py b/temp/scripts/xlsx_parser.py index f4a6adf..154307b 100644 --- a/temp/scripts/xlsx_parser.py +++ b/temp/scripts/xlsx_parser.py @@ -5,7 +5,7 @@ import xml.etree.ElementTree as ET import zipfile from typing import List, Optional, Tuple -from common import parse_with_docling, parse_with_markitdown +from common import _unstructured_elements_to_markdown, parse_with_docling, parse_with_markitdown def parse_xlsx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: @@ -13,6 +13,23 @@ def parse_xlsx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str return parse_with_docling(file_path) +def parse_xlsx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 unstructured 库解析 XLSX 文件""" + try: + from unstructured.partition.xlsx import partition_xlsx + except ImportError: + return None, "unstructured 库未安装" + + try: + elements = partition_xlsx(filename=file_path, infer_table_structure=True) + content = _unstructured_elements_to_markdown(elements) + if not content.strip(): + return None, "文档为空" + return content, None + except Exception as e: + return None, f"unstructured 解析失败: {str(e)}" + + def parse_xlsx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: """使用 MarkItDown 库解析 XLSX 文件""" return parse_with_markitdown(file_path) @@ -68,58 +85,59 @@ def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: def parse_cell_value(cell: ET.Element, shared_strings: List[str]) -> str: cell_type = cell.attrib.get("t") + + if cell_type == "inlineStr": + is_elem = cell.find("main:is", xlsx_namespace) + if is_elem is not None: + t_elem = is_elem.find("main:t", xlsx_namespace) + if t_elem is not None and t_elem.text: + return t_elem.text.replace("\n", " ").replace("\r", "") + return "" + cell_value_elem = cell.find("main:v", xlsx_namespace) + if cell_value_elem is None or not cell_value_elem.text: + return "" - if cell_value_elem is not None and cell_value_elem.text: - cell_value = cell_value_elem.text + cell_value = cell_value_elem.text - if cell_type == "s": - try: - idx = int(cell_value) - if 0 <= idx < len(shared_strings): - text = shared_strings[idx] - return text.replace("\n", " ").replace("\r", "") - except (ValueError, IndexError): - pass - return "" - elif cell_type == "b": - return "TRUE" if cell_value == "1" else "FALSE" - elif cell_type == "str": - return cell_value.replace("\n", " ").replace("\r", "") - elif cell_type == "inlineStr": - is_elem = cell.find("main:is", xlsx_namespace) - if is_elem is not None: - t_elem = is_elem.find("main:t", xlsx_namespace) - if t_elem is not None and t_elem.text: - return t_elem.text.replace("\n", " ").replace("\r", "") - return "" - elif cell_type == "e": - error_codes = { - "#NULL!": "空引用错误", - "#DIV/0!": "除零错误", - "#VALUE!": "值类型错误", - "#REF!": "无效引用", - "#NAME?": "名称错误", - "#NUM!": "数值错误", - "#N/A": "值不可用", - } - return error_codes.get(cell_value, f"错误: {cell_value}") - elif cell_type == "d": - return f"[日期] {cell_value}" - elif cell_type == "n": + if cell_type == "s": + try: + idx = int(cell_value) + if 0 <= idx < len(shared_strings): + text = shared_strings[idx] + return text.replace("\n", " ").replace("\r", "") + except (ValueError, IndexError): + pass + return "" + elif cell_type == "b": + return "TRUE" if cell_value == "1" else "FALSE" + elif cell_type == "str": + return cell_value.replace("\n", " ").replace("\r", "") + elif cell_type == "e": + _ERROR_CODES = { + "#NULL!": "空引用错误", + "#DIV/0!": "除零错误", + "#VALUE!": "值类型错误", + "#REF!": "无效引用", + "#NAME?": "名称错误", + "#NUM!": "数值错误", + "#N/A": "值不可用", + } + return _ERROR_CODES.get(cell_value, f"错误: {cell_value}") + elif cell_type == "d": + return f"[日期] {cell_value}" + elif cell_type == "n": + return cell_value + elif cell_type is None: + try: + float_val = float(cell_value) + if float_val.is_integer(): + return str(int(float_val)) return cell_value - elif cell_type is None: - try: - float_val = float(cell_value) - if float_val.is_integer(): - return str(int(float_val)) - return cell_value - except ValueError: - return cell_value - else: + except ValueError: return cell_value else: - return "" + return cell_value def get_non_empty_columns(data: List[List[str]]) -> set: non_empty_cols = set()