增加unstructured处理策略
This commit is contained in:
@@ -1,103 +1,23 @@
|
|||||||
# Document Parser 使用说明
|
# 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
|
```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 <file_path> [options]
|
python parser.py <file_path> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 必需参数
|
`file_path` 为 DOCX、PPTX、XLSX 或 PDF 文件路径(相对或绝对路径)。不带任何选项时输出完整 Markdown 内容。
|
||||||
|
|
||||||
- `file_path`: DOCX、PPTX、XLSX 或 PDF 文件的路径(相对或绝对路径)
|
### 参数说明
|
||||||
|
|
||||||
### 可选参数(互斥组,一次只能使用一个)
|
以下参数互斥,一次只能使用一个:
|
||||||
|
|
||||||
| 参数 | 短选项 | 长选项 | 说明 |
|
| 短选项 | 长选项 | 说明 |
|
||||||
|------|---------|---------|------|
|
|--------|--------|------|
|
||||||
| `-c` | `--count` | 返回解析后的 markdown 文档的总字数 |
|
| `-c` | `--count` | 输出解析后文档的总字符数(不含换行符) |
|
||||||
| `-l` | `--lines` | 返回解析后的 markdown 文档的总行数 |
|
| `-l` | `--lines` | 输出解析后文档的总行数 |
|
||||||
| `-t` | `--titles` | 返回解析后的 markdown 文档的标题行(1-6级) |
|
| `-t` | `--titles` | 输出所有标题行(1-6 级,含 `#` 前缀) |
|
||||||
| `-tc <name>` | `--title-content <name>` | 提取指定标题及其下级内容(不包含#号) |
|
| `-tc <name>` | `--title-content <name>` | 提取指定标题及其下级内容(`name` 不包含 `#` 号) |
|
||||||
| `-s <pattern>` | `--search <pattern>` | 使用正则表达式搜索文档,返回所有匹配结果(用---分隔) |
|
| `-s <pattern>` | `--search <pattern>` | 使用正则表达式搜索文档,返回匹配结果 |
|
||||||
|
|
||||||
### 搜索上下文参数
|
搜索辅助参数(与 `-s` 配合使用):
|
||||||
|
|
||||||
- `-n <num>` / `--context <num>`: 与 `-s` 配合使用,指定每个检索结果包含的前后行数(默认:2)
|
| 短选项 | 长选项 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `-n <num>` | `--context <num>` | 每个匹配结果包含的前后非空行数(默认:2) |
|
||||||
|
|
||||||
## 使用示例
|
### 退出码
|
||||||
|
|
||||||
### 1. 输出完整 Markdown 内容
|
| 退出码 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| `0` | 解析成功 |
|
||||||
|
| `1` | 错误(文件不存在、格式无效、所有解析器失败、标题未找到、正则无效或无匹配) |
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
**输出完整 Markdown:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 解析 DOCX
|
python parser.py report.docx # 输出到终端
|
||||||
python parser.py report.docx
|
python parser.py report.docx > output.md # 重定向到文件
|
||||||
|
|
||||||
# 解析 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 统计文档信息
|
**统计信息(`-c` / `-l`):**
|
||||||
|
|
||||||
|
输出单个数字,适合管道处理。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 统计字数
|
$ python parser.py report.docx -c
|
||||||
python parser.py report.docx -c
|
8500
|
||||||
python parser.py report.pdf -c
|
|
||||||
|
|
||||||
# 统计行数
|
$ python parser.py report.docx -l
|
||||||
python parser.py report.docx -l
|
215
|
||||||
python parser.py report.pdf -l
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 提取标题
|
**提取标题(`-t`):**
|
||||||
|
|
||||||
|
每行一个标题,保留 `#` 前缀和层级。PDF 通常不包含语义化标题层级。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 提取所有标题
|
$ python parser.py report.docx -t
|
||||||
python parser.py report.docx -t
|
|
||||||
python parser.py report.pdf -t
|
|
||||||
|
|
||||||
# 输出示例(DOCX):
|
|
||||||
# 第一章 概述
|
# 第一章 概述
|
||||||
## 1.1 背景
|
## 1.1 背景
|
||||||
## 1.2 目标
|
## 1.2 目标
|
||||||
# 第二章 实现
|
# 第二章 实现
|
||||||
|
|
||||||
# 输出示例(PDF - 注意:PDF 通常不包含明确的标题层级):
|
|
||||||
[内容提取成功,但 PDF 可能缺乏清晰的标题结构]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 提取特定标题内容
|
**提取标题内容(`-tc`):**
|
||||||
|
|
||||||
|
输出指定标题及其下级内容。如果文档中有多个同名标题,用 `---` 分隔。每段输出包含上级标题链。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 提取特定章节
|
$ python parser.py report.docx -tc "1.1 背景"
|
||||||
python parser.py report.docx -tc "第一章"
|
# 第一章 概述
|
||||||
python parser.py report.pdf -tc "第一章"
|
## 1.1 背景
|
||||||
|
这是背景的详细内容...
|
||||||
# 输出该标题及其所有子内容
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 搜索文档内容
|
**搜索(`-s`):**
|
||||||
|
|
||||||
|
支持 Python 正则表达式语法。多个匹配结果用 `---` 分隔。`-n` 控制上下文行数。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 搜索关键词
|
$ python parser.py report.docx -s "测试" -n 1
|
||||||
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
|
|
||||||
|
|
||||||
# 输出示例:
|
|
||||||
---
|
|
||||||
这是重要内容的前两行
|
|
||||||
**重要内容**
|
|
||||||
这是重要内容后两行
|
|
||||||
---
|
---
|
||||||
|
另一处上一行
|
||||||
|
另一处**测试**内容
|
||||||
|
另一处下一行
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用 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 格式<br>• 自动带 OCR,复杂文档召回率高<br>• 输出 Markdown 结构稳定 | • 首次运行需下载较大的模型<br>• 运行时内存占用相对更高 | • 需要"一键完成"解析<br>• 需要 OCR/多模态支持 |
|
|
||||||
| **pypandoc-binary** | • 自带 Pandoc,可直接使用<br>• 输出 Markdown 结构整洁<br>• 错误信息清晰易排查 | • 仅适用于 DOCX<br>• 依赖包体积较大 | • 需要标准化 Markdown 输出<br>• Docling 不可用时的首选 |
|
|
||||||
| **MarkItDown** | • 格式规范<br>• 微软官方支持<br>• 兼容性好 | • 需要安装<br>• 输出较简洁 | • 需要标准格式输出<br>• 自动化文档处理 |
|
|
||||||
| **python-docx** | • 输出最详细<br>• 保留完整结构<br>• 支持复杂样式 | • 需要安装<br>• 可能包含多余空行 | • 需要精确控制输出<br>• 分析文档结构 |
|
|
||||||
| **XML 原生** | • 无需依赖<br>• 运行速度快<br>• 输出原始内容 | • 格式可能不统一<br>• 样式处理有限 | • 依赖不可用时<br>• 快速提取内容 |
|
|
||||||
|
|
||||||
### PPTX 解析器
|
|
||||||
|
|
||||||
PPTX 文件会按以下优先级依次尝试解析:Docling → MarkItDown → python-pptx → XML 原生。
|
|
||||||
|
|
||||||
| 解析器 | 优点 | 缺点 | 适用场景 |
|
|
||||||
|---------|------|--------|---------|
|
|
||||||
| **Docling** | • 解析幻灯片文本、表格与图片 OCR<br>• 自动生成统一 Markdown,包含分页分隔符 | • 需要下载模型<br>• 细节控制少 | • 需要一次性转换全部幻灯片<br>• 有图片或扫描件的 PPTX |
|
|
||||||
| **MarkItDown** | • 格式规范<br>• 自动添加 Slide 分隔<br>• 输出简洁 | • 需要安装<br>• 详细度较低 | • 快速预览幻灯片<br>• 提取主要内容 |
|
|
||||||
| **python-pptx** | • 输出最详细<br>• 保留完整结构<br>• 支持层级列表 | • 需要安装<br>• 依赖私有 API | • 需要完整内容<br>• 分析演示结构 |
|
|
||||||
| **XML 原生** | • 无需依赖<br>• 结构化输出<br>• 运行速度快 | • 格式可能不统一<br>• 幻灯片分组简单 | • 依赖不可用时<br>• 结构化提取 |
|
|
||||||
|
|
||||||
### XLSX 解析器
|
|
||||||
|
|
||||||
XLSX 文件会按以下优先级依次尝试解析:Docling → MarkItDown → pandas → XML 原生。
|
|
||||||
|
|
||||||
| 解析器 | 优点 | 缺点 | 适用场景 |
|
|
||||||
|---------|------|--------|---------|
|
|
||||||
| **Docling** | • 单次遍历导出全部工作表为 Markdown<br>• 自动处理合并单元格/图像 OCR | • 需要下载模型<br>• 对极大体积表可能较慢 | • 快速完成全表转 Markdown<br>• 含扫描图片的表格 |
|
|
||||||
| **MarkItDown** | • 格式规范<br>• 支持多工作表<br>• 输出简洁 | • 需要安装<br>• 详细度较低 | • 快速预览表格<br>• 提取主要内容 |
|
|
||||||
| **pandas** | • 功能强大<br>• 支持复杂表格<br>• 数据处理灵活 | • 需要安装<br>• 依赖较多 | • 数据分析<br>• 复杂表格处理 |
|
|
||||||
| **XML 原生** | • 无需依赖<br>• 运行速度快<br>• 支持所有单元格类型 | • 格式可能不统一<br>• 无数据处理能力 | • 依赖不可用时<br>• 快速提取内容 |
|
|
||||||
|
|
||||||
### PDF 解析器
|
|
||||||
|
|
||||||
PDF 文件会按以下优先级依次尝试解析:Docling → MarkItDown → unstructured → pypdf。
|
|
||||||
|
|
||||||
| 解析器 | 优点 | 缺点 | 适用场景 |
|
|
||||||
|---------|------|--------|---------|
|
|
||||||
| **Docling** | • 内置 RapidOCR,可处理扫描版 PDF<br>• 输出结构化 Markdown,包含表格/图片占位 | • 模型下载体积大<br>• OCR 耗时较长 | • 需要 OCR、表格/图片识别<br>• 多语言 PDF |
|
|
||||||
| **MarkItDown** | • 格式规范<br>• 微软官方支持<br>• 兼容性好 | • 需要安装 `markitdown[pdf]`<br>• 输出较简洁 | • 需要标准格式输出<br>• 自动化文档处理 |
|
|
||||||
| **unstructured** | • 功能强大<br>• 支持表格提取<br>• 文本组织性好 | • 需要安装<br>• 可能包含页码标记 | • 需要完整内容<br>• 分析文档结构 |
|
|
||||||
| **pypdf** | • 轻量级<br>• 速度快<br>• 安装简单 | • 需要安装<br>• 功能相对简单 | • 快速提取内容<br>• 简单文本提取 |
|
|
||||||
|
|
||||||
## 输出格式
|
|
||||||
|
|
||||||
### Markdown 输出结构
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# 标题 1 (一级)
|
|
||||||
|
|
||||||
正文段落
|
|
||||||
|
|
||||||
## 标题 2 (二级)
|
|
||||||
|
|
||||||
- 列表项 1
|
|
||||||
- 列表项 2
|
|
||||||
|
|
||||||
1. 有序列表项 1
|
|
||||||
2. 有序列表项 2
|
|
||||||
|
|
||||||
| 列1 | 列2 | 列3 |
|
|
||||||
|------|------|------|
|
|
||||||
| 数据1 | 数据2 | 数据3 |
|
|
||||||
|
|
||||||
**粗体文本** *斜体文本* <u>下划线文本</u>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 图片 (``)
|
|
||||||
- HTML 图片标签 (`<img>`, `</img>`)
|
|
||||||
- 媒体链接 (`[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
|
```bash
|
||||||
@@ -480,40 +129,285 @@ Get-ChildItem *.docx | ForEach-Object {
|
|||||||
### 管道使用
|
### 管道使用
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进一步处理 Markdown 输出
|
# 过滤包含关键词的行
|
||||||
python parser.py report.docx | grep "重要" > important.md
|
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 |
|
||||||
|
|
||||||
|
**粗体** *斜体* <u>下划线</u>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 各格式特有结构
|
||||||
|
|
||||||
|
**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 策略可通过版面分析识别部分标题,但准确度取决于排版质量。
|
||||||
|
|
||||||
|
### 内容自动处理
|
||||||
|
|
||||||
|
输出前会自动进行以下处理:
|
||||||
|
|
||||||
|
| 处理 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 图片移除 | 删除 `` 语法 |
|
||||||
|
| 空行规范化 | 连续多个空行合并为一个 |
|
||||||
|
| 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` 输出最详细
|
### PDF 文件没有标题层级?
|
||||||
- `MarkItDown` 输出较简洁
|
|
||||||
- `XML 原生` 输出原始内容
|
|
||||||
|
|
||||||
建议安装该文档类型对应的所有解析器依赖,脚本会自动按优先级选择最佳可用解析器。
|
PDF 是版面描述格式,不包含语义化标题结构。Docling 或 unstructured hi_res 策略可通过版面分析识别部分标题,准确度取决于排版质量。建议用 `-s` 搜索定位内容,或用 `-c` / `-l` 了解文档规模。
|
||||||
|
|
||||||
### Q: PDF 文件没有标题层级?
|
### 表格格式不正确?
|
||||||
|
|
||||||
A: PDF 是一种版面描述格式,通常不包含语义化的标题层级结构。与 DOCX/PPTX 不同,PDF 中的标题只是视觉上的文本样式,解析器无法准确识别标题层级。建议:
|
XML 原生解析器对复杂表格(合并单元格、嵌套表格)支持有限。安装 Docling、unstructured 或对应的专用库可获得更好的表格处理效果。
|
||||||
|
|
||||||
- 使用搜索功能查找特定内容
|
### 中文显示乱码?
|
||||||
- 使用 `-l` 统计行数了解文档长度
|
|
||||||
- 使用 `-c` 统计字数了解文档规模
|
|
||||||
|
|
||||||
### Q: 表格格式不正确?
|
脚本输出 UTF-8 编码,确保终端支持:
|
||||||
|
|
||||||
A: 确保原始文档中的表格结构完整。XML 解析器可能无法处理复杂表格。
|
|
||||||
|
|
||||||
### Q: 中文显示乱码?
|
|
||||||
|
|
||||||
A: 脚本输出使用 UTF-8 编码。确保终端支持 UTF-8:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux/Mac
|
# Linux/Mac
|
||||||
@@ -523,96 +417,23 @@ export LANG=en_US.UTF-8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[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]"
|
|
||||||
```
|
```
|
||||||
|
scripts/
|
||||||
### Q: 大文件处理慢?
|
├── common.py # 公共函数和常量
|
||||||
|
├── docx_parser.py # DOCX 文件解析
|
||||||
A: 大文件建议使用 XML 原生解析(最快),或在脚本外部处理。
|
├── pptx_parser.py # PPTX 文件解析
|
||||||
|
├── xlsx_parser.py # XLSX 文件解析
|
||||||
## 性能参考
|
├── pdf_parser.py # PDF 文件解析
|
||||||
|
├── parser.py # 命令行入口
|
||||||
基于测试文件的参考数据:
|
└── README.md # 本文档
|
||||||
|
```
|
||||||
> 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)
|
|
||||||
- 增强错误处理(文件存在性检查、无效格式检测)
|
|
||||||
- 完善文档和示例
|
|
||||||
- 所有模块通过语法检查和功能测试
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ from typing import List, Optional, Tuple
|
|||||||
|
|
||||||
IMAGE_PATTERN = re.compile(r"!\[[^\]]*\]\([^)]+\)")
|
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(
|
def parse_with_markitdown(
|
||||||
file_path: str,
|
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)
|
return zip_file.open(name)
|
||||||
|
|
||||||
|
|
||||||
|
_CONSECUTIVE_BLANK_LINES = re.compile(r"\n{3,}")
|
||||||
|
|
||||||
|
|
||||||
def normalize_markdown_whitespace(content: str) -> str:
|
def normalize_markdown_whitespace(content: str) -> str:
|
||||||
"""规范化 Markdown 空白字符,保留单行空行"""
|
"""规范化 Markdown 空白字符,保留单行空行"""
|
||||||
lines = content.split("\n")
|
return _CONSECUTIVE_BLANK_LINES.sub("\n\n", content)
|
||||||
result = []
|
|
||||||
empty_count = 0
|
|
||||||
|
|
||||||
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:
|
def is_valid_docx(file_path: str) -> bool:
|
||||||
"""验证文件是否为有效的 DOCX 格式"""
|
"""验证文件是否为有效的 DOCX 格式"""
|
||||||
try:
|
return _is_valid_ooxml(file_path, _DOCX_REQUIRED)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pptx(file_path: str) -> bool:
|
def is_valid_pptx(file_path: str) -> bool:
|
||||||
"""验证文件是否为有效的 PPTX 格式"""
|
"""验证文件是否为有效的 PPTX 格式"""
|
||||||
try:
|
return _is_valid_ooxml(file_path, _PPTX_REQUIRED)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_xlsx(file_path: str) -> bool:
|
def is_valid_xlsx(file_path: str) -> bool:
|
||||||
"""验证文件是否为有效的 XLSX 格式"""
|
"""验证文件是否为有效的 XLSX 格式"""
|
||||||
try:
|
return _is_valid_ooxml(file_path, _XLSX_REQUIRED)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pdf(file_path: str) -> bool:
|
def is_valid_pdf(file_path: str) -> bool:
|
||||||
@@ -156,12 +142,8 @@ def get_heading_level(line: str) -> int:
|
|||||||
stripped = line.lstrip()
|
stripped = line.lstrip()
|
||||||
if not stripped.startswith("#"):
|
if not stripped.startswith("#"):
|
||||||
return 0
|
return 0
|
||||||
level = 0
|
without_hash = stripped.lstrip("#")
|
||||||
for char in stripped:
|
level = len(stripped) - len(without_hash)
|
||||||
if char == "#":
|
|
||||||
level += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
if not (1 <= level <= 6):
|
if not (1 <= level <= 6):
|
||||||
return 0
|
return 0
|
||||||
if len(stripped) == level:
|
if len(stripped) == level:
|
||||||
@@ -275,9 +257,6 @@ def search_markdown(
|
|||||||
start_line_idx = non_empty_indices[context_start_idx]
|
start_line_idx = non_empty_indices[context_start_idx]
|
||||||
end_line_idx = non_empty_indices[context_end_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 = [
|
result_lines = [
|
||||||
line
|
line
|
||||||
for i, line in enumerate(lines)
|
for i, line in enumerate(lines)
|
||||||
@@ -288,22 +267,71 @@ def search_markdown(
|
|||||||
return "\n---\n".join(results)
|
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]:
|
def detect_file_type(file_path: str) -> Optional[str]:
|
||||||
"""检测文件类型,返回 'docx'、'pptx'、'xlsx' 或 'pdf'"""
|
"""检测文件类型,返回 'docx'、'pptx'、'xlsx' 或 'pdf'"""
|
||||||
_, ext = os.path.splitext(file_path)
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
ext = ext.lower()
|
validator = _FILE_TYPE_VALIDATORS.get(ext)
|
||||||
|
if validator and validator(file_path):
|
||||||
if ext == ".docx":
|
return ext.lstrip(".")
|
||||||
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"
|
|
||||||
|
|
||||||
return None
|
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"")
|
||||||
|
else:
|
||||||
|
parts.append(text)
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import zipfile
|
|||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
from common import (
|
from common import (
|
||||||
|
_unstructured_elements_to_markdown,
|
||||||
build_markdown_table,
|
build_markdown_table,
|
||||||
parse_with_docling,
|
parse_with_docling,
|
||||||
parse_with_markitdown,
|
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)
|
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]]:
|
def parse_docx_with_pypandoc(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""使用 pypandoc-binary 库解析 DOCX 文件。"""
|
"""使用 pypandoc-binary 库解析 DOCX 文件。"""
|
||||||
try:
|
try:
|
||||||
@@ -59,32 +77,29 @@ def parse_docx_with_python_docx(file_path: str) -> Tuple[Optional[str], Optional
|
|||||||
try:
|
try:
|
||||||
doc = Document(file_path)
|
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:
|
def get_heading_level(para: Any) -> int:
|
||||||
if para.style and para.style.name:
|
if para.style and para.style.name:
|
||||||
style_name = para.style.name
|
return _HEADING_LEVELS.get(para.style.name, 0)
|
||||||
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 0
|
return 0
|
||||||
|
|
||||||
|
_LIST_STYLES = {
|
||||||
|
"Bullet": "bullet", "Number": "number",
|
||||||
|
}
|
||||||
|
|
||||||
def get_list_style(para: Any) -> Optional[str]:
|
def get_list_style(para: Any) -> Optional[str]:
|
||||||
if not para.style or not para.style.name:
|
if not para.style or not para.style.name:
|
||||||
return None
|
return None
|
||||||
style_name = para.style.name
|
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"
|
return "bullet"
|
||||||
elif style_name.startswith("List Number") or style_name == "Number":
|
if style_name.startswith("List Number"):
|
||||||
return "number"
|
return "number"
|
||||||
return None
|
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"
|
word_namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||||
namespaces = {"w": word_namespace}
|
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:
|
def get_heading_level(style_id: Optional[str], style_to_level: dict) -> int:
|
||||||
return style_to_level.get(style_id, 0)
|
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)
|
rows = table_elem.findall(".//w:tr", namespaces=namespaces)
|
||||||
if not rows:
|
if not rows:
|
||||||
return ""
|
return ""
|
||||||
md_lines = []
|
rows_data = []
|
||||||
for i, row in enumerate(rows):
|
for row in rows:
|
||||||
cells = row.findall(".//w:tc", namespaces=namespaces)
|
cells = row.findall(".//w:tc", namespaces=namespaces)
|
||||||
cell_texts = []
|
cell_texts = []
|
||||||
for cell in cells:
|
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_text = cell_text.replace("\n", " ").strip()
|
||||||
cell_texts.append(cell_text if cell_text else "")
|
cell_texts.append(cell_text if cell_text else "")
|
||||||
if cell_texts:
|
if cell_texts:
|
||||||
md_line = "| " + " | ".join(cell_texts) + " |"
|
rows_data.append(cell_texts)
|
||||||
md_lines.append(md_line)
|
return build_markdown_table(rows_data)
|
||||||
if i == 0:
|
|
||||||
sep_line = "| " + " | ".join(["---"] * len(cell_texts)) + " |"
|
|
||||||
md_lines.append(sep_line)
|
|
||||||
return "\n".join(md_lines)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
style_to_level = {}
|
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")
|
style_name = style_name_elem.get(f"{{{word_namespace}}}val")
|
||||||
if style_name:
|
if style_name:
|
||||||
style_name_lower = style_name.lower()
|
style_name_lower = style_name.lower()
|
||||||
if style_name_lower == "title":
|
if style_name_lower in _STYLE_NAME_TO_HEADING:
|
||||||
style_to_level[style_id] = 1
|
style_to_level[style_id] = _STYLE_NAME_TO_HEADING[style_name_lower]
|
||||||
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
|
|
||||||
elif (
|
elif (
|
||||||
style_name_lower.startswith("list bullet")
|
style_name_lower.startswith("list bullet")
|
||||||
or style_name_lower == "bullet"
|
or style_name_lower == "bullet"
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def main() -> None:
|
|||||||
if file_type == "docx":
|
if file_type == "docx":
|
||||||
parsers = [
|
parsers = [
|
||||||
("docling", docx_parser.parse_docx_with_docling),
|
("docling", docx_parser.parse_docx_with_docling),
|
||||||
|
("unstructured", docx_parser.parse_docx_with_unstructured),
|
||||||
("pypandoc-binary", docx_parser.parse_docx_with_pypandoc),
|
("pypandoc-binary", docx_parser.parse_docx_with_pypandoc),
|
||||||
("MarkItDown", docx_parser.parse_docx_with_markitdown),
|
("MarkItDown", docx_parser.parse_docx_with_markitdown),
|
||||||
("python-docx", docx_parser.parse_docx_with_python_docx),
|
("python-docx", docx_parser.parse_docx_with_python_docx),
|
||||||
@@ -73,6 +74,7 @@ def main() -> None:
|
|||||||
elif file_type == "pptx":
|
elif file_type == "pptx":
|
||||||
parsers = [
|
parsers = [
|
||||||
("docling", pptx_parser.parse_pptx_with_docling),
|
("docling", pptx_parser.parse_pptx_with_docling),
|
||||||
|
("unstructured", pptx_parser.parse_pptx_with_unstructured),
|
||||||
("MarkItDown", pptx_parser.parse_pptx_with_markitdown),
|
("MarkItDown", pptx_parser.parse_pptx_with_markitdown),
|
||||||
("python-pptx", pptx_parser.parse_pptx_with_python_pptx),
|
("python-pptx", pptx_parser.parse_pptx_with_python_pptx),
|
||||||
("XML 原生解析", pptx_parser.parse_pptx_with_xml),
|
("XML 原生解析", pptx_parser.parse_pptx_with_xml),
|
||||||
@@ -80,6 +82,7 @@ def main() -> None:
|
|||||||
elif file_type == "xlsx":
|
elif file_type == "xlsx":
|
||||||
parsers = [
|
parsers = [
|
||||||
("docling", xlsx_parser.parse_xlsx_with_docling),
|
("docling", xlsx_parser.parse_xlsx_with_docling),
|
||||||
|
("unstructured", xlsx_parser.parse_xlsx_with_unstructured),
|
||||||
("MarkItDown", xlsx_parser.parse_xlsx_with_markitdown),
|
("MarkItDown", xlsx_parser.parse_xlsx_with_markitdown),
|
||||||
("pandas", xlsx_parser.parse_xlsx_with_pandas),
|
("pandas", xlsx_parser.parse_xlsx_with_pandas),
|
||||||
("XML 原生解析", xlsx_parser.parse_xlsx_with_xml),
|
("XML 原生解析", xlsx_parser.parse_xlsx_with_xml),
|
||||||
@@ -87,8 +90,8 @@ def main() -> None:
|
|||||||
else:
|
else:
|
||||||
parsers = [
|
parsers = [
|
||||||
("docling", pdf_parser.parse_pdf_with_docling),
|
("docling", pdf_parser.parse_pdf_with_docling),
|
||||||
("MarkItDown", pdf_parser.parse_pdf_with_markitdown),
|
|
||||||
("unstructured", pdf_parser.parse_pdf_with_unstructured),
|
("unstructured", pdf_parser.parse_pdf_with_unstructured),
|
||||||
|
("MarkItDown", pdf_parser.parse_pdf_with_markitdown),
|
||||||
("pypdf", pdf_parser.parse_pdf_with_pypdf),
|
("pypdf", pdf_parser.parse_pdf_with_pypdf),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""PDF 文件解析模块,提供三种解析方法。"""
|
"""PDF 文件解析模块,提供多种解析方法。"""
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
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]]:
|
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)
|
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]]:
|
def parse_pdf_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""使用 unstructured 库解析 PDF 文件"""
|
"""使用 unstructured 库解析 PDF 文件,优先 hi_res 策略配合 PaddleOCR"""
|
||||||
try:
|
try:
|
||||||
from unstructured.partition.pdf import partition_pdf
|
from unstructured.partition.pdf import partition_pdf
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None, "unstructured 库未安装"
|
return None, "unstructured 库未安装"
|
||||||
|
|
||||||
|
base_kwargs = {"filename": file_path, "infer_table_structure": True}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 优先 hi_res 策略(版面分析 + PaddleOCR),失败则回退 fast
|
||||||
|
try:
|
||||||
|
from unstructured.partition.utils.constants import OCR_AGENT_PADDLE
|
||||||
|
|
||||||
elements = partition_pdf(
|
elements = partition_pdf(
|
||||||
filename=file_path,
|
**base_kwargs,
|
||||||
strategy="fast",
|
strategy="hi_res",
|
||||||
infer_table_structure=True,
|
languages=["chi_sim"],
|
||||||
extract_images_in_pdf=False,
|
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
|
||||||
|
|
||||||
md_lines = []
|
content = _unstructured_elements_to_markdown(elements, trust_titles)
|
||||||
for element in elements:
|
if not content.strip():
|
||||||
if hasattr(element, "text") and element.text and element.text.strip():
|
|
||||||
text = element.text.strip()
|
|
||||||
md_lines.append(text)
|
|
||||||
md_lines.append("")
|
|
||||||
|
|
||||||
content = "\n".join(md_lines).strip()
|
|
||||||
if not content:
|
|
||||||
return None, "文档为空"
|
return None, "文档为空"
|
||||||
return content, None
|
return content, None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None, f"unstructured 解析失败: {str(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]]:
|
def parse_pdf_with_pypdf(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""使用 pypdf 库解析 PDF 文件"""
|
"""使用 pypdf 库解析 PDF 文件"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import zipfile
|
|||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
from common import (
|
from common import (
|
||||||
|
_unstructured_elements_to_markdown,
|
||||||
build_markdown_table,
|
build_markdown_table,
|
||||||
flush_list_stack,
|
flush_list_stack,
|
||||||
parse_with_docling,
|
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)
|
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]]:
|
def parse_pptx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""使用 MarkItDown 库解析 PPTX 文件"""
|
"""使用 MarkItDown 库解析 PPTX 文件"""
|
||||||
return parse_with_markitdown(file_path)
|
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:
|
except ImportError:
|
||||||
return None, "python-pptx 库未安装"
|
return None, "python-pptx 库未安装"
|
||||||
|
|
||||||
|
_A_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prs = Presentation(file_path)
|
prs = Presentation(file_path)
|
||||||
md_content = []
|
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 hasattr(shape, "has_table") and shape.has_table:
|
||||||
if list_stack:
|
if list_stack:
|
||||||
md_content.append(
|
flush_list_stack(list_stack, md_content)
|
||||||
"\n" + "\n".join([x for x in list_stack if x]) + "\n"
|
|
||||||
)
|
|
||||||
list_stack.clear()
|
|
||||||
|
|
||||||
table_md = convert_table_to_md_pptx(shape.table)
|
table_md = convert_table_to_md_pptx(shape.table)
|
||||||
md_content.append(table_md)
|
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:
|
if pPr is not None:
|
||||||
is_list = (
|
is_list = (
|
||||||
para.level > 0
|
para.level > 0
|
||||||
or pPr.find(
|
or pPr.find(".//a:buChar", namespaces=_A_NS) is not None
|
||||||
".//a:buChar",
|
or pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None
|
||||||
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:
|
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)
|
text = extract_formatted_text_pptx(para.runs)
|
||||||
if text:
|
if text:
|
||||||
pPr = para._element.pPr
|
|
||||||
is_ordered = (
|
is_ordered = (
|
||||||
pPr is not None
|
pPr is not None
|
||||||
and pPr.find(
|
and pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None
|
||||||
".//a:buAutoNum",
|
|
||||||
namespaces={
|
|
||||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
is not None
|
|
||||||
)
|
)
|
||||||
marker = "1. " if is_ordered else "- "
|
marker = "1. " if is_ordered else "- "
|
||||||
indent = " " * level
|
indent = " " * level
|
||||||
@@ -149,20 +149,14 @@ def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional
|
|||||||
list_stack[i] = ""
|
list_stack[i] = ""
|
||||||
else:
|
else:
|
||||||
if list_stack:
|
if list_stack:
|
||||||
md_content.append(
|
flush_list_stack(list_stack, md_content)
|
||||||
"\n"
|
|
||||||
+ "\n".join([x for x in list_stack if x])
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
list_stack.clear()
|
|
||||||
|
|
||||||
text = extract_formatted_text_pptx(para.runs)
|
text = extract_formatted_text_pptx(para.runs)
|
||||||
if text:
|
if text:
|
||||||
md_content.append(f"{text}\n")
|
md_content.append(f"{text}\n")
|
||||||
|
|
||||||
if list_stack:
|
if list_stack:
|
||||||
md_content.append("\n" + "\n".join([x for x in list_stack if x]) + "\n")
|
flush_list_stack(list_stack, md_content)
|
||||||
list_stack.clear()
|
|
||||||
|
|
||||||
md_content.append("---\n")
|
md_content.append("---\n")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import xml.etree.ElementTree as ET
|
|||||||
import zipfile
|
import zipfile
|
||||||
from typing import List, Optional, Tuple
|
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]]:
|
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)
|
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]]:
|
def parse_xlsx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""使用 MarkItDown 库解析 XLSX 文件"""
|
"""使用 MarkItDown 库解析 XLSX 文件"""
|
||||||
return parse_with_markitdown(file_path)
|
return parse_with_markitdown(file_path)
|
||||||
@@ -68,9 +85,19 @@ 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:
|
def parse_cell_value(cell: ET.Element, shared_strings: List[str]) -> str:
|
||||||
cell_type = cell.attrib.get("t")
|
cell_type = cell.attrib.get("t")
|
||||||
cell_value_elem = cell.find("main:v", xlsx_namespace)
|
|
||||||
|
|
||||||
if cell_value_elem is not None and cell_value_elem.text:
|
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 ""
|
||||||
|
|
||||||
cell_value = cell_value_elem.text
|
cell_value = cell_value_elem.text
|
||||||
|
|
||||||
if cell_type == "s":
|
if cell_type == "s":
|
||||||
@@ -86,15 +113,8 @@ def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
|||||||
return "TRUE" if cell_value == "1" else "FALSE"
|
return "TRUE" if cell_value == "1" else "FALSE"
|
||||||
elif cell_type == "str":
|
elif cell_type == "str":
|
||||||
return cell_value.replace("\n", " ").replace("\r", "")
|
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":
|
elif cell_type == "e":
|
||||||
error_codes = {
|
_ERROR_CODES = {
|
||||||
"#NULL!": "空引用错误",
|
"#NULL!": "空引用错误",
|
||||||
"#DIV/0!": "除零错误",
|
"#DIV/0!": "除零错误",
|
||||||
"#VALUE!": "值类型错误",
|
"#VALUE!": "值类型错误",
|
||||||
@@ -103,7 +123,7 @@ def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
|||||||
"#NUM!": "数值错误",
|
"#NUM!": "数值错误",
|
||||||
"#N/A": "值不可用",
|
"#N/A": "值不可用",
|
||||||
}
|
}
|
||||||
return error_codes.get(cell_value, f"错误: {cell_value}")
|
return _ERROR_CODES.get(cell_value, f"错误: {cell_value}")
|
||||||
elif cell_type == "d":
|
elif cell_type == "d":
|
||||||
return f"[日期] {cell_value}"
|
return f"[日期] {cell_value}"
|
||||||
elif cell_type == "n":
|
elif cell_type == "n":
|
||||||
@@ -118,8 +138,6 @@ def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
|||||||
return cell_value
|
return cell_value
|
||||||
else:
|
else:
|
||||||
return cell_value
|
return cell_value
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_non_empty_columns(data: List[List[str]]) -> set:
|
def get_non_empty_columns(data: List[List[str]]) -> set:
|
||||||
non_empty_cols = set()
|
non_empty_cols = set()
|
||||||
|
|||||||
Reference in New Issue
Block a user