本文へ移動
購読
GUIDE#ai-ml更新 2026.09.25

MarkItDownでPDF・WordをMarkdown化:Doclingと実測比較

自作の日本語PDF・Word・ExcelをMarkItDown 0.1.8とDocling 2.130で変換し、速度・メモリ・表と見出しの再現・OCRを実測。RAG取り込みの判断基準と、実際に出たエラーの対処も。

level
初級
time
約45分
stack
MarkItDown 0.1.8 / Docling 2.130.0 / Python 3.12 / Debian 13(CPUのみ)
author
@ryoma_t
// 前提条件
Python 3.10以上とpip・venvが使えること / ターミナルの基本操作 / メモリ2GB以上(Doclingを動かす場合)
ターミナルでMarkItDownとDoclingを同じサンプル文書に実行した結果。MarkItDownは表の見出し行が空になり、Doclingは項目・設定値・担当・備考の見出し付きの表を出力している
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

MarkItDownとは、Microsoftが公開しているPythonのOSSで、PDF・Word・Excel・PowerPointなどのファイルをMarkdown形式のテキストに変換するツールです(執筆時点の最新は0.1.8、MITライセンス)。レイアウト解析やOCRの機械学習モデルは持たず、形式ごとの抽出ライブラリで取り出した文字をMarkdownに並べ直す、軽い変換器です。対照的なのがDoclingで、IBM Research発のOSS(同じくMIT)がPDFのページ画像をレイアウト認識モデルと表構造モデルで読み取り、見出し・表・図の構造を復元します。

対象読者は、社内文書をRAG(検索拡張生成)に取り込む前処理を作る情シスや開発担当の方です。ここでは日本語と表を含むDOCX・XLSX・2ページのPDF、そのスキャン画像版を自分で作り、両方のツールで変換して処理時間・メモリ・表と見出しの再現度・OCRの結果を測りました。読み終えると、手元の文書の種類に応じてどちらを使うか、どのオプションで動かすかを決められます。

この記事でわかること

  • MarkItDownとDoclingの仕組みの違いと、それが表・見出し・OCRの結果にどう表れるか
  • MarkItDownのインストール、CLIとPython APIの使い方、出力の癖(空の見出し行、結合セルの列ずれ、数式セルのNaN)
  • DoclingをCPUだけで動かす手順(CPU版torch、モデルの事前取得、--no-ocr)と、実測の処理時間・メモリ
  • 同じ文書を変換した結果の差分と、スキャンPDFをRapidOCRで日本語OCRした結果
  • RAG取り込み用の判断マトリクスと、検証中に実際に出たエラー4つの対処

MarkItDownとDoclingの違い:仕組みが結果を決める

MarkItDownは、ファイル種別を判定したあと(Googleのmagikaという小さな判定モデルをonnxruntimeで動かします)、形式ごとの変換器にファイルを渡します。DOCXはmammothでいったんHTMLにしてからMarkdownに落とし、XLSXはpandasの read_excel(エンジンはopenpyxl)でシートごとに表を読み、PDFはpdfplumberで単語の座標を取り出し、3つ以上の塊に分かれた行から列の位置を割り出して、その列位置に2つ以上そろう行を表の行とみなす独自のヒューリスティックで組み立てます(表らしい行がページの2割未満なら、pdfminer.sixのテキスト抽出に戻ります)。文字の大きさは見ないので、PDFの見出しは判別できません。

対するDoclingは、PDFをdocling-parse(または --pdf-backend pypdfium2)でテキストと座標に分解し、ページ画像に対してレイアウト認識モデル(docling-layout-heron)で「見出し・本文・表・図・リスト項目」といった領域を検出し、表はTableFormerというモデルでセル構造を復元します。結果はDoclingDocumentという構造化データになり、そこからMarkdown・HTML・JSON・DocTags・チャンク(--to chunks)に書き出せます。DOCX・XLSX・PPTX・HTMLはモデルを使わず、ファイル構造を直接読むバックエンドで処理されます。ビットマップ部分はOCRエンジン(既定の自動選択ではRapidOCR、ほかにEasyOCRやTesseractを選択可)にかけられます。

観点MarkItDown 0.1.8Docling 2.130.0
変換の方式形式ごとの抽出ライブラリ → HTML → Markdown解析(座標)+レイアウト認識モデル+表構造モデル → 構造化データ → 各形式
PDFの見出し判別しない(ただの行)モデルが見出し領域を検出
PDFの表単語のX座標がそろった行を表とみなすTableFormerでセルの結合・階層を推定
スキャンPDF非対応(テキスト層がなければ空)OCRエンジンで文字化
出力形式Markdownのみmd / json / yaml / html / text / doctags / chunks など
主な入力形式PDF・DOCX・XLSX/XLS・PPTX・HTML・CSV・EPUB・Outlook msg・ipynb・zip、音声(文字起こし)PDF・DOCX/DOC・PPTX・XLSX・HTML・Markdown・CSV・画像・ODF・EPUB・音声/動画(ASR)など
依存関係純Python中心(52パッケージ、約410MB)torch・transformers・OpenCVを含む(107パッケージ、約1.8GB+モデル約0.7GB)
ライセンスMITMIT

要するに、MarkItDownは「テキスト層をそのまま出す」道具、Doclingは「紙面を見て構造を推定する」道具です。速度と軽さは前者、PDFの見出し・表・スキャン対応は後者が得意で、後述の実測もそのとおりの結果になりました。

検証環境とサンプル文書を自作する

検証は python:3.12-slim(Debian 13、Python 3.12.14)のコンテナで行い、CPUはAMD EPYC 7713の2コア分(--cpus 2)、メモリは1.5GB(--memory 1500m)に制限し、GPUは使っていません。この上限は「社内の小さな仮想マシンで動くか」を確かめるために意図的に低くしています。使ったバージョンは、MarkItDown 0.1.8(pdfminer.six 20260107、pdfplumber 0.11.10、mammoth 1.11.0、pandas 3.0.6)、Docling 2.130.0(docling-core 2.98.0、docling-ibm-models 4.0.3、docling-parse 7.21.0、rapidocr 3.9.2、onnxruntime 1.30.0、torch 2.14.0+cpu)です。

変換対象は、架空の「ノートPC入替 作業手順(例)」という社内文書を、python-docx・openpyxl・reportlabで生成しました。DOCXには見出し2階層、日本語の段落、4列×5行の表、番号付きリスト、PNG画像、縦に結合したセルを含む表を入れています。XLSXは「資産一覧」「集計」の2シートで、集計シートの最終行は =SUM() の数式です。PDFは同じ本文を2ページに分け、罫線つきの表を置きました。表の再現・見出しの検出・画像の扱い・数式セル・改ページをまとめて確認できる構成です。

make_samples.py
"""検証用のサンプル文書を自作する(内容はすべて架空の「例」)"""
from datetime import date

from PIL import Image, ImageDraw
from docx import Document
from docx.shared import Cm
from openpyxl import Workbook
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

ROWS = [["項目", "設定値", "担当", "備考"],
        ["ホスト名", "PC-2026-0412", "情シス", "資産番号と同じにする"],
        ["BitLocker", "有効", "情シス", "回復キーはEntra IDに保存"],
        ["ローカル管理者", "無効", "情シス", "LAPSで管理"],
        ["Office", "Microsoft 365 Apps", "利用者", "初回サインインで自動有効化"]]
INTRO = ("この手順書は、貸与ノートPCを入れ替えるときに情シスと利用者がそれぞれ行う作業をまとめたものです。"
         "作業は合計で約90分を見込み、旧PCの回収は新PCの初回サインイン確認後に行います。")
STEPS = ["資産管理台帳に新PCの資産番号とシリアル番号を登録する。",
         "Autopilotのデバイス一覧に登録されていることを確認する。",
         "利用者に新PCを渡し、初回サインインとMFAの登録を見届ける。",
         "旧PCのデータ移行が終わったら、旧PCを回収してワイプする。"]
NOTE = "旧PCのワイプは回収日の翌営業日までに実施し、台帳のステータスを「廃棄待ち」に更新してください。"

# 画像(棒グラフ風のPNG)を1枚
img = Image.new("RGB", (640, 240), "white")
d = ImageDraw.Draw(img)
for i, h in enumerate([90, 150, 200, 120]):
    d.rectangle([60 + i * 140, 220 - h, 140 + i * 140, 220], fill=(52, 101, 164))
d.line([40, 220, 620, 220], fill="black", width=2)
img.save("figure.png")

# DOCX:見出し、段落、表、番号付きリスト、画像、縦結合セルのある表
doc = Document()
doc.add_heading("ノートPC入替 作業手順(例)", level=1)
doc.add_paragraph(INTRO)
doc.add_heading("設定項目一覧", level=2)
t = doc.add_table(rows=len(ROWS), cols=4)
t.style = "Table Grid"
for r, row in enumerate(ROWS):
    for c, val in enumerate(row):
        t.cell(r, c).text = val
doc.add_heading("作業の流れ", level=2)
for s in STEPS:
    doc.add_paragraph(s, style="List Number")
doc.add_heading("作業時間の目安", level=2)
doc.add_picture("figure.png", width=Cm(10))
doc.add_paragraph("図1 工程ごとの所要時間(分)")
doc.add_heading("作業分担", level=2)
t2 = doc.add_table(rows=4, cols=3)
t2.style = "Table Grid"
for c, v in enumerate(["区分", "作業", "所要時間"]):
    t2.cell(0, c).text = v
t2.cell(1, 0).merge(t2.cell(2, 0)).text = "情シス"   # 2行分を縦に結合
t2.cell(1, 1).text, t2.cell(1, 2).text = "台帳登録とAutopilot確認", "20分"
t2.cell(2, 1).text, t2.cell(2, 2).text = "旧PCの回収とワイプ", "30分"
t2.cell(3, 0).text, t2.cell(3, 1).text, t2.cell(3, 2).text = "利用者", "初回サインインとデータ移行", "40分"
doc.add_heading("注意事項", level=2)
doc.add_paragraph(NOTE)
doc.save("sample.docx")

# XLSX:2シート。2枚目の最終行は数式セル
wb = Workbook()
ws = wb.active
ws.title = "資産一覧"
ws.append(["資産番号", "機種", "利用者", "部署", "導入日"])
ws.append(["PC-2026-0412", "ThinkPad L14", "佐藤", "総務部", date(2026, 4, 12)])
ws.append(["PC-2026-0413", "ThinkPad L14", "鈴木", "営業部", date(2026, 4, 12)])
ws.append(["PC-2026-0420", "Surface Laptop", "高橋", "経理部", date(2026, 4, 20)])
ws2 = wb.create_sheet("集計")
for row in [["部署", "台数"], ["総務部", 1], ["営業部", 1], ["経理部", 1], ["合計", "=SUM(B2:B4)"]]:
    ws2.append(row)
wb.save("sample.xlsx")

# PDF:TrueTypeアウトラインの日本語フォント(IPAexゴシック)を埋め込み、罫線つきの表を置く。2ページ
pdfmetrics.registerFont(TTFont("IPAexGothic", "/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf"))
h1 = ParagraphStyle("h1", fontName="IPAexGothic", fontSize=18, leading=24, spaceAfter=8)
h2 = ParagraphStyle("h2", fontName="IPAexGothic", fontSize=14, leading=20, spaceBefore=10, spaceAfter=6)
body = ParagraphStyle("body", fontName="IPAexGothic", fontSize=10.5, leading=16)
tbl = Table(ROWS, colWidths=[30 * mm, 40 * mm, 20 * mm, 60 * mm])
tbl.setStyle(TableStyle([("FONTNAME", (0, 0), (-1, -1), "IPAexGothic"), ("FONTSIZE", (0, 0), (-1, -1), 9.5),
                         ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
                         ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey)]))
story = [Paragraph("ノートPC入替 作業手順(例)", h1), Paragraph(INTRO, body), Spacer(1, 6),
         Paragraph("設定項目一覧", h2), tbl, Spacer(1, 6), Paragraph("作業の流れ", h2)]
story += [Paragraph(f"{i}. {s}", body) for i, s in enumerate(STEPS, 1)]
story += [PageBreak(), Paragraph("注意事項", h2), Paragraph(NOTE, body)]
SimpleDocTemplate("sample.pdf", pagesize=A4, leftMargin=20 * mm, rightMargin=20 * mm,
                  topMargin=20 * mm, bottomMargin=20 * mm).build(story)
print("sample.docx / sample.xlsx / sample.pdf を作成しました")

実行前に sudo apt-get install -y fonts-ipaexfont-gothic でフォントを入れ、後述のMarkItDown用の仮想環境に pip install python-docx openpyxl reportlab を追加します(Pillowはreportlabと一緒に入ります)。python make_samples.py を実行すると、38KBの sample.docx、6KBの sample.xlsx、47KBの sample.pdf ができます。

PDFの日本語フォントに fonts-noto-cjk のNotoSansCJK-Regular.ttcを指定すると、reportlab 5.0.1は TTFError: TTC file "...NotoSansCJK-Regular.ttc": postscript outlines are not supported で止まります。Noto CJKはCFF(PostScript)アウトラインのOpenTypeで、reportlabのTrueType埋め込みが扱えないためです。TrueTypeアウトラインのIPAexゴシック(fonts-ipaexfont-gothic、/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf)に替えると通ります。

スキャン文書の代わりには、この sample.pdf を200dpi相当でラスタライズし、画像だけのPDFに詰め直した scan.pdf(2ページ、1654×2339ピクセル、262KB)を使いました。描画にはDoclingの仮想環境に入っているpypdfium2を使い、Pillowで保存しています。

make_scan.py
import pypdfium2 as pdfium

pdf = pdfium.PdfDocument("sample.pdf")
pages = [pdf[i].render(scale=200 / 72).to_pil().convert("RGB") for i in range(len(pdf))]
pages[0].save("scan.pdf", save_all=True, append_images=pages[1:], resolution=200.0)

MarkItDownのインストールと使い方(CLIとPython API)

extrasは形式ごとに分かれていて(pdf・docx・xlsx・xls・pptx・outlook・audio-transcription・youtube-transcription・az-doc-intel など)、必要な形式だけ入れるか [all] でまとめて入れるかを選べます。今回は [all] にサンプル生成用のライブラリを足して、仮想環境のサイズが411MB(pip自身を含めて52パッケージ)、インストール時間は19.7秒でした。extrasを付けずに pip install markitdown だけ実行した環境でPDFを渡すと、FileConversionException の中に PdfConverter threw MissingDependencyException と出て変換できません。本体だけでは形式ごとの抽出ライブラリが入らないためで、最低でも "markitdown[pdf,docx,xlsx]" のように使う形式を指定してください。

terminal
mkdir -p ~/docs && cd ~/docs
python3 -m venv .venv-md
source .venv-md/bin/activate
pip install "markitdown[all]" python-docx openpyxl reportlab
markitdown --version
# markitdown 0.1.8

# CLI:標準出力に出す(先頭だけ見るなら head へ)/ -o でファイルに書く / 標準入力から読む(-x で拡張子のヒント)
markitdown sample.docx | head -n 9
markitdown sample.pdf -o sample.pdf.md
cat sample.xlsx | markitdown -x xlsx

CLIのオプションは少なく、手元で完結するのは -o(出力先)、-x/-m/-c(拡張子・MIMEタイプ・文字コードのヒント)、-p(--use-plugins、サードパーティ変換プラグインの有効化。--list-plugins で一覧)です。残りの -d(Azure AI Document Intelligence、-e でエンドポイント指定)と --use-cu(Azure Content Understanding)は変換をAzureに任せるオプションで、ファイルが社外に出ます。社内規程で外部送信を認めた文書以外には使わないでください。以下は sample.docx を変換した出力のうち、表と画像の部分です。

markitdown sample.docx(抜粋)
## 設定項目一覧

|  |  |  |  |
| --- | --- | --- | --- |
| 項目 | 設定値 | 担当 | 備考 |
| ホスト名 | PC-2026-0412 | 情シス | 資産番号と同じにする |
| BitLocker | 有効 | 情シス | 回復キーはEntra IDに保存 |

## 作業時間の目安

![](data:image/png;base64...)

図1 工程ごとの所要時間(分)

## 作業分担

|  |  |  |
| --- | --- | --- |
| 区分 | 作業 | 所要時間 |
| 情シス | 台帳登録とAutopilot確認 | 20分 |
| 旧PCの回収とワイプ | 30分 |
| 利用者 | 初回サインインとデータ移行 | 40分 |

見出しと番号付きリストはWordの階層どおりに出ました。一方で表には2つの癖があります。1つ目は、見出し行が空(| | | | |)になり、本来の見出し「項目・設定値・担当・備考」が本文の1行目に落ちていることです。mammothはWordで「タイトル行の繰り返し」(w:tblHeader)が設定された行だけを見出しセル(th)として扱うため、python-docxや手作業で作った普通の表は見出しなしになります。同じ表の1行目に w:tblHeader を付けて試すと、正しく | 項目 | 値 | が見出し行になりました。2つ目は、縦に結合したセルのある行で列が1つ消えることです。「旧PCの回収とワイプ」の行は2列しかなく、Markdown上では「区分」の列にずれて見えます。RAGに入れると「旧PCの回収とワイプ」が区分名として検索される恐れがあり、結合セルの多い様式(申請書、設定一覧)では要注意です。

画像は既定で ![](data:image/png;base64...) という短い印に置き換えられ、元データは捨てられます。convert(path, keep_data_uris=True) を渡すとbase64が丸ごと残り、今回の文書では770文字の出力が2,232文字に膨らみました。埋め込み検索の対象にする文字列としてはどちらも意味を持たないので、既定のままで問題ありません。

markitdown sample.xlsx
## 資産一覧
| 資産番号 | 機種 | 利用者 | 部署 | 導入日 |
| --- | --- | --- | --- | --- |
| PC-2026-0412 | ThinkPad L14 | 佐藤 | 総務部 | 2026-04-12 |
| PC-2026-0413 | ThinkPad L14 | 鈴木 | 営業部 | 2026-04-12 |
| PC-2026-0420 | Surface Laptop | 高橋 | 経理部 | 2026-04-20 |

## 集計
| 部署 | 台数 |
| --- | --- |
| 総務部 | 1.0 |
| 営業部 | 1.0 |
| 経理部 | 1.0 |
| 合計 | NaN |

XLSXはシート名が ## 見出しになり、1行目が見出し行として正しく出ます。日付は 2026-04-12 と短く整い、RAG用途では読みやすい形です。ただし数式セル =SUM(B2:B4) は NaN になり、その影響で同じ列の整数が 1.0 に変わっています。pandasは計算結果のキャッシュ値を読むだけで数式を計算しないため、openpyxlなどのスクリプトで生成したファイル(キャッシュ値を持たない)ではこうなります。Excelで保存したファイルは通常キャッシュ値を持つので値が入りますが、取り込み前に NaN を検索して確認する手順を入れておくと安全です。

markitdown sample.pdf(抜粋)
ノートPC入替 作業手順(例)
この手順書は、貸与ノートPCを入れ替えるときに情シスと利用者がそれぞれ行う作業をまとめたも
のです。作業は合計で約90分を見込み、旧PCの回収は新PCの初回サインイン確認後に行います。
設定項目一覧
| 項目        | 設定値                | 担当  | 備考               |
| --------- | ------------------ | --- | ---------------- |
| ホスト名      | PC-2026-0412       | 情シス | 資産番号と同じにする       |
| BitLocker | 有効                 | 情シス | 回復キーはEntra IDに保存 |

PDFでは見出しが判別されず、タイトルも節見出しもただの行です。段落はPDF上の折り返し位置で改行され、「まとめたも/のです」のように単語の途中で切れます。この改行はチャンク分割の境界に化けやすいので、後述の後処理で結合が要ります。その一方で、罫線つきの表は座標ヒューリスティックがうまく働き、Markdownの表として出ました。列幅がそろっていない表や、セル内で折り返す長文が入った表では崩れやすい方式なので、自社の代表的なPDFで必ず確認してください。

convert_md.py
import sys, time
from markitdown import MarkItDown

md = MarkItDown()  # enable_plugins=False が既定
for path in sys.argv[1:]:
    t0 = time.perf_counter()
    result = md.convert(path)
    dt = time.perf_counter() - t0
    print(f"{path}: {dt:.2f}s, {len(result.text_content)} chars, title={result.title!r}")
    print(result.text_content[:300])

Python APIは MarkItDown().convert(path) の1呼び出しで、戻り値の text_content がMarkdown文字列です(title は今回の3ファイルともNoneでした)。所要時間はDOCX 0.34秒、XLSX 0.04秒、PDF 0.13秒で、CLIで同じ変換をすると各1.5〜2.2秒かかりました。差の大半はプロセス起動とmagikaモデルの読み込みなので、数百ファイルを処理するときはCLIをループで呼ばず、1つのプロセスで MarkItDown インスタンスを使い回してください。メモリのピークは /usr/bin/time -v で165〜180MBでした。

DoclingをCPUだけでインストールして動かす

こちらはMarkItDownとは別の仮想環境に入れます。理由は依存の重さです。Linuxで何も考えずに pip install docling を実行すると、PyPIのtorchはCUDA対応ビルドなのでNVIDIA関連ライブラリまで一緒に落ちてきます(執筆時点)。GPUのないサーバーでは、先にPyTorch公式のCPU用インデックスからtorchを入れてからDoclingを入れると、torch 2.14.0+cpuとtorchvisionで999MB、Docling本体と依存を足した仮想環境全体で1.8GB(pip自身を含めて107パッケージ)に収まりました。インストール時間はtorchが35秒、Doclingが47秒です。docling は2.130ではメタパッケージで、実体は docling-slim[standard] です。OCRエンジンはextrasで選び、今回は既定のRapidOCRをonnxruntimeで動かす docling[rapidocr] を使いました(ほかに [easyocr]、[tesserocr]、[vlm] があります)。

terminal
# OpenCV(rapidocrの依存)が要求する共有ライブラリ。slim系イメージでは入っていない
sudo apt-get install -y libgl1 libglib2.0-0 libxcb1

cd ~/docs
python3 -m venv .venv-docling
source .venv-docling/bin/activate
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install "docling[rapidocr]"
pip show docling | grep ^Version
# Version: 2.130.0

# モデルを事前に取得する(レイアウト認識、表構造、日本語OCR)
docling-tools models download layout tableformer rapidocr --rapidocr-backend-lang onnxruntime:japan
# Models downloaded into: /home/ryoma/.cache/docling/models.
du -sh ~/.cache/docling/models
# 700M

# 取得したモデルの場所を毎回渡す(フラグでも環境変数でも可)
export DOCLING_ARTIFACTS_PATH=$HOME/.cache/docling/models

モデルは docling-tools models download で先に落としておきます。レイアウト認識のdocling-layout-heron(PyTorch版とONNX版で164MBずつ)とTableFormerを含むdocling-models(342MB)で669MB、ダウンロードは16.7秒でした。RapidOCRの日本語モデルは onnxruntime:japan の指定で31MB(PP-OCRv6 smallの検出・認識モデルと方向分類モデルのONNXファイル3つ)です。ここで指定する言語は「バックエンド:言語」の形で、BCP-47の ja を使うなら onnxruntime:iso:ja のように iso: を前置します。

事前取得したモデルは、--artifacts-path か環境変数 DOCLING_ARTIFACTS_PATH で場所を渡さないと使われません。渡さずに変換すると、DoclingはHugging Face Hubから同じモデルをもう一度 ~/.cache/huggingface に取得し(今回の実測で506MB)、ディスクに二重に置かれます。ネットワークを閉じた環境で動かすなら HF_HUB_OFFLINE=1 も併せて設定し、実際にオフラインで1回変換して確かめてください。

terminal
# テキスト層のあるPDF:OCRを切って変換(2.130 では docling convert でも docling FILE でも同じ)
time docling sample.pdf --to md --output out --no-ocr 2>/dev/null
head -n 9 out/sample.md

# DOCX:画像はbase64ではなく位置だけ残す
docling sample.docx --to md --output out-docx --image-export-mode placeholder

# スキャンPDF:OCRあり(既定の自動選択で rapidocr + onnxruntime)。言語はPP-OCRの表記で japan
docling scan.pdf --to md --output out-scan --ocr-lang japan

# 必要に応じて:ページ範囲、スレッド数、表構造モデルの高速モード、JSON出力
docling big.pdf --to md --to json --output out --page-range 1-5 --num-threads 2 --table-mode fast

主なオプションは、--to(複数指定可)、--output(ディレクトリ。既定はカレント)、--ocr/--no-ocr(既定はOCRあり)、--ocr-engine(既定 auto)、--ocr-lang、--image-export-mode placeholder|embedded|referenced(既定は embedded)、--table-mode fast|accurate(既定 accurate)、--num-threads(既定4)、--device auto|cpu|cuda|mps|xpu、--page-range、--pdf-password です。sample.pdf の出力は次のようになりました。

out/sample.md(Docling、抜粋)
## ノートPC入替 作業手順(例)

この手順書は、貸与ノートPCを入れ替えるときに情シスと利用者がそれぞれ行う作業をまとめたも のです。作業は合計で約90分を見込み、旧PCの回収は新PCの初回サインイン確認後に行います。

## 設定項目一覧

| 項目        | 設定値                | 担当   | 備考               |
|-----------|--------------------|------|------------------|
| ホスト名      | PC-2026-0412       | 情シス  | 資産番号と同じにする       |
| BitLocker | 有効                 | 情シス  | 回復キーはEntra IDに保存 |

## 作業の流れ

1. 資産管理台帳に新PCの資産番号とシリアル番号を登録する。
2. Autopilotのデバイス一覧に登録されていることを確認する。

タイトルと節見出しが ## として検出され、表は見出し行つき、番号付きリストもMarkdownのリストになり、2ページ目の「注意事項」も見出しとして続きました。PDFの変換にかかった時間は、ログの Finished converting document sample.pdf in 11.37 sec. と2回目の 9.10 sec. で、プロセス全体では25.2秒と24.6秒、メモリのピークは1.22GBです。一方で日本語の折り返しは「まとめたも のです」のように半角スペースで結合されます。行末で単語が切れない英語向けの結合規則がそのまま適用されるためで、こちらも後処理が要ります。

out-docx/sample.md(Docling、抜粋)
### 作業時間の目安

<!-- image -->

図1 工程ごとの所要時間(分)

### 作業分担

| 区分   | 作業               | 所要時間   |
|------|------------------|--------|
| 情シス  | 台帳登録とAutopilot確認 | 20分    |
| 情シス  | 旧PCの回収とワイプ       | 30分    |
| 利用者  | 初回サインインとデータ移行    | 40分    |

DOCXでは縦結合セルの値「情シス」が両方の行に展開され、列ずれは起きません。見出し行も正しく出ます。気を付けたいのは見出しの階層で、Wordの見出し1が ##、見出し2が ### と1段深く出ます(MarkItDownは #/##)。両方のツールを混ぜて使うなら、見出しレベルをそろえる正規化を入れてください。画像は既定の embedded だとbase64が本文に埋め込まれ、今回の1枚の小さなPNGだけで出力が3,221バイト、placeholder にすると <!-- image --> の1行になって1,739バイトでした。埋め込み検索に画像のbase64は不要なので、RAG用途は placeholder か、画像を別途扱うなら referenced(PNGファイルとして書き出し)を選びます。DOCXの変換自体は0.16〜0.26秒ですが、CLIのプロセス全体では12〜15秒、ピークメモリは約500MBかかりました。torchやtransformersの読み込みがDOCXの変換でも走るためです。

sample.xlsx → Docling(抜粋)
| 資産番号         | 機種             | 利用者   | 部署   | 導入日                 |
|--------------|----------------|-------|------|---------------------|
| PC-2026-0412 | ThinkPad L14   | 佐藤    | 総務部  | 2026-04-12 00:00:00 |

| 部署   |   台数 |
|------|------|
| 総務部  |    1 |
| 合計   |      |

XLSXはシート名が出ず、表が2つ続けて並びます。日付は 2026-04-12 00:00:00 と時刻つき、数式セルは空欄で、整数は整数のまま(MarkItDownは 1.0)です。シート名を文脈として残したい台帳系のファイルは、MarkItDownの出力のほうがそのまま使えます。

Doclingの出力ファイル名は入力ファイルの拡張子を除いた名前(sample.md)になります。sample.docx と sample.xlsx を同じ --output に変換したところ、後から書いたXLSXの結果でDOCXの結果が黙って上書きされました。拡張子違いの同名ファイルがある文書フォルダでは、出力先を入力ごとに分けるか、Python APIで自分でファイル名を決めてください。

convert_docling.py
import os, sys, time
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption

opts = PdfPipelineOptions(artifacts_path=os.path.expanduser("~/.cache/docling/models"))
opts.do_ocr = False  # テキスト層のあるPDF向け。スキャンは別経路でOCRをかける
converter = DocumentConverter(
    format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=opts)}
)
for path in sys.argv[1:]:
    t0 = time.perf_counter()
    doc = converter.convert(path).document
    dt = time.perf_counter() - t0
    md = doc.export_to_markdown()  # image_mode や見出しの扱いは引数で調整できる
    print(f"{path}: {dt:.2f}s, {len(md)} chars, tables={len(doc.tables)}, pictures={len(doc.pictures)}")

Python APIでは DocumentConverter を1つ作って使い回します。今回の計測では sample.pdf が11.65〜15.71秒(2回計測。初回の呼び出しなのでモデル読み込みを含む)、続く sample.docx が0.15〜0.23秒で、DOCXから表2つと画像1つが doc.tables・doc.pictures として取れました。運用では、ワーカープロセスを常駐させて変換要求を受ける形にし、1ワーカーあたり2GB以上のメモリを見込んでください。CPU 2コアでの目安は「PDF 2ページで10秒前後」なので、数千ページの規程集は夜間バッチで前もって変換し、検索時に変換しない設計にします。

同じ文書の変換結果を比べる:表・見出し・画像・日本語・OCR

確認項目MarkItDown 0.1.8Docling 2.130.0
DOCXの見出しWordどおり #/##1段深い ##/###
DOCXの表の見出し行「タイトル行の繰り返し」がない表は空の見出し行1行目が見出し行
DOCXの縦結合セル結合された行の列が1つ消える(列ずれ)値を各行に展開
DOCXの画像![](data:image/png;base64...) の印(keep_data_uris で全量)既定はbase64埋め込み、placeholder で <!-- image -->
PDFの見出し判別なしタイトル・節とも ##
PDFの罫線つき表座標ヒューリスティックで表になった(1行目が見出し)TableFormerで表になった(1行目が見出し)
PDFの番号付きリスト「1. …」のただの行Markdownの番号付きリスト
PDFの日本語の折り返し行の途中で改行が残る半角スペースで結合される
XLSXシート名が見出し、数式セルは NaN、整数が 1.0シート名なし、数式セルは空、日付に 00:00:00
スキャンPDF(画像のみ)出力は空(1バイト)、エラーなしOCRで文字化(19.4秒、ピーク1.48GB)

日本語に関しては、どちらの出力も段落の途中に折り返し由来のノイズが残ります。ノイズの形は、MarkItDownのPDF出力が改行、Doclingが半角スペースです。埋め込みモデルはこの程度のノイズには比較的強いものの、キーワード検索やハイライト表示では「まとめたも のです」が一致しなくなるので、日本語文字の間にだけ入ったスペースを消す後処理を1行入れておきます。以下の strip_cjk_spaces はDoclingの出力で確認済みで、「Entra IDに保存」のような英単語間のスペースは残ります。改行のほうは行の長さで判定してつなぎます。今回のPDFは本文1行が45〜46文字、見出しは4〜15文字だったので、30文字以上あって句点で終わらない行だけを次の行と結合する join_wrapped_lines で、見出しを巻き込まずに2つの段落が1行ずつに戻りました。本文の文字数が違う様式では min_len を1行の6〜7割に合わせます。

fix_ja.py
import re

CJK = r"[぀-ヿ一-鿿 -〿!-⦆]"  # かな・漢字・全角記号

def strip_cjk_spaces(text: str) -> str:
    """日本語文字どうしの間に入った半角スペースだけを取り除く"""
    return re.sub(rf"(?<={CJK}) +(?={CJK})", "", text)

# MarkItDownのPDF出力向け:行幅いっぱいまで埋まって折り返された行だけを次の行とつなぐ。
# min_len は本文1行の文字数の6〜7割に合わせる(見出しのような短い行は結合しない)
def join_wrapped_lines(text: str, min_len: int = 30) -> str:
    out, prev = [], ""
    for line in text.splitlines():
        structural = not line.strip() or line.startswith(("|", "#", "- ")) or re.match(r"\d+\. ", line)
        wrapped = (len(prev) >= min_len and not prev.startswith(("|", "#"))
                   and not prev.endswith(("。", "!", "?", "|")))
        if out and wrapped and not structural:
            out[-1] += line
        else:
            out.append(line)
        prev = line
    return "\n".join(out)

スキャンPDFの結果は明確に分かれました。scan.pdf を渡すと、MarkItDownは改行1文字だけを返し、エラーも警告も出しません。取り込みパイプラインでは「出力が空なら別経路へ」という判定を必ず入れてください。OCRを有効にしたDoclingは、RapidOCR(onnxruntime、PP-OCRv6 small)で2ページを19.41秒で処理し、プロセス全体では34.2秒、ピークメモリ1.48GBでした。見出しと表の構造は復元されましたが、文字レベルでは次のような誤りが出ています。

out-scan/scan.md(Docling OCR、抜粋)
## ノ一トPC入替作業手順 (例)

| 項目        | 設定值                | 担当   | 備考              |
|-----------|--------------------|------|-----------------|
| BitLocker | 有効                 | 情シス  | 回復キ一はEntraIDに保存 |

2. Autopilotのデバイスー覧に登録されていることを確認する。
4. 4.旧PCのデータ移行が終わったら、旧PCを回収してワイプする。

長音「ー」と漢数字「一」の取り違え(「ノ一ト」「回復キ一」「デバイスー覧」)、「値」が中国語の字形「值」になる、「Entra ID」の空白が消える、リストの番号「4.」が重複する、といった典型的な日本語OCRの誤りです。人が読む用途なら十分ですが、資産番号や金額のような正確さが要る値を検索する用途では、OCR結果をそのまま正とせず元画像へのリンクを残す設計にしてください。EasyOCRやTesseract(jpn の学習データが必要)、VLMパイプライン(--pipeline vlm)への切り替えもできますが、今回は検証していないので、精度と負荷は自社の文書で測ってから選んでください(執筆時点)。

RAG取り込み用の判断マトリクス

基準MarkItDown 0.1.8Docling 2.130.0有利
速度(PDF 2ページ、CPU 2コア)API 0.13秒/CLI 1.47秒変換9.1〜11.4秒/CLI全体25秒MarkItDown
速度(DOCX)API 0.34秒/CLI 2.22秒変換0.16〜0.26秒/CLI全体12〜15秒API同士なら同等
メモリのピーク165〜180MBDOCX 0.5GB、PDF 1.22GB、OCR 1.48GBMarkItDown
表の再現(DOCX)見出し行なし、結合セルで列ずれ見出し行あり、結合セル展開Docling
表の再現(PDF)座標のそろった表のみモデルでセル構造を推定Docling
見出し(PDF)なしあり(チャンク分割の境界に使える)Docling
スキャンPDF/OCR不可(空出力)RapidOCR・EasyOCR・Tesseract・VLMDocling
日本語の扱い折り返しの改行が残る折り返しがスペース結合どちらも後処理が要る
インストールサイズ411MB(52パッケージ)1.8GB+モデル0.70GB(107パッケージ)MarkItDown
オフライン運用追加設定なしモデルの事前取得と DOCLING_ARTIFACTS_PATH が必要MarkItDown
ライセンスMITMIT同じ

判断の目安は3つです。1つ目、文書の大半がMicrosoft 365由来のDOCX・XLSX・PPTXで、PDFも「Wordから書き出したテキスト層つき」に限られるなら、MarkItDownのPython APIだけで十分です。1ファイル0.1〜0.4秒、メモリ200MB以下なので、既存の小さなバッチサーバーに同居できます。表の見出し行と結合セルの癖は、取り込み前に上の表で確認した様式だけ人手で直すか、Doclingへ回します。

2つ目、規程集・ベンダー資料・スキャンした契約書のようにPDFが中心で、表や見出し単位の検索精度が問われるなら、Doclingを選びます。テキスト層のあるPDFは --no-ocr で変換し、スキャンだけOCRの経路に流します。1ワーカー2GB以上のメモリと「2ページ10秒」の処理時間を見込んで、夜間バッチで前もって変換しておく設計にしてください。GPUを積んだ検証機があれば --device cuda で短縮できますが、この記事の数値はCPUのものです。

3つ目、多くの現場で現実的なのは併用です。拡張子で振り分け、DOCX・XLSX・PPTXはMarkItDown、PDFはDoclingへ送り、MarkItDownの出力が空だったPDFはDoclingのOCR経路へ回します。どちらの出力にも strip_cjk_spaces のような日本語の後処理をかけ、見出しレベルをそろえてから、見出し単位でチャンクに分けます。チャンクの作り方とアクセス権のフィルタはpgvectorで作る権限フィルタ付きの社内文書RAGで扱っており、変換品質が検索精度にどう効くかの原則はRAGの仕組みと社内導入の設計原則にまとめています。

つまずきポイント

ImportError: libxcb.so.1: cannot open shared object file

DoclingでPDFを変換した最初の実行が、この ImportError で止まりました。rapidocrが依存するopencv-python(cv2)が、X11やOpenGLの共有ライブラリを要求するためで、python:3.12-slim のような最小イメージや、GUIのないサーバーには入っていません。apt-get install -y libgl1 libglib2.0-0 libxcb1 で解消し、python -c "import cv2" が通ることを確認してから再実行します。同じ実行で WARNING ... No OCR engine found. Please review the install details. も出ていましたが、これはcv2の読み込み失敗が原因でRapidOCRが候補から外れたためで、ライブラリを入れると消えました。

FileNotFoundError: RapidOCR artifacts not found or incomplete in artifacts_path

docling-tools models download layout tableformer でレイアウトと表のモデルだけ取得した状態で --artifacts-path を渡して変換すると、OCRが既定で有効なため、この例外になります。対処は2つで、テキスト層のあるPDFなら --no-ocr を付ける、OCRが必要なら rapidocr --rapidocr-backend-lang onnxruntime:japan をモデル取得に加えることです。言語の書き方にも落とし穴があり、torch:ja と書くと Invalid RapidOCR model spec 'torch:ja' で拒否されました。BCP-47の言語コードは iso:ja のように前置し、PP-OCR固有の japan はそのまま書きます。また --ocr-engine rapidocr を明示するとonnxruntimeのモデルが前提になるので、onnxruntimeを入れていない環境では docling[rapidocr] を入れ直してください。

OCR中にプロセスが終了コード137で消える

RapidOCRをtorchバックエンド(.pth モデル)で動かしたとき、メモリ上限1.5GBのコンテナでは scan.pdf の処理中にプロセスがコード137で終了しました。/usr/bin/time -v の最大常駐サイズは1,506,412KBで、上限に達してOOMキラーに落とされた形です。onnxruntimeバックエンドに切り替えると同じ文書がピーク1.48GBで完走したので、CPUのOCRはonnxruntimeを使い、それでも足りない場合は --page-range で分割する、スキャンの解像度を150dpi程度に落とす、コンテナの上限を2GB以上にする、の順に試してください。原因の確定には、ホストで journalctl -k | grep -i 'out of memory' を実行します。今回は Memory cgroup out of memory: Killed process ... (docling) の行が残っており、コンテナのメモリ上限(cgroup)による強制終了だと分かりました。コンテナの主プロセスとしてDoclingを動かしている場合は、docker inspect の State.OOMKilled でも確認できます。

MarkItDownがスキャンPDFに対して何も返さない

画像だけのPDFを渡しても、MarkItDownはエラーにならず、改行1文字(1バイト)を返します。CLIでは画面に何も出ないだけなので、バッチで数百ファイルを処理していると空の取り込みに気付きません。Python側で if not result.text_content.strip(): の判定を入れ、空だったファイルはDoclingのOCR経路に回すか、Azure AI Document Intelligence(-d オプション、社外送信になる点に注意)へ送る設計にします。テキスト層があるのに極端に文字数が少ないPDF(図面や表紙だけの文書)も同じ判定で拾えます。

動作確認

両方の環境が正しく動いているかは、次のコマンドで確かめられます。期待する出力をコメントで添えています。

terminal
cd ~/docs
ls
# make_samples.py の実行直後なら:figure.png  make_samples.py  sample.docx  sample.pdf  sample.xlsx

# MarkItDown:バージョンと、XLSXの1シート目の先頭
.venv-md/bin/markitdown --version
# markitdown 0.1.8
.venv-md/bin/markitdown sample.xlsx | head -n 3
# ## 資産一覧
# | 資産番号 | 機種 | 利用者 | 部署 | 導入日 |
# | --- | --- | --- | --- | --- |

# Docling:モデルの配置と、PDFの見出し検出
ls ~/.cache/docling/models
# RapidOcr  docling-project--docling-layout-heron  docling-project--docling-layout-heron-onnx  docling-project--docling-models
export DOCLING_ARTIFACTS_PATH=$HOME/.cache/docling/models HF_HUB_OFFLINE=1
.venv-docling/bin/docling sample.pdf --to md --output out --no-ocr 2>/dev/null
grep -c '^## ' out/sample.md
# 4

# Docling API:DOCXから表2つと画像1つが取れる
.venv-docling/bin/python -c "from docling.document_converter import DocumentConverter; d = DocumentConverter().convert('sample.docx').document; print(len(d.tables), len(d.pictures))"
# 2 1

見出し数を数える grep -c '^## ' が4になるのは、タイトル・設定項目一覧・作業の流れ・注意事項の4つが見出しとして検出されたことを意味します。0や1なら、モデルの読み込みに失敗して見出し検出が働いていないか、--artifacts-path の指定が違っている可能性が高いので、2>/dev/null を外してログの WARNING 行を読んでください。

よくある質問

MarkItDownとは何ですか?

Microsoftが公開しているPythonのOSSで、PDF・Word・Excel・PowerPoint・HTML・CSV・EPUB・Outlookのmsgなどを、LLMに渡しやすいMarkdownテキストに変換します。pip install "markitdown[all]" で入り、markitdown ファイル名 のCLIと MarkItDown().convert() のAPIで使えます。文字を取り出して並べ直す方式なので、レイアウト解析やOCRはしません(執筆時点の最新は0.1.8)。

Excelの表をマークダウンに変換するには?

markitdown 台帳.xlsx -o 台帳.md で、シートごとに ## シート名 の見出しとMarkdownの表になります。1行目が見出し行として扱われ、日付は 2026-04-12 の形で出ます。数式セルは保存時のキャッシュ値が読まれるため、スクリプトで生成した未計算のファイルでは NaN になります。結合セルや複数段の見出しがある表は、Excel側で1行1レコードの形に整えてから変換したほうが検索に使いやすくなります。

Markdownの何がそんなにいいのですか?

見出し(#)、表(|)、リスト(-、1.)の構造が短い記号で残るため、LLMに渡すときのトークン数が少なく、チャンク分割の境界を見出しで決められます。HTMLのようにタグが本文より多くなることがなく、プレーンテキストのように表の構造が失われることもありません。今回のサンプルでは、DOCXがMarkItDownで770文字、Doclingで941文字のMarkdownになりました。

Markdownとはどういうプログラムですか?

Markdownはプログラムではなく、テキストに # や * などの記号で構造を示す記法(軽量マークアップ言語)です。拡張子は .md で、中身はただのテキストなので、メモ帳でもVS Codeでも開けます。一方、MarkItDownやDoclingは「ほかの形式からMarkdownを書き出すプログラム」で、書き出したファイルはWordのようにレイアウトを持たず、表示側(GitHub、Wiki、LLM)が記号を解釈します。

まとめ

自作の日本語文書で測った結果、MarkItDownはDOCX 0.34秒・PDF 0.13秒・メモリ200MB以下と圧倒的に軽い一方、PDFの見出しを出せず、Wordの表は見出し行なしと結合セルの列ずれが起きました。DoclingはPDF 2ページに9〜11秒・1.22GB(OCRありで1.48GB)を使いますが、見出し・表・リストを構造ごと復元し、スキャンPDFもRapidOCRで文字化できます。Office文書中心ならMarkItDownのAPI、PDFと表が中心ならDocling、迷うなら拡張子で振り分ける併用が現実的です。次の一歩として、自社の代表的な文書を3つ選び、この記事の make_samples.py の代わりにそれらを両方のツールで変換して、表と見出しがどう出るかを自分の目で確かめてください。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。