为什么需要 MarkItDown
把 Word、PDF、Excel 变成 AI 能读懂的纯文本 —— 这就是 116K 开发者的选择
AI 的"阅读障碍"
你用 ChatGPT 写代码、让 Claude 分析文档。但你有没有想过:AI 其实看不懂你的 Word 文件。它只能读纯文本。
大语言模型(LLM)"说" Markdown 不是巧合 —— 它们在海量 Markdown 文本上训练过。这意味着 Markdown 是喂给 AI 的最高效格式:结构清晰,token 消耗最少。
没有 MarkItDown 的世界 vs 有它的世界
收到一个 .docx 文件
手动打开、复制文本
粘贴给 AI,格式全乱
AI 理解不了表格
一行命令
完整的 Markdown 输出
AI 完美理解
它能转换什么?
MarkItDown 是一个"万能翻译器",把 15+ 种文件格式统一翻译成 Markdown。
含表格提取、表单识别,双引擎(pdfplumber + pdfminer)
Word (.docx)
保留标题层级、表格结构,用 mammoth 转 HTML 再转 Markdown
Excel (.xlsx)
每个 Sheet 变成独立的 Markdown 表格
PowerPoint (.pptx)
逐页转换,保留图片 alt 文字、图表数据、演讲者备注
图片 / 音频
提取 EXIF 元数据,还能用 LLM 生成图片描述、转录音频
HTML / URL
网页、YouTube 字幕、Wikipedia、RSS 订阅源都能处理
架构总览:它是怎么工作的
用户层
核心引擎
转换器家族
一次转换的完整旅程
为什么 MarkItDown 选择 Markdown 而不是 HTML 或 JSON 作为输出格式?
如果有人把一个 PDF 文件重命名为 .docx,MarkItDown 会怎样?
五分钟上手
安装、命令行使用、Python 调用 —— 三种方式,总有一种适合你
安装:一行命令搞定
就像装一个 App 一样简单。在 终端 里输入:
# 安装所有格式的支持(推荐新手)
pip install "markitdown[all]"
# 或者只装你需要的格式
pip install "markitdown[pdf, docx, pptx]"
方括号里的 [all] 表示安装所有可选依赖,让它能处理所有文件格式
如果你只需要处理 PDF、Word 和 PPT,可以只装这三个格式
pip 是 Python 的"应用商店"。方括号 [all] 是 pip 的"套餐"语法 —— 表示安装这个包的"全家桶"版本。
命令行:转换一个文件有多简单
# 基本用法:转换后输出到屏幕
markitdown report.pdf
# 保存到文件
markitdown report.pdf -o report.md
# 管道操作:从其他命令读取输入
cat report.pdf | markitdown
# 使用 Azure 文档智能服务
markitdown report.pdf -d -e "<your-endpoint>"
最简单的用法,转换结果直接显示在终端里
-o 参数指定输出到文件,就像"另存为"
管道符 | 把前一个命令的输出传给后一个命令
用微软 Azure 云服务来处理,效果更好但需要付费
Python 代码调用:最灵活的方式
如果你在写自己的程序,需要在代码里调用 MarkItDown:
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("季度报告.xlsx")
print(result.text_content)
从 markitdown 包里导入主类
创建一个 MarkItDown 实例,就像打开了一个"翻译机"
把文件喂给 convert() 方法,它会自动识别类型并转换
打印转换后的 Markdown 文本
用 AI 增强:让图片和音频也能"说话"
传入一个 LLM 客户端,MarkItDown 就能让 AI 描述图片内容:
from markitdown import MarkItDown
from openai import OpenAI
client = OpenAI()
md = MarkItDown(
llm_client=client,
llm_model="gpt-4o",
)
result = md.convert("产品截图.jpg")
print(result.text_content)
导入 OpenAI 客户端,用来调用 AI 模型
创建一个 OpenAI 客户端(会自动读取你的 API Key)
创建 MarkItDown,并告诉它用哪个 AI 来看图说话
指定用 gpt-4o 模型(它的"视觉"能力很强)
转换图片时,AI 会自动生成文字描述
一次转换的"后台对话"
当你调用 md.convert("报告.pptx") 时,代码内部发生了什么?想象几个角色在对话:
你有一个本地 PDF 文件和一个网页 URL,想分别转换。你应该:
你用 MarkItDown 转换一个包含产品截图的 PPTX,但输出只有 "图片.jpg" 这样的占位文字。最可能的原因是?
转换器引擎揭秘
理解 accepts/convert 模式、优先级排序和 StreamInfo —— MarkItDown 的"小脑"
所有转换器的"老祖宗":DocumentConverter
想象一个基类是一份合同。每个转换器都必须签两份"承诺书":
class DocumentConverter:
def accepts(self, file_stream,
stream_info, **kwargs) -> bool:
"我能处理这个文件吗?"
raise NotImplementedError
def convert(self, file_stream,
stream_info, **kwargs):
"转换它!"
raise NotImplementedError
DocumentConverter 是所有转换器的基类
accepts() —— "举手报到":给我看看文件信息,如果我会处理就返回 True
NotImplementedError 表示"子类必须重写这个方法"
convert() —— "真刀真枪":把文件转成 Markdown
两个方法的参数完全一样,保证调用安全
StreamInfo:文件的"身份证"
每个文件在被转换前,都会拿到一张"身份证",上面记录了引擎知道的所有信息:
@dataclass(kw_only=True, frozen=True)
class StreamInfo:
mimetype: Optional[str] = None
extension: Optional[str] = None
charset: Optional[str] = None
filename: Optional[str] = None
local_path: Optional[str]= None
url: Optional[str] = None
@dataclass 自动生成构造函数,frozen=True 表示不可修改
mimetype:文件的 MIME 类型,如 "application/pdf"
extension:文件后缀,如 ".docx"
charset:文本编码,如 "utf-8"
filename / local_path / url:来源信息
frozen=True 意味着 StreamInfo 是不可变的。想更新?用 copy_and_update() 创建新副本。这保证了多转换器并发时的安全性。
优先级:谁先来,谁后来
引擎按照"专业优先、通用兜底"的原则排序。数值越小,优先级越高:
0.0
专业转换器(PdfConverter、DocxConverter 等)—— 先试它们
9.0
第三方插件 —— 比通用转换器优先,可以覆盖内置行为
10.0
通用兜底(PlainTextConverter、HtmlConverter、ZipConverter)—— 最后试
# 排序后逐个尝试
sorted_registrations = sorted(
self._converters,
key=lambda x: x.priority
)
for converter_registration in sorted_registrations:
_accepts = converter.accepts(
file_stream, stream_info, **_kwargs
)
if _accepts:
res = converter.convert(
file_stream, stream_info, **_kwargs
)
if res is not None:
return res
按优先级排序所有已注册的转换器
逐个尝试,问它们"你能处理这个文件吗?"
第一个说"能"的,就让它来转换
如果转换成功(结果不为 None),立刻返回
错误处理:失败了不放弃
如果第一个匹配的转换器失败了怎么办?引擎不会立刻报错,而是继续尝试下一个:
成功路径
转换器 accepts() = True → convert() 成功 → 直接返回结果
单转换器失败
convert() 抛异常 → 记录到 failed_attempts → 继续试下一个转换器
全军覆没
所有转换器都不行 → 抛出 FileConversionException,报告所有失败原因
无人认领
没有任何转换器 accepts() = True → 抛出 UnsupportedFormatException
结果归一化:统一输出格式
不管哪个转换器输出的,最终都会经过"保洁"处理:
# 归一化内容
res.text_content = "\n".join(
[line.rstrip()
for line in re.split(r"\r?\n",
res.text_content)]
)
res.text_content = re.sub(
r"\n{3,}", "\n\n",
res.text_content
)
第一步:去掉每行末尾的多余空格
处理 Windows(\r\n)和 Unix(\n)换行符差异
第二步:把 3 个以上的连续空行压缩成 2 个
确保输出格式干净整洁
找 Bug 挑战
下面这段代码有一个潜在问题,看看你能不能找到:
这段转换器注册代码有什么隐藏风险?
def register_converter(self, converter, *, priority=0.0):
self._converters.insert(0, ConverterRegistration(
converter=converter, priority=priority
))
如果一个 PDF 文件被 PdfConverter.accepts() 认领,但 convert() 时抛了异常,引擎会怎样?
StreamInfo 被设计为 frozen=True(不可变)。如果你需要更新它的某个字段,应该怎么做?
文件格式大通关
深入 PDF、Word、Excel、PowerPoint 转换器的内部实现
17 个内置转换器全家福
每个转换器都是某个文件格式的"专家":
PDF 转换器:双引擎策略
PDF 是最复杂的格式。MarkItDown 用了一个聪明的"双引擎"策略:
分析每个文字的位置坐标,把对齐的文字识别为表格行和列。特别擅长处理无边框表格和表单。
更擅长处理连续文本,保持正确的段落间距和阅读顺序。当 pdfplumber 不擅长时,它来兜底。
# 逐页判断是否是表单
with pdfplumber.open(pdf_bytes) as pdf:
for page_idx, page in enumerate(pdf.pages):
page_content = _extract_form_content(
page
)
if page_content is not None:
markdown_chunks.append(
page_content
)
page.close() # 立即释放内存
用 pdfplumber 打开 PDF,逐页分析
对每一页,尝试用表单提取方法
如果这一页有表格/表单结构,就用 pdfplumber 的结果
每一页处理完立刻关闭,释放内存(重要!)
page.close() 立刻释放 pdfplumber 的缓存对象。这样无论 PDF 有 10 页还是 1000 页,内存使用量保持恒定。这是一个流式处理的典型实践。
Word 转换器:两段接力
DocxConverter 用了一个优雅的"中间人"策略 —— 先转 HTML,再转 Markdown:
读取 .docx 文件
mammoth 转 HTML
HtmlConverter 转 Markdown
class DocxConverter(HtmlConverter):
def convert(self, file_stream,
stream_info, **kwargs):
pre_process_stream = pre_process_docx(
file_stream
)
return self._html_converter.convert_string(
mammoth.convert_to_html(
pre_process_stream,
style_map=style_map
).value,
**kwargs,
)
DocxConverter 继承 HtmlConverter,复用 HTML 转 Markdown 的能力
先对 docx 做预处理(修复一些格式问题)
用 mammoth 库把 docx 转成 HTML
然后把 HTML 交给 HtmlConverter 转成 Markdown —— 完美的接力
Excel 和 PowerPoint 的转换策略
Excel:pandas 做桥梁
用 pandas 读取每个 Sheet → 转 HTML 表格 → HtmlConverter 转 Markdown 表格。每个 Sheet 独立一个标题。
PPT:逐页遍历
遍历每张幻灯片的 shapes(形状):图片、表格、图表、文本框、分组形状。按位置排序,从上到下、从左到右。
ZIP:递归解包
解压 ZIP → 对每个内部文件递归调用 markitdown.convert_stream() → 拼合所有结果。一个 ZIP 包含 10 个 PDF?全部转换!
转换策略连连看
把每个转换器拖到它使用的"中间格式"上:
mammoth → HTML → Markdown(两段接力)
pandas DataFrame → HTML → Markdown(数据做桥)
分析文字 X/Y 坐标 → 直接生成 Markdown 表格
DocxConverter 为什么选择继承 HtmlConverter?
PDF 转换器为什么在处理每一页后立刻调用 page.close()?
插件系统与 MCP
扩展 MarkItDown 的能力边界 —— 自定义转换器和 AI 工具集成
插件:像装 App 一样扩展功能
MarkItDown 的插件系统就像手机的应用商店。任何人都可以写一个新转换器,安装后自动生效:
在包的配置里声明 markitdown.plugin entry point
安装后,entry point 自动注册到 Python 环境
MarkItDown 自动发现并加载所有已安装的插件转换器
插件加载:懒加载 + 容错
def _load_plugins():
global _plugins
if _plugins is not None:
return _plugins # 已加载过
_plugins = []
for entry_point in entry_points(
group="markitdown.plugin"
):
try:
_plugins.append(
entry_point.load()
)
except Exception:
warn("Plugin failed to load")
return _plugins
全局变量 _plugins 缓存已加载的插件,避免重复加载
如果已经加载过,直接返回缓存结果
查找所有注册了 "markitdown.plugin" 的 Python 包
尝试加载每个插件
某个插件失败不会影响其他插件 —— 容错设计
实战:一个 RTF 插件的完整代码
这是 MarkItDown 官方示例插件,只有 40 行代码,给 MarkItDown 加上了 RTF(富文本格式)支持:
from markitdown import (
MarkItDown, DocumentConverter,
DocumentConverterResult, StreamInfo,
)
def register_converters(
markitdown: MarkItDown, **kwargs
):
markitdown.register_converter(
RtfConverter()
)
class RtfConverter(DocumentConverter):
def accepts(self, file_stream,
stream_info, **kwargs) -> bool:
ext = (stream_info.extension
or "").lower()
return ext in [".rtf"]
def convert(self, file_stream,
stream_info, **kwargs):
encoding = stream_info.charset
data = file_stream.read()
.decode(encoding)
return DocumentConverterResult(
markdown=rtf_to_text(data)
)
导入 MarkItDown 的核心接口
register_converters() 是插件的入口函数,MarkItDown 会自动调用它
注册一个 RtfConverter 实例
accepts():只接受 .rtf 后缀的文件
convert():读取文件内容,解码文本
用 striprtf 库把 RTF 转成纯文本,作为 Markdown 返回
三步走:(1) 继承 DocumentConverter,(2) 实现 accepts() 和 convert(),(3) 在 register_converters() 里注册。你的插件就能处理全新的文件格式了。
markitdown-ocr:用 AI 看懂图片中的文字
官方 OCR 插件利用 LLM 的视觉能力,从 PDF/DOCX/PPTX 的嵌入图片中提取文字:
MCP:让 AI 工具直接调用 MarkItDown
MCP 让 MarkItDown 变成 AI 助手的"手"和"眼"。Claude、Cursor 等 AI 工具可以通过 MCP 直接调用 MarkItDown:
一个独立的 MCP 服务器,AI 工具通过它调用 MarkItDown 的转换能力。你的 AI 助手瞬间就能"读懂"任何文件。
在 Claude Desktop 或 Cursor 的配置里加一行 MCP 服务器地址,AI 就能自动转换你拖进去的文件。
# Claude Desktop 配置
{
"mcpServers": {
"markitdown": {
"command": "uvx",
"args": ["markitdown-mcp"]
}
}
}
在 Claude Desktop 的 MCP 配置文件中添加
用 uvx(Python 包运行工具)启动 markitdown-mcp 服务
之后 Claude 就能自动转换你发来的任何文件了
你想写一个 MarkItDown 插件来处理 .svg 文件。你的插件需要实现什么?
如果一个第三方插件有 Bug 导致加载失败,MarkItDown 会怎样?
实战:构建你的转换管道
用 MarkItDown 搭建真实场景的文件处理管道 —— 从批量转换到 AI 增强
场景一:批量转换文件夹
假设你的老板发来一个压缩包,里面有 50 份季度报告(混合 PDF、Word、Excel)。你需要把它们全部转成 Markdown 存入知识库。
from markitdown from pathlib import Path
md = MarkItDown()
output_dir = Path("markdown_output")
output_dir.mkdir(exist_ok=True)
# 转换 ZIP 包内所有文件
result = md.convert("季度报告.zip")
# 或者遍历文件夹
for f in Path("reports/").glob("*.*"):
if f.suffix in [".pdf", ".docx", ".xlsx"]:
r = md.convert(str(f))
out = output_dir / (f.stem + ".md")
out.write_text(r.text_content,
encoding="utf-8")
print(f"Converted: {f.name}")
创建输出目录(如果不存在)
方案 A:直接转换 ZIP,ZipConverter 会递归处理内部所有文件
方案 B:遍历文件夹,过滤支持的文件类型
对每个文件调用 convert(),自动识别格式
把结果保存为 .md 文件,文件名保持一致
场景二:AI 增强的文档分析管道
把 MarkItDown 和 RAG 结合,打造智能文档问答系统:
场景三:命令行高手的工作流
# 提示文件类型(从管道读取时有用)
cat data.bin | markitdown -x .pdf
# 指定 MIME 类型
markitdown data.file -m application/pdf
# 列出所有已安装的插件
markitdown --list-plugins
# 启用插件转换
markitdown --use-plugins document.rtf
# 保留 base64 图片数据
markitdown slides.pptx --keep-data-uris
-x 参数提示文件后缀(从管道读取时没有文件名)
-m 参数指定 MIME 类型(比后缀更精确)
查看有哪些第三方插件已安装
启用第三方插件(默认是关闭的)
保留图片的 base64 编码数据(默认会截断)
安全提醒:能力越大,责任越大
MarkItDown 能访问当前进程权限下的所有资源。在处理不受信任的文件时,请遵循以下原则:
消毒输入
不要把不受信任的用户输入直接传给 MarkItDown。在服务端应用中,必须先验证和限制文件路径、URI 方案。
使用最窄的 API
如果你的应用只需要读取本地文件,调用 convert_local() 而不是万能的 convert()。这样 MarkItDown 不会去请求网络。
限制网络访问
convert() 可以处理 HTTP URL 和 data URI。在安全环境中,应该阻止访问私有地址、环回地址和元数据服务地址。
从最安全到最灵活:convert_stream() > convert_local() > convert_response() > convert_uri() > convert()。选择你能接受的最窄范围。
完整实战对话:构建 RAG 管道
假设你是一个 vibe coder(用 AI 工具写代码),你要和 AI 助手配合搭建一个文档问答系统: