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の中に埋もれたビジネスルールは、人間が開かない限りエージェントにとって存在しないのと同じだ。Building Agent-Operable Systemsで整理した三条件(読める・検証できる・永続する)のうち最初の条件、「ノイズなしで読める」がここに当たる。
ではなぜ他でもなくMarkItDownなのか。サードパーティのパーサー(PyPDF2、python-docxを自前で組み合わせる)でも同じ変換は作れる。違いは誰が作ったかにある。docx・xlsx・pptxはいずれもマイクロソフト自身が所有するOOXML仕様だ。仕様の所有者が自社フォーマット向けのコンバーターを作れば、結合セル・入れ子のテーブル・脚注といったエッジケースはサードパーティのパーサーよりも構造的に信頼できる。文書をagent-operableな状態に変える最初の段階で、変換そのものの信頼性が揺らげば、その上に積み上げるものすべてが無意味になる。MarkItDownを使う理由はここにある — マイクロソフト自社フォーマットについては、この信頼がデフォルトで確保されている。
問題は、この信頼がPDF、特に画像だけのPDFにはそのまま引き継がれないことだ。この記事の後半は、その隙間をOCRで埋めることに集中する。
インストール
基本インストール
pip install markitdown
基本インストールだけではテキストやHTMLなど軽量なフォーマットしか扱えない。PDFやOffice文書を変換するには、フォーマット別の拡張(extra)を合わせてインストールする必要がある。
フォーマット別拡張のインストール
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(スキャナーで取り込んだ文書、写真を画像として埋め込んだ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(charset)、-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フォールバック: テキスト抽出結果がまるごと空の場合(スキャン文書全体)、ページ全体を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. 複数ファイルの一括処理
スキャン文書のフォルダ全体を一度に処理する場合、ファイル一つひとつに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のレンダリングは画像一枚あたりのトークン消費が少なくないため、数百ページの文書をまるごと処理する前に、数ページのサンプルで品質とコストをまず見極めるほうが安全だ。
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で扱う |
関連記事
- Building Agent-Operable Systems — 読める・検証できる・永続するという三条件、そして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 — extra一覧
変更履歴
- 2026-07-09: 初版 — Agent Operableのフレーミングを追加、OCR設定を後半の中心として拡張、verifierのプレースホルダー節を追加