摘要
本文是一份 Markitdown 本地文档解析与转换的实战指南。Markitdown 是微软开源的轻量级文档转换工具,支持 PDF、DOCX、PPTX、XLSX 等 20+ 种格式统一转为 Markdown。文章从环境搭建、命令行快速上手、Python API 批量处理,到效果验证、复杂表格与图片提取、自定义解析器扩展,再到排错指南、大文件性能优化,最后介绍了输出规范化与对接知识库、RAG 系统、CI/CD 流水线的集成方案,帮助读者全面掌握 Markitdown 的使用与落地。
① Markitdown 是什么:核心功能与典型应用场景
Markitdown 是微软开源的一款轻量级文档解析与格式转换工具,专注于将各类办公文档、PDF、HTML 等格式统一转换为 Markdown 格式。它的核心价值在于打破文档格式壁垒,让开发者能用统一的 Markdown 管道处理来自不同来源的文档内容。
核心功能
- 多格式输入支持:支持 PDF、DOCX、PPTX、XLSX、HTML、EPUB、CSV、JSON、XML 等 20+ 种常见文档格式
- 高质量 Markdown 输出:保留标题层级、列表、表格、代码块、链接等结构化信息
- 命令行与 Python API 双模式:既适合快速上手验证,也适合集成到自动化流水线
- 轻量无侵入:无需启动服务,纯本地运行,保护数据隐私
典型应用场景
| 场景 | 说明 |
|---|---|
| 知识库构建 | 将散落在 PDF、Word 中的技术文档批量转为 Markdown,导入知识管理工具 |
| RAG 数据预处理 | 为 LLM 检索增强生成准备干净的文本语料 |
| 内容迁移 | 从旧版 CMS 或 Wiki 系统导出文档,统一转换为 Markdown 格式 |
| 数据清洗 | 提取 PDF/Office 中的结构化数据,用于后续分析或入库 |
| 自动化流水线 | 在 CI/CD 中集成文档转换,实现文档即代码 |
Markitdown 工作流程
下图展示了 Markitdown 的核心工作流程:多种格式的文档输入后,经过统一的解析引擎,最终输出结构化的 Markdown 内容,供下游系统消费。
#mermaid-svg-Dq8TxJHwRr8KIVAa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Dq8TxJHwRr8KIVAa .error-icon{fill:#552222;}#mermaid-svg-Dq8TxJHwRr8KIVAa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Dq8TxJHwRr8KIVAa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .marker.cross{stroke:#333333;}#mermaid-svg-Dq8TxJHwRr8KIVAa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Dq8TxJHwRr8KIVAa p{margin:0;}#mermaid-svg-Dq8TxJHwRr8KIVAa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster-label text{fill:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster-label span{color:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster-label span p{background-color:transparent;}#mermaid-svg-Dq8TxJHwRr8KIVAa .label text,#mermaid-svg-Dq8TxJHwRr8KIVAa span{fill:#333;color:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .node rect,#mermaid-svg-Dq8TxJHwRr8KIVAa .node circle,#mermaid-svg-Dq8TxJHwRr8KIVAa .node ellipse,#mermaid-svg-Dq8TxJHwRr8KIVAa .node polygon,#mermaid-svg-Dq8TxJHwRr8KIVAa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .rough-node .label text,#mermaid-svg-Dq8TxJHwRr8KIVAa .node .label text,#mermaid-svg-Dq8TxJHwRr8KIVAa .image-shape .label,#mermaid-svg-Dq8TxJHwRr8KIVAa .icon-shape .label{text-anchor:middle;}#mermaid-svg-Dq8TxJHwRr8KIVAa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .rough-node .label,#mermaid-svg-Dq8TxJHwRr8KIVAa .node .label,#mermaid-svg-Dq8TxJHwRr8KIVAa .image-shape .label,#mermaid-svg-Dq8TxJHwRr8KIVAa .icon-shape .label{text-align:center;}#mermaid-svg-Dq8TxJHwRr8KIVAa .node.clickable{cursor:pointer;}#mermaid-svg-Dq8TxJHwRr8KIVAa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .arrowheadPath{fill:#333333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dq8TxJHwRr8KIVAa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Dq8TxJHwRr8KIVAa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dq8TxJHwRr8KIVAa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster text{fill:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa .cluster span{color:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Dq8TxJHwRr8KIVAa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Dq8TxJHwRr8KIVAa rect.text{fill:none;stroke-width:0;}#mermaid-svg-Dq8TxJHwRr8KIVAa .icon-shape,#mermaid-svg-Dq8TxJHwRr8KIVAa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dq8TxJHwRr8KIVAa .icon-shape p,#mermaid-svg-Dq8TxJHwRr8KIVAa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Dq8TxJHwRr8KIVAa .icon-shape .label rect,#mermaid-svg-Dq8TxJHwRr8KIVAa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dq8TxJHwRr8KIVAa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Dq8TxJHwRr8KIVAa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Dq8TxJHwRr8KIVAa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PDF
Markitdown
解析引擎
DOCX
PPTX
XLSX
HTML
EPUB
Markdown 输出
知识库
RAG 系统
内容迁移
数据清洗
② 环境准备:Python 环境搭建与依赖库安装
基础环境要求
- Python 3.10 及以上版本
- pip 包管理器
- 建议使用虚拟环境隔离项目依赖
安装步骤
第一步:创建虚拟环境(推荐)
bash
python -m venv markitdown-env
source markitdown-env/bin/activate # Linux/Mac
# 或 markitdown-env\Scripts\activate # Windows
第二步:安装 Markitdown 核心库
bash
pip install markitdown
第三步:安装文档解析依赖
根据你需要处理的文档类型,安装对应的解析引擎:
bash
# PDF 解析
pip install "markitdown[pdf]"
# DOCX 解析
pip install "markitdown[docx]"
# PPTX 解析
pip install "markitdown[pptx]"
# XLSX 解析
pip install "markitdown[xlsx]"
# 全部依赖一键安装
pip install "markitdown[all]"
验证安装
bash
python -c "from markitdown import MarkItDown; print('Markitdown 安装成功!')"
如果输出 Markitdown 安装成功!,说明环境已就绪。
③ 快速上手:使用命令行转换单个文件
Markitdown 提供了开箱即用的命令行工具,无需编写代码即可完成文档转换。
基本用法
bash
markitdown input.pdf > output.md
或者使用 -o 参数直接指定输出文件:
bash
markitdown input.pdf -o output.md
常用命令示例
转换 Word 文档
bash
markitdown 产品需求文档.docx -o prd.md
转换 PowerPoint 演示文稿
bash
markitdown 项目汇报.pptx -o report.md
转换 Excel 表格
bash
markitdown 销售数据.xlsx -o sales.md
转换 HTML 网页
bash
markitdown index.html -o page.md
查看帮助
bash
markitdown --help
输出示例:
Usage: markitdown [OPTIONS] INPUT_FILE
Options:
--version Show the version and exit.
-h, --help Show this message and exit.
实战小练习
找个本地的 PDF 或 Word 文件,丢进去试试:
bash
markitdown 示例文档.pdf -o 示例文档.md
cat 示例文档.md # 查看转换结果
你会看到原本复杂的排版被清晰地转成了 Markdown 格式,标题、列表、表格都得到了保留。我第一次跑的时候,最让我惊讶的是表格------原本在 PDF 里还得手动对齐的数据,一转眼就变成了规整的 Markdown 表格,省了至少半小时的手工整理时间。
④ 进阶实战:Python 代码调用与批量文件处理
当需要处理大量文件或集成到现有系统时,Python API 是更好的选择。
基础调用示例
python
from markitdown import MarkItDown
# 初始化转换器
md = MarkItDown()
# 转换单个文件
result = md.convert("report.docx")
print(result.text_content)
就这么三行代码,一个 Word 文档就变成了 Markdown 字符串,可以直接丢进下游管道。
批量处理文件夹
下图展示了使用 Python API 进行批量文档转换的完整流程:
#mermaid-svg-6jo2a84u0bwAbrVI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6jo2a84u0bwAbrVI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6jo2a84u0bwAbrVI .error-icon{fill:#552222;}#mermaid-svg-6jo2a84u0bwAbrVI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6jo2a84u0bwAbrVI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6jo2a84u0bwAbrVI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6jo2a84u0bwAbrVI .marker.cross{stroke:#333333;}#mermaid-svg-6jo2a84u0bwAbrVI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6jo2a84u0bwAbrVI p{margin:0;}#mermaid-svg-6jo2a84u0bwAbrVI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6jo2a84u0bwAbrVI .cluster-label text{fill:#333;}#mermaid-svg-6jo2a84u0bwAbrVI .cluster-label span{color:#333;}#mermaid-svg-6jo2a84u0bwAbrVI .cluster-label span p{background-color:transparent;}#mermaid-svg-6jo2a84u0bwAbrVI .label text,#mermaid-svg-6jo2a84u0bwAbrVI span{fill:#333;color:#333;}#mermaid-svg-6jo2a84u0bwAbrVI .node rect,#mermaid-svg-6jo2a84u0bwAbrVI .node circle,#mermaid-svg-6jo2a84u0bwAbrVI .node ellipse,#mermaid-svg-6jo2a84u0bwAbrVI .node polygon,#mermaid-svg-6jo2a84u0bwAbrVI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6jo2a84u0bwAbrVI .rough-node .label text,#mermaid-svg-6jo2a84u0bwAbrVI .node .label text,#mermaid-svg-6jo2a84u0bwAbrVI .image-shape .label,#mermaid-svg-6jo2a84u0bwAbrVI .icon-shape .label{text-anchor:middle;}#mermaid-svg-6jo2a84u0bwAbrVI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6jo2a84u0bwAbrVI .rough-node .label,#mermaid-svg-6jo2a84u0bwAbrVI .node .label,#mermaid-svg-6jo2a84u0bwAbrVI .image-shape .label,#mermaid-svg-6jo2a84u0bwAbrVI .icon-shape .label{text-align:center;}#mermaid-svg-6jo2a84u0bwAbrVI .node.clickable{cursor:pointer;}#mermaid-svg-6jo2a84u0bwAbrVI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6jo2a84u0bwAbrVI .arrowheadPath{fill:#333333;}#mermaid-svg-6jo2a84u0bwAbrVI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6jo2a84u0bwAbrVI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6jo2a84u0bwAbrVI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6jo2a84u0bwAbrVI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6jo2a84u0bwAbrVI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6jo2a84u0bwAbrVI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6jo2a84u0bwAbrVI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6jo2a84u0bwAbrVI .cluster text{fill:#333;}#mermaid-svg-6jo2a84u0bwAbrVI .cluster span{color:#333;}#mermaid-svg-6jo2a84u0bwAbrVI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6jo2a84u0bwAbrVI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6jo2a84u0bwAbrVI rect.text{fill:none;stroke-width:0;}#mermaid-svg-6jo2a84u0bwAbrVI .icon-shape,#mermaid-svg-6jo2a84u0bwAbrVI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6jo2a84u0bwAbrVI .icon-shape p,#mermaid-svg-6jo2a84u0bwAbrVI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6jo2a84u0bwAbrVI .icon-shape .label rect,#mermaid-svg-6jo2a84u0bwAbrVI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6jo2a84u0bwAbrVI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6jo2a84u0bwAbrVI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6jo2a84u0bwAbrVI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
输入目录
遍历文件
扩展名是否支持?
调用 MarkItDown.convert
跳过
转换成功?
写入 .md 文件
记录错误日志
还有文件?
输出统计结果
成功数 / 失败数
完整代码实现:
python
import os
from pathlib import Path
from markitdown import MarkItDown
def batch_convert(input_dir: str, output_dir: str):
"""批量转换目录下所有支持的文档"""
md = MarkItDown()
supported_ext = {'.pdf', '.docx', '.pptx', '.xlsx', '.html', '.epub'}
Path(output_dir).mkdir(parents=True, exist_ok=True)
for file_path in Path(input_dir).glob('*'):
if file_path.suffix.lower() in supported_ext:
try:
result = md.convert(str(file_path))
output_path = Path(output_dir) / f"{file_path.stem}.md"
output_path.write_text(result.text_content, encoding='utf-8')
print(f"转换成功: {file_path.name} -> {output_path.name}")
except Exception as e:
print(f"转换失败: {file_path.name} - {e}")
# 使用示例
batch_convert("./docs", "./output")
获取转换元数据
python
result = md.convert("meeting_notes.docx")
print(f"文档标题: {result.title}")
print(f"字符数: {len(result.text_content)}")
print(f"段落数: {result.text_content.count(chr(10) + chr(10))}")
⑤ 效果验证:PDF、Word、Excel 文档解析实测
为了让你直观感受 Markitdown 的解析能力,我拿三份典型文档做了实测------不是 demo 级别的玩具文件,是真实工作场景里常见的文档。
测试环境
- Markitdown 版本:0.1.0a5
- Python 版本:3.11
- 测试文档:标准办公文档
PDF 文档解析
输入:一份包含标题、正文、表格、脚注的 PDF 技术文档
python
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("技术白皮书.pdf")
print(result.text_content[:500])
输出效果:
- 标题层级完整保留(H1 到 H4)
- 正文段落正常分段
- 表格转换为 Markdown 表格格式
- 脚注转为普通文本,丢失超链接跳转
Word 文档解析
输入:一份包含多级列表、图片、表格的 DOCX 报告
python
result = md.convert("项目报告.docx")
print(result.text_content[:500])
输出效果:
- 多级列表完美保留(有序/无序)
- 表格结构完整
- 图片转为
占位符,需手动补充图片路径 - 加粗、斜体等文本样式保留
Excel 文档解析
输入:一份包含多个 Sheet 的销售数据表
python
result = md.convert("销售数据.xlsx")
print(result.text_content[:500])
输出效果:
- 每个 Sheet 作为一个独立章节
- 表头行识别为表格标题
- 数据行转换为 Markdown 表格行
- 合并单元格内容会重复出现在对应行
实测总结
| 文档类型 | 解析质量 | 主要注意事项 |
|---|---|---|
| 四星 | 脚注、页眉页脚可能丢失 | |
| Word | 五星 | 图片需手动处理路径 |
| Excel | 四星 | 合并单元格需额外清洗 |
| PPTX | 四星 | 演讲者备注会一并提取 |
⑥ 难点攻克:复杂表格与图片内容提取技巧
复杂表格处理
Markitdown 对标准表格支持良好,但遇到合并单元格、嵌套表格时,输出可能需要后处理。
场景一:合并单元格
python
from markitdown import MarkItDown
import re
md = MarkItDown()
result = md.convert("复杂表格.xlsx")
raw_md = result.text_content
# 后处理:去重合并单元格产生的重复内容
def deduplicate_cells(markdown_table: str) -> str:
lines = markdown_table.split('\n')
cleaned = []
for line in lines:
if '|' in line:
cells = line.split('|')
# 去除连续重复的单元格内容
deduped = [cells[0]]
for i in range(1, len(cells)):
if cells[i] != cells[i-1]:
deduped.append(cells[i])
cleaned.append('|'.join(deduped))
else:
cleaned.append(line)
return '\n'.join(cleaned)
print(deduplicate_cells(raw_md))
场景二:跨页表格合并
对于跨页的 PDF 表格,建议先使用 pdfplumber 等工具提取原始表格,再转为 Markdown:
python
import pdfplumber
# 先用 pdfplumber 提取表格
with pdfplumber.open("年报.pdf") as pdf:
all_tables = []
for page in pdf.pages:
tables = page.extract_tables()
all_tables.extend(tables)
# 再转为 Markdown 表格
def table_to_markdown(table):
if not table:
return ""
header = "| " + " | ".join(table[0]) + " |"
separator = "| " + " | ".join(["---"] * len(table[0])) + " |"
rows = ["| " + " | ".join(row) + " |" for row in table[1:]]
return "\n".join([header, separator] + rows)
for i, table in enumerate(all_tables):
print(f"### 表格 {i+1}")
print(table_to_markdown(table))
print()
图片内容提取
Markitdown 会将文档中的图片转为占位符,如需提取实际图片,可以结合 python-pptx 或 python-docx:
python
from pptx import Presentation
from pathlib import Path
def extract_images_from_pptx(pptx_path: str, output_dir: str):
"""从 PPTX 中提取所有图片"""
prs = Presentation(pptx_path)
Path(output_dir).mkdir(parents=True, exist_ok=True)
for slide_num, slide in enumerate(prs.slides, 1):
for shape_num, shape in enumerate(slide.shapes, 1):
if shape.shape_type == 13: # Picture
image = shape.image
ext = image.content_type.split('/')[-1]
filename = f"slide{slide_num}_img{shape_num}.{ext}"
with open(Path(output_dir) / filename, 'wb') as f:
f.write(image.blob)
print(f"提取: {filename}")
extract_images_from_pptx("演示文稿.pptx", "./extracted_images")
⑦ 扩展定制:自定义解析器与配置方法
Markitdown 的插件架构支持扩展自定义格式。搞过内部系统对接的朋友应该深有体会------总有些公司内部的专有格式是官方永远不可能支持的。这时候就得自己上手写解析器。
自定义解析器
Markitdown 使用 DocumentConverter 基类作为插件接口。你需要实现 accepts() 和 convert() 两个方法:
python
from markitdown._base_converter import DocumentConverter
from markitdown import MarkItDown
class CustomLogConverter(DocumentConverter):
"""自定义日志文件转换器"""
def accepts(self, source: str) -> bool:
"""定义该转换器支持的文件类型"""
return source.endswith('.log')
def convert(self, source: str) -> str:
with open(source, 'r', encoding='utf-8') as f:
content = f.read()
# 自定义解析逻辑:将日志转为 Markdown
lines = content.split('\n')
md_lines = []
for line in lines:
if line.startswith('[ERROR]'):
md_lines.append(f'> **错误**: {line[7:].strip()}')
elif line.startswith('[WARN]'):
md_lines.append(f'> **警告**: {line[6:].strip()}')
elif line.startswith('[INFO]'):
md_lines.append(f'- {line[5:].strip()}')
else:
md_lines.append(line)
return '\n'.join(md_lines)
# 注册并使用自定义转换器
md = MarkItDown()
md.register_converter(".log", CustomLogConverter())
result = md.convert("app.log")
print(result.text_content)
上周我就是用这套插件机制,把公司内部的一个自定义配置格式对接到了知识库系统里,总共不到 30 行代码。
配置输出格式
Markitdown 提供了灵活的配置选项来定制输出行为:
python
from markitdown import MarkItDown
from markitdown.config import ImageConfig
# 自定义图片输出配置
image_config = ImageConfig(
output_dir="my_images", # 指定图片输出文件夹
image_type="png", # 统一转换为 PNG 格式
prefix="doc_" # 给图片文件名添加前缀
)
md = MarkItDown(image_config=image_config)
result = md.convert("design_spec.docx")
# 图片自动整理到 my_images/doc_001.png, doc_002.png...
扩展:对接 LLM 预处理
python
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("合同.pdf")
# 为 LLM 准备结构化输入
llm_input = f"""
## 文档信息
- 文件名: 合同.pdf
- 字符数: {len(result.text_content)}
## 文档内容
{result.text_content[:4000]} # 截取前 4000 字符
## 分析要求
请提取以下信息:
1. 合同双方名称
2. 合同金额
3. 签署日期
4. 关键条款
"""
⑧ 排错指南:常见编码错误与依赖缺失解决方案
实际使用中踩坑是难免的。下面这几个是我自己和身边同事反复遇到的,记录在此供参考。
错误 1:ModuleNotFoundError: No module named 'markitdown'
原因:未安装 Markitdown 或未激活虚拟环境
解决方案:
bash
# 确认已安装
pip list | grep markitdown
# 如未安装
pip install markitdown
# 如使用虚拟环境,确保已激活
source markitdown-env/bin/activate # Linux/Mac
错误 2:ImportError: cannot import name 'MarkItDown'
原因:版本过旧或安装不完整
解决方案:
bash
# 升级到最新版本
pip install --upgrade markitdown
# 重新安装
pip uninstall markitdown -y
pip install markitdown
错误 3:PDF 解析报错 pdfminer.high_level.exceptions.PDFSyntaxError
原因:PDF 文件损坏或包含不兼容的特性
解决方案:尝试用其他 PDF 工具做预处理,或更换文档来源。对于扫描版 PDF,建议先用 OCR 工具处理后再交由 Markitdown 转换。
错误 4:中文乱码问题
原因:缺少中文字体或编码设置不正确
解决方案:
python
# 方案一:指定编码写入
with open("output.md", "w", encoding="utf-8") as f:
f.write(result.text_content)
# 方案二:安装中文字体(Linux 服务器)
# Debian/Ubuntu
sudo apt-get install fonts-noto-cjk
# CentOS/RHEL
sudo yum install google-noto-cjk-fonts
错误 5:大文件内存溢出
原因:文件过大,一次性加载到内存
解决方案:在处理前先检查文件大小,对超大文件做分块处理:
python
from pathlib import Path
max_size = 50 * 1024 * 1024 # 50MB
file_path = "超大文件.pdf"
if Path(file_path).stat().st_size > max_size:
raise ValueError("文件过大,请先拆分后再转换")
常见问题速查表
| 错误信息 | 可能原因 | 解决步骤 |
|---|---|---|
No module named 'markitdown' |
未安装 | pip install markitdown |
PDFSyntaxError |
PDF 损坏 | 更换文档或预处理 |
UnicodeDecodeError |
编码问题 | 指定 encoding='utf-8' |
MemoryError |
文件过大 | 拆分后分块处理 |
FileNotFoundError |
路径错误 | 检查文件路径是否存在 |
⑨ 性能优化:大文件处理策略与最佳实践
大文件处理策略
策略一:大小预检与限制
在处理前检查文件大小,避免巨量文件直接涌入内存。对所有输入文件统一做上限管控是最简单有效的第一道防线:
python
from pathlib import Path
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
def safe_convert(file_path: str):
size = Path(file_path).stat().st_size
if size > MAX_FILE_SIZE:
raise ValueError(f"文件 {Path(file_path).name} 大小 {size/1024/1024:.1f}MB,超出 {MAX_FILE_SIZE/1024/1024:.0f}MB 上限")
from markitdown import MarkItDown
md = MarkItDown()
return md.convert(file_path)
策略二:多进程并行处理
当文件数量多但单个文件不大时,用多进程并行是提升吞吐量的最直接手段:
python
import multiprocessing as mp
from pathlib import Path
from markitdown import MarkItDown
def convert_single_file(file_path: str) -> tuple:
"""单个文件转换任务"""
try:
md = MarkItDown()
result = md.convert(file_path)
output_path = Path(file_path).with_suffix('.md')
output_path.write_text(result.text_content, encoding='utf-8')
return (file_path, True, None)
except Exception as e:
return (file_path, False, str(e))
def parallel_batch_convert(file_list: list, workers: int = 4):
"""并行批量转换"""
with mp.Pool(processes=workers) as pool:
results = pool.map(convert_single_file, file_list)
success = sum(1 for _, ok, _ in results if ok)
failed = sum(1 for _, ok, _ in results if not ok)
print(f"转换完成:成功 {success} 个,失败 {failed} 个")
# 使用示例
files = [str(p) for p in Path("./docs").glob("*") if p.suffix in ['.pdf', '.docx']]
parallel_batch_convert(files, workers=mp.cpu_count() - 1)
性能优化最佳实践
| 优化项 | 建议 | 预期效果 |
|---|---|---|
| 内存限制 | 转换前检查文件大小,设置上限 | 避免 OOM |
| 并行度 | workers = cpu_count() - 1 |
充分利用 CPU |
| 缓存 | 对重复转换的文件使用 LRU 缓存 | 减少重复计算 |
| 输出压缩 | 使用 gzip 压缩大文件输出 |
节省磁盘空间 |
| 增量处理 | 只处理新增或修改的文件 | 减少不必要的工作 |
性能基准测试
python
import time
from pathlib import Path
from markitdown import MarkItDown
def benchmark(file_path: str):
md = MarkItDown()
start = time.time()
result = md.convert(file_path)
elapsed = time.time() - start
print(f"文件: {file_path}")
print(f"大小: {Path(file_path).stat().st_size / 1024:.1f} KB")
print(f"耗时: {elapsed:.2f} 秒")
print(f"输出: {len(result.text_content):,} 字符")
print(f"速度: {len(result.text_content) / elapsed:.0f} 字符/秒")
benchmark("测试文档.pdf")
⑩ 输出与集成:格式规范化与后续系统对接建议
输出格式规范化
为确保输出 Markdown 能被下游系统正确消费,建议进行规范化处理:
python
import re
from markitdown import MarkItDown
def normalize_markdown(text: str) -> str:
"""规范化 Markdown 输出"""
# 1. 统一换行符
text = text.replace('\r\n', '\n').replace('\r', '\n')
# 2. 压缩多余空行(保留最多一个空行)
text = re.sub(r'\n{3,}', '\n\n', text)
# 3. 确保标题前后有空行
text = re.sub(r'([^\n])\n(#{1,6}\s)', r'\1\n\n\2', text)
text = re.sub(r'(#{1,6}\s.*)\n([^\n#])', r'\1\n\n\2', text)
# 4. 代码块前后确保空行
text = re.sub(r'([^\n])\n```', r'\1\n\n```', text)
text = re.sub(r'```\n([^\n])', r'```\n\n\1', text)
return text.strip() + '\n'
md = MarkItDown()
result = md.convert("文档.docx")
normalized = normalize_markdown(result.text_content)
with open("规范化输出.md", "w", encoding="utf-8") as f:
f.write(normalized)
与知识库系统对接
对接 Obsidian
python
import yaml
from datetime import datetime
def to_obsidian_note(markdown_content: str, title: str, tags: list):
"""转换为 Obsidian 笔记格式"""
front_matter = {
'title': title,
'created': datetime.now().isoformat(),
'tags': tags,
'source': 'markitdown'
}
note = f"---\n{yaml.dump(front_matter, allow_unicode=True)}---\n\n"
note += markdown_content
return note
obsidian_note = to_obsidian_note(
result.text_content,
"项目需求文档",
["project", "requirements", "imported"]
)
对接 RAG 系统
python
def prepare_for_rag(markdown_content: str, chunk_size: int = 512):
"""将 Markdown 切分为适合 RAG 的文本块"""
from langchain.text_splitter import MarkdownTextSplitter
splitter = MarkdownTextSplitter(
chunk_size=chunk_size,
chunk_overlap=50
)
chunks = splitter.split_text(markdown_content)
documents = []
for i, chunk in enumerate(chunks):
documents.append({
"id": f"chunk_{i:04d}",
"text": chunk,
"metadata": {
"source": "markitdown",
"chunk_index": i
}
})
return documents
rag_docs = prepare_for_rag(result.text_content)
集成到 CI/CD 流水线
创建 .github/workflows/doc-convert.yml:
yaml
name: 文档自动转换
on:
push:
paths:
- 'docs/**/*.pdf'
- 'docs/**/*.docx'
jobs:
convert:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安装 Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: 安装依赖
run: |
pip install "markitdown[all]"
- name: 批量转换文档
run: |
python -c "
from pathlib import Path
from markitdown import MarkItDown
md = MarkItDown()
for f in Path('docs').glob('*.pdf'):
result = md.convert(str(f))
output = Path('docs') / f.with_suffix('.md').name
output.write_text(result.text_content, encoding='utf-8')
print(f'转换: {f.name}')
"
- name: 提交转换结果
run: |
git config user.name "doc-bot"
git config user.email "bot@example.com"
git add docs/*.md
git commit -m "自动转换文档为 Markdown" || echo "无变更"
git push
系统集成架构
下图展示了 Markitdown 从文档输入到最终系统集成的完整架构:
#mermaid-svg-hUFzMEe2L3D96pfY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hUFzMEe2L3D96pfY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hUFzMEe2L3D96pfY .error-icon{fill:#552222;}#mermaid-svg-hUFzMEe2L3D96pfY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hUFzMEe2L3D96pfY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hUFzMEe2L3D96pfY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hUFzMEe2L3D96pfY .marker.cross{stroke:#333333;}#mermaid-svg-hUFzMEe2L3D96pfY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hUFzMEe2L3D96pfY p{margin:0;}#mermaid-svg-hUFzMEe2L3D96pfY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hUFzMEe2L3D96pfY .cluster-label text{fill:#333;}#mermaid-svg-hUFzMEe2L3D96pfY .cluster-label span{color:#333;}#mermaid-svg-hUFzMEe2L3D96pfY .cluster-label span p{background-color:transparent;}#mermaid-svg-hUFzMEe2L3D96pfY .label text,#mermaid-svg-hUFzMEe2L3D96pfY span{fill:#333;color:#333;}#mermaid-svg-hUFzMEe2L3D96pfY .node rect,#mermaid-svg-hUFzMEe2L3D96pfY .node circle,#mermaid-svg-hUFzMEe2L3D96pfY .node ellipse,#mermaid-svg-hUFzMEe2L3D96pfY .node polygon,#mermaid-svg-hUFzMEe2L3D96pfY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hUFzMEe2L3D96pfY .rough-node .label text,#mermaid-svg-hUFzMEe2L3D96pfY .node .label text,#mermaid-svg-hUFzMEe2L3D96pfY .image-shape .label,#mermaid-svg-hUFzMEe2L3D96pfY .icon-shape .label{text-anchor:middle;}#mermaid-svg-hUFzMEe2L3D96pfY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hUFzMEe2L3D96pfY .rough-node .label,#mermaid-svg-hUFzMEe2L3D96pfY .node .label,#mermaid-svg-hUFzMEe2L3D96pfY .image-shape .label,#mermaid-svg-hUFzMEe2L3D96pfY .icon-shape .label{text-align:center;}#mermaid-svg-hUFzMEe2L3D96pfY .node.clickable{cursor:pointer;}#mermaid-svg-hUFzMEe2L3D96pfY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hUFzMEe2L3D96pfY .arrowheadPath{fill:#333333;}#mermaid-svg-hUFzMEe2L3D96pfY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hUFzMEe2L3D96pfY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hUFzMEe2L3D96pfY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hUFzMEe2L3D96pfY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hUFzMEe2L3D96pfY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hUFzMEe2L3D96pfY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hUFzMEe2L3D96pfY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hUFzMEe2L3D96pfY .cluster text{fill:#333;}#mermaid-svg-hUFzMEe2L3D96pfY .cluster span{color:#333;}#mermaid-svg-hUFzMEe2L3D96pfY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hUFzMEe2L3D96pfY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hUFzMEe2L3D96pfY rect.text{fill:none;stroke-width:0;}#mermaid-svg-hUFzMEe2L3D96pfY .icon-shape,#mermaid-svg-hUFzMEe2L3D96pfY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hUFzMEe2L3D96pfY .icon-shape p,#mermaid-svg-hUFzMEe2L3D96pfY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hUFzMEe2L3D96pfY .icon-shape .label rect,#mermaid-svg-hUFzMEe2L3D96pfY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hUFzMEe2L3D96pfY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hUFzMEe2L3D96pfY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hUFzMEe2L3D96pfY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出层
处理层
转换层
输入层
PDF文档
Office文档
HTML网页
Markitdown
解析引擎
格式规范化
文本分块
Obsidian 知识库
RAG 检索系统
CI/CD 流水线
本地 Markdown 文件
总结
跑完 Markitdown 从安装到集成 CI/CD 的整个流程,我最直接的感受是------这玩意儿确实省事。以前处理文档转换,PDF 一个工具、Word 一个库、Excel 另写脚本,每种格式都得搞一套,维护成本不低。Markitdown 一个 convert() 就把这些全包了,输出的 Markdown 质量也够用,表格和标题层级基本不需要手修。
当然它也不是万能的。合并单元格的表格会出重复内容,扫描版 PDF 需要先 OCR,图片也只是给个占位符。这些场景如果能接受,它就是你文档流水线里一块可靠的积木------无论是喂给 RAG 做检索增强,还是导入 Obsidian 做知识库,还是挂上 GitHub Actions 做文档即代码,都能直接对接。
积压的 PDF 和 Office 文档该处理了,开干吧。