Image: AI generated
仅凭
pip install markitdown,PDF、Word 文档、Excel 表格和 PowerPoint 演示文稿就能转换成 markdown。但如果喂给它一份纯扫描图片的 PDF——比如一份扫描合同——得到的却是一份空文档。本文分两条线展开:为什么以及如何使用 MarkItDown,以及如何在这份信任崩溃的确切位置——纯图片 PDF——接入 OCR。
为什么要使用 MarkItDown——agent-operable 这个条件
MarkItDown 是微软开源的 CLI/Python 库,可将 PDF、Word、Excel、PowerPoint、HTML、图片和音频文件转换为 markdown。
先从 agent-operable(智能体可操作的状态)这个概念说起。要让智能体自主处理一份文档,该文档必须处于机器能够结构化解析的状态——而不仅仅是人类觉得可读的状态。这并非只适用于代码的条件。埋藏在 PDF 或 Excel 里的业务规则,对智能体来说等于不存在,因为人类必须打开它才能知道里面写了什么。这正是我在《构建 Agent-Operable 系统》中列出的三个条件——可读、可验证、可持久——中的第一个:“无噪声的可读性”。
那为什么偏偏是 MarkItDown 呢?你完全可以自己拼接第三方解析器(PyPDF2、python-docx)来实现同样的转换。区别在于是谁做的。docx、xlsx、pptx 都是 OOXML——微软自己拥有的规范。当规范的所有者亲自为自家格式打造转换器时,边缘情况(合并单元格、嵌套表格、脚注)在结构上比第三方解析器更可靠。如果转换本身不可信,那么在其之上为了达到 agent-operable 状态所做的一切都毫无意义。这就是使用 MarkItDown 的理由——对于微软自家格式,这份信任是默认自带的。
问题在于,这份信任在 PDF,尤其是纯图片 PDF 上并不会延续下去。本文后半部分专注于用 OCR 补上这个缺口。
安装
基础安装
pip install markitdown
仅基础安装只能处理纯文本、HTML 等轻量格式。要转换 PDF 和 Office 文档,需要一并安装格式专属的扩展项。
按格式安装扩展项
pip install 'markitdown[pdf]' # PDF (pdfminer.six + pdfplumber)
pip install 'markitdown[docx]' # Word
pip install 'markitdown[pptx]' # PowerPoint
pip install 'markitdown[xlsx]' # Excel
pip install 'markitdown[all]' # everything above, plus audio transcription, YouTube captions, Azure Document Intelligence, etc.
如果本文只需要处理 PDF,markitdown[pdf] 就够用了。
基本用法
CLI
markitdown example.pdf # print markdown to stdout
markitdown example.pdf -o example.md # save to a file
cat example.pdf | markitdown # read from stdin (pipe)
Python API
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("example.pdf")
print(result.markdown)
两种方式都会根据文件扩展名或 MIME 类型自动选择转换器。当没有扩展名信息时——比如从标准输入读取——可以用 -x(--extension)或 -m(--mime-type)提供提示。
PDF 转换的实际行为——只读取文本层
MarkItDown 的 PDF 转换器(packages/markitdown/src/markitdown/converters/_pdf_converter.py)首先用 pdfplumber 检测每一页的表格/表单结构,如果不是表格则回退到 page.extract_text()。如果 pdfplumber 失败,则回退到 pdfminer.six。
这两个库都是读取 PDF 内部已存在文本层的工具。如果某一页只是一张扫描图片、没有文本层,extract_text() 会返回空字符串,MarkItDown 的输出实际上也就是空的。基础包里根本没有渲染图片或识别文字的代码路径。
换言之,这是两个不同的问题:
- 带文本层的 PDF(从 Word 导出的 PDF、大多数电子合同等)——基础安装就能很好地工作。微软自家格式转换所带来的那份信任在这里同样延续。
- 没有文本层的 PDF(扫描仪扫出的文档、以图片形式嵌入的照片)——基础安装只会返回空结果。Agent Operable 的"可读性"条件在这里断裂。
OCR 设置方法——修补断裂信任的地方
从这里开始是本文的第二条线。MarkItDown 在自家格式上确保的那份信任,在纯图片 PDF 上必须另行搭建。
1. 安装插件
OCR 支持位于一个独立的包 markitdown-ocr 中。它位于同一个仓库(microsoft/markitdown)里,但不包含在 pip install markitdown 中。
pip install markitdown-ocr
pip install openai # an OpenAI-compatible client — this plugin has no OCR model of its own
安装后确认插件已被识别。
markitdown --list-plugins
# * ocr (package: markitdown_ocr)
2. 不可或缺的东西——LLM Vision 客户端
markitdown-ocr 不内置 Tesseract、PaddleOCR 之类的传统 OCR 引擎。它实际做的事只是把图片进行 base64 编码,然后向兼容 OpenAI 的 chat.completions API(例如 gpt-4o)请求"从这张图片中提取文字"(_ocr_service.py 中的 LLMVisionOCRService)。因此,必须拥有能调用视觉模型的 API 密钥,OCR 才能真正运作。
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(), # reads OPENAI_API_KEY from the environment
llm_model="gpt-4o",
)
result = md.convert("scanned_contract.pdf")
print(result.markdown)
Azure OpenAI 等任何兼容 OpenAI 的客户端都可以照样传入。
from openai import AzureOpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=AzureOpenAI(
api_key="...",
azure_endpoint="https://<resource>.openai.azure.com/",
api_version="2024-02-01",
),
llm_model="gpt-4o",
)
3. CLI 做不到——通过实际代码确认的事实
markitdown-ocr 的 README 给出了如下命令作为示例:
markitdown document.pdf --use-plugins --llm-client openai --llm-model gpt-4o
但直接查看 MarkItDown 核心的 CLI 参数定义(packages/markitdown/src/markitdown/__main__.py)后发现,--llm-client 或 --llm-model 这两个标志根本不存在。CLI 支持的选项只有 -o(输出文件)、-x(扩展名提示)、-m(MIME 提示)、-c(字符集)、-d/--use-docintel、--use-cu、-p/--use-plugins、--list-plugins、--keep-data-uris,MarkItDown 构造函数收到的也只有 enable_plugins=args.use_plugins。也就是说,README 里的 CLI 示例是一个实际跑不通的文档错误。
OCR 只能通过 Python API 使用——必须直接传入 llm_client/llm_model。没有办法通过 CLI 传递 API 密钥。
4. 运作原理——嵌入图片 OCR 与整页 OCR 回退
PdfConverterWithOCR(_pdf_converter_with_ocr.py)分两个阶段运作。
- 优先处理嵌入图片:当页面中文字与图片混杂时(例如正文中间插入了扫描的签名图片),只裁切出图片发送给 LLM Vision,并按其原始 Y 坐标位置与周围文字重新交织,保持阅读顺序。
- 整页 OCR 回退:如果文本提取结果整体为空(整份扫描 PDF),则将整页渲染为 300dpi 的 PNG,逐页发送给 LLM Vision(
_ocr_full_pages)。
提取出的文本总是以下列格式包裹输出。
*[Image OCR]
<extracted text>
[End OCR]*
5. 用自定义提示词提升准确度
默认提示词是"提取这张图片中的所有文字,保持原始版面和顺序"。对于表格较多、竖排文字,或掺有印章、签名的文档,可以用 llm_prompt 直接覆盖提示词来提升准确度。
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(),
llm_model="gpt-4o",
llm_prompt="Extract all text from this image, but preserve table structure as markdown tables.",
)
6. 批量处理多个文件
一次性处理整个扫描 PDF 文件夹时,每个文件都会触发一次 API 调用——需要对每次转换单独捕获异常,避免一次失败拖垮整批处理。
from pathlib import Path
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model="gpt-4o")
for pdf_path in Path("scans/").glob("*.pdf"):
try:
result = md.convert(str(pdf_path))
pdf_path.with_suffix(".md").write_text(result.markdown, encoding="utf-8")
except Exception as e:
print(f"failed: {pdf_path.name} — {e}")
7. 只安装插件而不提供 llm_client 会怎样?
插件会加载,但 OCR 会悄悄关闭。_plugin.py 里的 register_converters() 只有在 llm_client 和 llm_model 同时存在时才会创建 LLMVisionOCRService,否则会以 ocr_service=None 注册转换器。在这种状态下,会自动回退到仅提取文本层的基础行为,既不报错也不警告。“装了插件但 OCR 不起作用"的问题大多都源于这一点。
8. 甚至会尝试修复损坏的 PDF
当 pdfplumber 和 pdfminer 完全无法打开某个 PDF 时(例如 EOF 被截断的文件),markitdown-ocr 会回退到直接用 PyMuPDF(fitz)渲染页面并重试。如果这条路径也失败,就会在结果中留下 *[Error: Could not process scanned PDF]*。
9. 成本与速度
由于是把整页发送给视觉模型,处理页数越多的扫描件,视觉模型的 API 调用成本和延迟就越是按页数比例累积。300dpi 渲染每张图片消耗的 token 并不少,因此在把成百上千页的文档整份跑完之前,先用几页样本估算质量和成本更为稳妥。
OCR 结果验证 (Verifier)——另行处理
到目前为止的设置都是关于如何"开启"OCR。但开启了不代表结果就可信——在目前这种即便 llm_client 未设置、即便页面为空也会静默通过的结构下,并不存在判断 OCR 输出是否真正可信的确定性流程。
这一流程本文不涉及。如何将"LLM 生成 → 确定性工具判定 → 反馈 → 重复"这一 Symbolic Feedback Loop 原则应用到 OCR 输出上,留给另一篇文章/另一个工具处理。
实战检查清单
- 如果只处理带文本层的普通 PDF,基础的
markitdown[pdf]安装就足够了,不需要 OCR 插件。 - 如果必须处理扫描件或照片 PDF,
markitdown-ocr加上 OpenAI(或兼容)API 密钥是必需的。没有与 Tesseract 等免费本地传统 OCR 引擎的集成。 - 如果 OCR 结果为空,首先要怀疑的是是否真的传入了
llm_client/llm_model。由于是静默跳过,日志中不会显示任何异常。 - 仅靠 CLI 无法开启 OCR。必须通过调用
MarkItDown(enable_plugins=True, llm_client=..., llm_model=...)的 Python 脚本。 - 目前还没有以机器方式判断结果可信度的手段——现阶段需要人工抽样检查。
总结表
| 情况 | 所需条件 | 备注 |
|---|---|---|
| 带文本层的 PDF | pip install markitdown[pdf] | pdfplumber/pdfminer 提取文本 |
| 部分嵌入图片需要 OCR | markitdown-ocr + LLM Vision 客户端 | 只裁切图片进行 OCR,与文本交织 |
| 整页扫描(整页都是图片) | markitdown-ocr + LLM Vision 客户端 | 整页渲染为 300dpi 后进行 OCR |
| 损坏的 PDF | 同上(自动回退) | 用 PyMuPDF 渲染重试 |
未设置 llm_client | — | 静默跳过 OCR,不报错 |
在 CLI 中指定 --llm-client | 不可能(实际不存在该标志) | 仅能通过 Python API |
| 判断 OCR 结果可靠性 | (计划中) | 交由另一个 verifier 处理 |
相关文章
- 《构建 Agent-Operable 系统》 — 可读、可验证、可持久这三个条件,以及 MarkItDown 所填补的位置
- 《Agent Operable Codebase》 — 将同一原则应用于代码的案例
参考文献
- microsoft/markitdown — 仓库根目录
_pdf_converter.py— 基础 PDF 转换器(仅文本层提取)__main__.py— CLI 参数定义(确认--llm-client/--llm-model不存在)markitdown-ocrREADME_pdf_converter_with_ocr.py_ocr_service.py_plugin.py- markitdown pyproject.toml — extras 列表
变更历史
- 2026-07-09: 初版 — 增加 agent-operable 框架、以 OCR 设置作为后半部分重点扩充、加入 verifier 占位小节